Compare commits

..

26 Commits

Author SHA1 Message Date
James Coglan dbedb370b7 Bump version to 0.3.5. 2014-07-06 10:18:19 +01:00
James Coglan 28858d9fb3 Delegate the protocol and version methods correctly in the TCP driver. 2014-07-04 17:49:11 +01:00
James Coglan ae52cf995e Stop the Hybi parser hanging onto data it doesn't need after emitFrame(). 2014-06-16 20:30:44 +01:00
James Coglan dfe7b2b000 Trailing blank lines are bad. 2014-05-17 00:35:31 +01:00
James Coglan d40de878b2 Bump version to 0.3.4. 2014-05-08 02:19:58 +01:00
James Coglan ed3907d5fd Clean up the state management code in StreamReader. 2014-05-07 20:49:49 +01:00
James Coglan 84b6f50f1a Merge pull request #6 from meteor/forget-finished-buffers
StreamReader: Don't hang on to fully read buffers
2014-05-07 20:41:15 +01:00
David Glasser 8d19ee823e StreamReader: Don't hang on to fully read buffers 2014-05-06 11:52:59 -07:00
James Coglan 3bee0366c5 Bump version to 0.3.3. 2014-04-24 23:31:43 +01:00
James Coglan 9f2782da14 Correct the draft-76 status line reason phrase. 2014-04-17 02:23:58 +01:00
James Coglan c752c76712 Bump version to 0.3.2. 2013-12-29 12:21:49 +00:00
James Coglan ad8f08f19e Make Node 0.6 work on Travis. 2013-12-28 17:40:18 +00:00
James Coglan d343c7d21b Document the new lower frame size limit. 2013-12-28 16:56:15 +00:00
James Coglan 0b089ad921 Merge pull request #5 from Zarel/patch-1
Lower default max buffer length
2013-12-27 10:17:09 -08:00
James Coglan e65e837968 Stop parsing Hybi when we go into a failure state. 2013-12-27 18:16:41 +00:00
Guangcong Luo cda22e3bef Lower default max buffer length
Real-world testing shows that the previous max buffer length
of ~1GB was too high, and messages of the size of roughly 300MB
could still cause the process to crash from running out of
memory. 64MB seems like a reasonably conservative maximum.
2013-12-24 19:26:38 -06:00
James Coglan 1ae37d6efe Remove an unused instance variable _lengthBuffer. 2013-12-20 10:20:41 +00:00
James Coglan 82c42a6ce5 Extend max-length checking to draft-{75,76}. 2013-12-20 10:09:31 +00:00
James Coglan aad1519f3f Extend max-length checking to short (<= 125 bytes) frames and to sequences of continuation frames. 2013-12-20 09:50:18 +00:00
James Coglan 11a9b75185 Bump version to 0.3.1. 2013-12-03 00:46:42 +00:00
James Coglan d93c853414 Don't pre-allocate a huge Buffer before we know there's enough chunks in the read queue to complete a message. 2013-12-02 21:07:33 +00:00
James Coglan 2dff35d3e4 Map Node v0.11's magic numbers for HTTP methods. 2013-12-02 13:02:38 +00:00
James Coglan 6588928445 Make HttpParser work on v0.11.6+. 2013-11-29 00:42:49 +00:00
James Coglan 151fddd206 Make the maximum frame length into an option that the user can set. 2013-11-28 23:38:42 +00:00
James Coglan 0b1f16a7ee Merge pull request #2 from meteor/limit-frame-to-max-buffer
Reduce frame max length to max Node Buffer length.
2013-11-28 15:25:05 -08:00
David Glasser f15b331a34 Reduce frame max length to max Node Buffer length. 2013-10-11 19:21:14 -07:00
22 changed files with 167 additions and 99 deletions
+2
View File
@@ -6,3 +6,5 @@ node_js:
- "0.10"
- "0.11"
before_install:
- '[ "${TRAVIS_NODE_VERSION}" = "0.6" ] && npm conf set strict-ssl false || true'
+25 -1
View File
@@ -1,3 +1,28 @@
### 0.3.5 / 2014-07-06
* Don't hold references to frame buffers after a message has been emitted
* Make sure that `protocol` and `version` are exposed properly by the TCP driver
### 0.3.4 / 2014-05-08
* Don't hold memory-leaking references to I/O buffers after they have been parsed
### 0.3.3 / 2014-04-24
* Correct the draft-76 status line reason phrase
### 0.3.2 / 2013-12-29
* 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
### 0.3.1 / 2013-12-03
* Add a `maxLength` option to limit allowed frame size
* Don't pre-allocate a message buffer until the whole frame has arrived
* Fix compatibility with Node v0.11 `HTTPParser`
### 0.3.0 / 2013-09-09
* Support client URLs with Basic Auth credentials
@@ -19,4 +44,3 @@
### 0.1.0 / 2013-05-04
* First stable release
+3 -2
View File
@@ -173,6 +173,8 @@ with masking enabled on outgoing frames.
The `options` argument is optional, and is an object. It may contain the
following fields:
* `maxLength` - the maximum allowed size of incoming message frames, in bytes.
The default value is `2^26 - 1`, or 1 byte short of 64 MiB.
* `protocols` - an array of strings representing acceptable subprotocols for
use over the socket. The driver will negotiate one of these to use via the
`Sec-WebSocket-Protocol` header if supported by the other peer.
@@ -283,7 +285,7 @@ after `emit('open')` has fired.
(The MIT License)
Copyright (c) 2010-2013 James Coglan
Copyright (c) 2010-2014 James Coglan
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the 'Software'), to deal in
@@ -302,4 +304,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-1
View File
@@ -18,4 +18,3 @@ var server = net.createServer(function(connection) {
});
server.listen(process.argv[2]);
-1
View File
@@ -41,4 +41,3 @@ var Driver = {
};
module.exports = Driver;
+5 -1
View File
@@ -8,6 +8,7 @@ var Base = function(request, url, options) {
this._request = request;
this._options = options || {};
this._maxLength = this._options.maxLength || this.MAX_LENGTH;
this.__headers = new Headers();
this.__queue = [];
this.readyState = 0;
@@ -20,6 +21,10 @@ var Base = function(request, url, options) {
util.inherits(Base, Emitter);
var instance = {
// This is 64MB, small enough for an average VPS to handle without
// crashing from process out of memory
MAX_LENGTH: 0x3ffffff,
STATES: ['connecting', 'open', 'closing', 'closed'],
_bindEventListeners: function() {
@@ -118,4 +123,3 @@ Base.MessageEvent = function(data) {
};
module.exports = Base;
-1
View File
@@ -109,4 +109,3 @@ for (var key in instance)
Client.prototype[key] = instance[key];
module.exports = Client;
+11 -3
View File
@@ -9,7 +9,16 @@ var Draft75 = function(request, url, options) {
util.inherits(Draft75, Base);
var instance = {
close: function() {
if (this.readyState === 3) return false;
this.readyState = 3;
this.emit('close', new Base.CloseEvent(null, null));
return true;
},
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];
@@ -29,8 +38,7 @@ var instance = {
this._length = value + 128 * this._length;
if (this._closing && this._length === 0) {
this.readyState = 3;
this.emit('close', new Base.CloseEvent(null, null));
return this.close();
}
else if ((0x80 & data) !== 0x80) {
if (this._length === 0) {
@@ -56,6 +64,7 @@ var instance = {
this._stage = 0;
} else {
this._buffer.push(data);
if (this._buffer.length > this._maxLength) return this.close();
}
}
break;
@@ -106,4 +115,3 @@ for (var key in instance)
Draft75.prototype[key] = instance[key];
module.exports = Draft75;
+1 -2
View File
@@ -48,7 +48,7 @@ var instance = {
},
_handshakeResponse: function() {
return new Buffer('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
return new Buffer('HTTP/1.1 101 WebSocket Protocol Handshake\r\n' +
'Upgrade: WebSocket\r\n' +
'Connection: Upgrade\r\n' +
'Sec-WebSocket-Origin: ' + this._request.headers.origin + '\r\n' +
@@ -106,4 +106,3 @@ for (var key in instance)
Draft76.prototype[key] = instance[key];
module.exports = Draft76;
-1
View File
@@ -27,4 +27,3 @@ Headers.prototype._strip = function(string) {
};
module.exports = Headers;
+27 -4
View File
@@ -22,8 +22,8 @@ var HttpParser = function(type) {
self.headers[current] = b.toString('utf8', start, start + length);
};
this._parser.onHeadersComplete = function(info) {
self.method = info.method;
this._parser.onHeadersComplete = this._parser[HTTPParser.kOnHeadersComplete] = function(info) {
self.method = (typeof info.method === 'number') ? HttpParser.METHODS[info.method] : info.method;
self.statusCode = info.statusCode;
self.url = info.url;
@@ -34,11 +34,35 @@ var HttpParser = function(type) {
self.headers[headers[i].toLowerCase()] = headers[i+1];
};
this._parser.onMessageComplete = function() {
this._parser.onMessageComplete = this._parser[HTTPParser.kOnMessageComplete] = function() {
self._complete = true;
};
};
HttpParser.METHODS = {
0: 'DELETE',
1: 'GET',
2: 'HEAD',
3: 'POST',
4: 'PUT',
5: 'CONNECT',
6: 'OPTIONS',
7: 'TRACE',
8: 'COPY',
9: 'LOCK',
10: 'MKCOL',
11: 'MOVE',
12: 'PROPFIND',
13: 'PROPPATCH',
14: 'SEARCH',
15: 'UNLOCK',
16: 'REPORT',
17: 'MKACTIVITY',
18: 'CHECKOUT',
19: 'MERGE',
24: 'PATCH'
};
HttpParser.prototype.isComplete = function() {
return this._complete;
};
@@ -54,4 +78,3 @@ HttpParser.prototype.parse = function(data) {
};
module.exports = HttpParser;
+68 -50
View File
@@ -7,26 +7,33 @@ var Hybi = function(request, url, options) {
Base.apply(this, arguments);
this._reset();
this._reader = new Reader();
this._stage = 0;
this._masking = this._options.masking;
this._protocols = this._options.protocols || [];
this._reader = new Reader();
this._stage = 0;
this._masking = this._options.masking;
this._protocols = this._options.protocols || [];
this._requireMasking = this._options.requireMasking;
this._pingCallbacks = {};
if (typeof this._protocols === 'string')
this._protocols = this._protocols.split(/\s*,\s*/);
this._requireMasking = this._options.requireMasking;
this._pingCallbacks = {};
if (!this._request) return;
if (!this.version) {
var version = this._request.headers['sec-websocket-version'];
this.version = 'hybi-' + version;
var protos = this._request.headers['sec-websocket-protocol'],
supported = this._protocols;
if (protos !== undefined) {
if (typeof protos === 'string') protos = protos.split(/\s*,\s*/);
this.protocol = protos.filter(function(p) { return supported.indexOf(p) >= 0 })[0];
}
var version = this._request.headers['sec-websocket-version'];
this.version = 'hybi-' + version;
};
util.inherits(Hybi, Base);
Hybi.mask = function(payload, mask, offset) {
if (mask.length === 0) return payload;
if (!mask || mask.length === 0) return payload;
offset = offset || 0;
for (var i = 0, n = payload.length - offset; i < n; i++) {
@@ -66,7 +73,6 @@ var instance = {
FRAGMENTED_OPCODES: [0, 1, 2],
OPENING_OPCODES: [1, 2],
MAX_LENGTH: Math.pow(2, 53) - 1,
TWO_POWERS: [0, 1, 2, 3, 4, 5, 6, 7].map(function(n) { return Math.pow(2, 8 * n) }),
ERRORS: {
@@ -119,11 +125,13 @@ var instance = {
case 4:
buffer = this._reader.read(this._length);
if (buffer) {
this._payload = buffer;
this._emitFrame();
this._emitFrame(buffer);
this._stage = 0;
}
break;
default:
buffer = null;
}
}
},
@@ -218,26 +226,15 @@ var instance = {
var secKey = this._request.headers['sec-websocket-key'];
if (!secKey) return '';
var accept = Hybi.generateAccept(secKey),
protos = this._request.headers['sec-websocket-protocol'],
supported = this._protocols,
proto,
var headers = [
'HTTP/1.1 101 Switching Protocols',
'Upgrade: websocket',
'Connection: Upgrade',
'Sec-WebSocket-Accept: ' + Hybi.generateAccept(secKey)
];
headers = [
'HTTP/1.1 101 Switching Protocols',
'Upgrade: websocket',
'Connection: Upgrade',
'Sec-WebSocket-Accept: ' + accept
];
if (protos !== 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);
}
}
if (this.protocol)
headers.push('Sec-WebSocket-Protocol: ' + this.protocol);
return new Buffer(headers.concat(this.__headers.toString(), '').join('\r\n'), 'utf8');
},
@@ -245,6 +242,7 @@ var instance = {
_shutdown: function(code, reason) {
this.frame(reason, 'close', code);
this.readyState = 3;
this._stage = 5;
this.emit('close', new Base.CloseEvent(code, reason));
},
@@ -266,8 +264,6 @@ var instance = {
this._final = (data & this.FIN) === this.FIN;
this._opcode = (data & this.OPCODE);
this._mask = [];
this._payload = [];
if (this.OPCODE_CODES.indexOf(this._opcode) < 0)
return this._fail('protocol_error', 'Unrecognized frame opcode: ' + this._opcode);
@@ -289,11 +285,11 @@ var instance = {
this._length = (data & this.LENGTH);
if (this._length >= 0 && this._length <= 125) {
if (!this._checkFrameLength()) return;
this._stage = this._masked ? 3 : 4;
} else {
this._lengthBuffer = [];
this._lengthSize = (this._length === 126 ? 2 : 8);
this._stage = 2;
this._lengthSize = (this._length === 126 ? 2 : 8);
this._stage = 2;
}
},
@@ -303,21 +299,32 @@ var instance = {
if (this.FRAGMENTED_OPCODES.indexOf(this._opcode) < 0 && this._length > 125)
return this._fail('protocol_error', 'Received control frame having too long payload: ' + this._length);
if (this._length > this.MAX_LENGTH)
return this._fail('too_large', 'WebSocket frame length too large');
if (!this._checkFrameLength()) return;
this._stage = this._masked ? 3 : 4;
},
_emitFrame: function() {
var payload = Hybi.mask(this._payload, this._mask),
_checkFrameLength: function() {
if (this.__blength + this._length > this._maxLength) {
this._fail('too_large', 'WebSocket frame length too large');
return false;
} else {
return true;
}
},
_emitFrame: function(buffer) {
var payload = Hybi.mask(buffer, this._mask),
isFinal = this._final,
opcode = this._opcode;
this._final = this._opcode = this._length = this._lengthSize = this._masked = this._mask = null;
if (opcode === this.OPCODES.continuation) {
if (!this._mode) return this._fail('protocol_error', 'Received unexpected continuation frame');
this._buffer(payload);
if (this._final) {
var message = new Buffer(this.__buffer);
if (isFinal) {
var message = this._concatBuffer();
if (this._mode === 'text') message = this._encode(message);
this._reset();
if (message === null)
@@ -327,7 +334,7 @@ var instance = {
}
}
else if (opcode === this.OPCODES.text) {
if (this._final) {
if (isFinal) {
var message = this._encode(payload);
if (message === null)
this._fail('encoding_error', 'Could not decode a text frame as UTF-8');
@@ -339,7 +346,7 @@ var instance = {
}
}
else if (opcode === this.OPCODES.binary) {
if (this._final) {
if (isFinal) {
this.emit('message', new Base.MessageEvent(payload));
} else {
this._mode = 'binary';
@@ -374,13 +381,25 @@ var instance = {
},
_buffer: function(fragment) {
for (var i = 0, n = fragment.length; i < n; i++)
this.__buffer.push(fragment[i]);
this.__buffer.push(fragment);
this.__blength += fragment.length;
},
_concatBuffer: function() {
var buffer = new Buffer(this.__blength),
offset = 0;
for (var i = 0, n = this.__buffer.length; i < n; i++) {
this.__buffer[i].copy(buffer, offset);
offset += this.__buffer[i].length;
}
return buffer;
},
_reset: function() {
this._mode = null;
this.__buffer = [];
this._mode = null;
this.__buffer = [];
this.__blength = 0;
},
_encode: function(buffer) {
@@ -403,4 +422,3 @@ for (var key in instance)
Hybi.prototype[key] = instance[key];
module.exports = Hybi;
+17 -18
View File
@@ -1,41 +1,40 @@
var StreamReader = function() {
this._queue = [];
this._cursor = 0;
};
StreamReader.prototype.read = function(bytes) {
return this._readBuffer(bytes);
this._queue = [];
this._queueSize = 0;
this._cursor = 0;
};
StreamReader.prototype.put = function(buffer) {
if (!buffer || buffer.length === 0) return;
if (!buffer.copy) buffer = new Buffer(buffer);
this._queue.push(buffer);
this._queueSize += buffer.length;
};
StreamReader.prototype._readBuffer = function(length) {
StreamReader.prototype.read = function(length) {
if (length > this._queueSize) return null;
var buffer = new Buffer(length),
queue = this._queue,
remain = length,
n = queue.length,
i = 0,
chunk, offset, size;
if (remain === 0) return buffer;
chunk, size;
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;
size = Math.min(remain, chunk.length - this._cursor);
chunk.copy(buffer, length - remain, this._cursor, this._cursor + size);
remain -= size;
this._queueSize -= size;
this._cursor = (this._cursor + size) % chunk.length;
i += 1;
}
if (remain > 0) return null;
queue.splice(0, i-1);
this._cursor = (i === 1 ? this._cursor : 0) + size;
queue.splice(0, this._cursor === 0 ? i : i - 1);
return buffer;
};
+3 -1
View File
@@ -40,6 +40,9 @@ var instance = {
this._delegate.on(event, function(e) { self.emit(event, e) });
}, this);
this.protocol = this._delegate.protocol;
this.version = this._delegate.version;
this.parse(this._http.body);
this.emit('connect', new Base.ConnectEvent());
},
@@ -101,4 +104,3 @@ Server.http = function(request, options) {
};
module.exports = Server;
-1
View File
@@ -141,4 +141,3 @@ Messages.prototype.destroy = function() {};
exports.IO = IO;
exports.Messages = Messages;
+1 -2
View File
@@ -5,7 +5,7 @@
, "keywords" : ["websocket"]
, "license" : "MIT"
, "version" : "0.3.0"
, "version" : "0.3.5"
, "engines" : {"node": ">=0.4.0"}
, "main" : "./lib/websocket/driver"
, "devDependencies" : {"jstest": ""}
@@ -18,4 +18,3 @@
, "bugs" : "http://github.com/faye/websocket-driver-node/issues"
}
-1
View File
@@ -42,4 +42,3 @@ require('./websocket/driver/draft75_spec')
require('./websocket/driver/draft76_spec')
require('./websocket/driver/hybi_spec')
require('./websocket/driver/client_spec')
-1
View File
@@ -249,4 +249,3 @@ test.describe("Client", function() { with(this) {
}})
}})
}})
@@ -112,4 +112,3 @@ test.describe("draft-75", function() { with(this) {
}})
}})
}})
-1
View File
@@ -97,4 +97,3 @@ test.describe("Draft75", function() { with(this) {
itShouldBehaveLike("draft-75 protocol")
}})
+3 -4
View File
@@ -53,7 +53,7 @@ test.describe("Draft76", function() { with(this) {
describe("start", function() { with(this) {
it("writes the handshake response to the socket", function() { with(this) {
expect(driver().io, "emit").given("data", buffer(
"HTTP/1.1 101 Web Socket Protocol Handshake\r\n" +
"HTTP/1.1 101 WebSocket Protocol Handshake\r\n" +
"Upgrade: WebSocket\r\n" +
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Origin: http://www.example.com\r\n" +
@@ -95,7 +95,7 @@ test.describe("Draft76", function() { with(this) {
it("queues the frames until the handshake has been sent", function() { with(this) {
expect(driver().io, "emit").given("data", buffer(
"HTTP/1.1 101 Web Socket Protocol Handshake\r\n" +
"HTTP/1.1 101 WebSocket Protocol Handshake\r\n" +
"Upgrade: WebSocket\r\n" +
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Origin: http://www.example.com\r\n" +
@@ -116,7 +116,7 @@ test.describe("Draft76", function() { with(this) {
it("writes the handshake response with no body", function() { with(this) {
expect(driver().io, "emit").given("data", buffer(
"HTTP/1.1 101 Web Socket Protocol Handshake\r\n" +
"HTTP/1.1 101 WebSocket Protocol Handshake\r\n" +
"Upgrade: WebSocket\r\n" +
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Origin: http://www.example.com\r\n" +
@@ -184,4 +184,3 @@ test.describe("Draft76", function() { with(this) {
}})
}})
}})
+1 -2
View File
@@ -308,7 +308,7 @@ test.describe("Hybi", function() { with(this) {
}})
it("returns an error for too-large frames", function() { with(this) {
driver().parse([0x81, 0x7f, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
driver().parse([0x81, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00])
assertEqual( "WebSocket frame length too large", error.message )
assertEqual( [1009, "WebSocket frame length too large"], close )
assertEqual( "closed", driver().getState() )
@@ -539,4 +539,3 @@ test.describe("Hybi", function() { with(this) {
}})
}})
}})