Compare commits

...

32 Commits

Author SHA1 Message Date
James Coglan 7953bc39ff Implement masking as a transform stream. 2014-05-11 22:07:34 +01:00
James Coglan bf39b7b44b Optimise some special cases in StreamReader; return a new Buffer from read() immediately if there's enough buffered data, and if the read can be fulfilled entirely from the first queued buffer than take a slice rather than copying into a new buffer. 2014-05-11 13:33:51 +01:00
James Coglan 85ba40ac32 Convert StreamReader so that it emits streams rather than waiting for an entire payload. 2014-05-09 15:35:40 +01:00
James Coglan e6edb23681 Rather than a sync read() method in a loop, use an async read() method with a callback. 2014-05-08 16:35:21 +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
James Coglan 95261a1779 Bump version to 0.3.0. 2013-09-09 20:15:10 +01:00
James Coglan 12f9f4d444 Add support for Basic Auth URLs to the client driver. 2013-08-13 17:16:49 +01:00
James Coglan d3e81b478e Bump version to 0.2.2. 2013-07-05 15:16:37 +01:00
James Coglan c5b3df986b Migrate to jstest. 2013-07-01 02:10:29 +01:00
James Coglan 286dea4337 Write the reflexive pipe examples as one-liners. [http://nodejsreactions.tumblr.com/post/51566814133/src-pipe-dst-pipe-src] 2013-05-29 02:24:27 +01:00
James Coglan 2378f4c484 An example in the README was missing a closing paren. 2013-05-17 12:46:35 +01:00
19 changed files with 574 additions and 248 deletions
+3
View File
@@ -6,3 +6,6 @@ node_js:
- "0.10" - "0.10"
- "0.11" - "0.11"
before_install:
- '[ "${TRAVIS_NODE_VERSION}" = "0.6" ] && npm conf set strict-ssl false || true'
+28
View File
@@ -1,3 +1,31 @@
### 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
### 0.2.2 / 2013-07-05
* No functional changes, just updates to package.json
### 0.2.1 / 2013-05-17 ### 0.2.1 / 2013-05-17
* Export the isSecureRequest() method since faye-websocket relies on it * Export the isSecureRequest() method since faye-websocket relies on it
+7 -8
View File
@@ -62,15 +62,14 @@ server.on('upgrade', function(request, socket, body) {
var driver = websocket.http(request); var driver = websocket.http(request);
driver.io.write(body); driver.io.write(body);
socket.pipe(driver.io); socket.pipe(driver.io).pipe(socket);
driver.io.pipe(socket);
driver.messages.on('data', function(message) { driver.messages.on('data', function(message) {
console.log('Got a message', message); console.log('Got a message', message);
}); });
driver.start(); driver.start();
}; });
``` ```
Note the line `driver.io.write(body)` - you must pass the `body` buffer to the Note the line `driver.io.write(body)` - you must pass the `body` buffer to the
@@ -104,8 +103,7 @@ var server = net.createServer(function(connection) {
driver.on('close', function() { connection.end() }); driver.on('close', function() { connection.end() });
connection.on('error', function() {}); connection.on('error', function() {});
connection.pipe(driver.io); connection.pipe(driver.io).pipe(connection);
driver.io.pipe(connection);
driver.messages.pipe(driver.messages); driver.messages.pipe(driver.messages);
}); });
@@ -138,8 +136,7 @@ var net = require('net'),
var driver = websocket.client('ws://www.example.com/socket'), var driver = websocket.client('ws://www.example.com/socket'),
tcp = net.createConnection(80, 'www.example.com'); tcp = net.createConnection(80, 'www.example.com');
tcp.pipe(driver.io); tcp.pipe(driver.io).pipe(tcp);
driver.io.pipe(tcp);
driver.messages.on('data', function(message) { driver.messages.on('data', function(message) {
console.log('Got a message', message); console.log('Got a message', message);
@@ -176,6 +173,8 @@ with masking enabled on outgoing frames.
The `options` argument is optional, and is an object. It may contain the The `options` argument is optional, and is an object. It may contain the
following fields: 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 * `protocols` - an array of strings representing acceptable subprotocols for
use over the socket. The driver will negotiate one of these to use via the 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. `Sec-WebSocket-Protocol` header if supported by the other peer.
@@ -286,7 +285,7 @@ after `emit('open')` has fired.
(The MIT License) (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 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 this software and associated documentation files (the 'Software'), to deal in
+5
View File
@@ -8,6 +8,7 @@ var Base = function(request, url, options) {
this._request = request; this._request = request;
this._options = options || {}; this._options = options || {};
this._maxLength = this._options.maxLength || this.MAX_LENGTH;
this.__headers = new Headers(); this.__headers = new Headers();
this.__queue = []; this.__queue = [];
this.readyState = 0; this.readyState = 0;
@@ -20,6 +21,10 @@ var Base = function(request, url, options) {
util.inherits(Base, Emitter); util.inherits(Base, Emitter);
var instance = { 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'], STATES: ['connecting', 'open', 'closing', 'closed'],
_bindEventListeners: function() { _bindEventListeners: function() {
+4 -1
View File
@@ -17,7 +17,7 @@ util.inherits(Client, Hybi);
Client.generateKey = function() { Client.generateKey = function() {
var buffer = new Buffer(16), i = buffer.length; var buffer = new Buffer(16), i = buffer.length;
while (i--) buffer[i] = Math.floor(Math.random() * 256); while (i--) buffer[i] = ~~(Math.random() * 256);
return buffer.toString('base64'); return buffer.toString('base64');
}; };
@@ -53,6 +53,9 @@ var instance = {
if (this._protocols.length > 0) if (this._protocols.length > 0)
headers.push('Sec-WebSocket-Protocol: ' + this._protocols.join(', ')); headers.push('Sec-WebSocket-Protocol: ' + this._protocols.join(', '));
if (uri.auth)
headers.push('Authorization: Basic ' + new Buffer(uri.auth, 'utf8').toString('base64'));
return new Buffer(headers.concat(this.__headers.toString(), '').join('\r\n'), 'utf8'); return new Buffer(headers.concat(this.__headers.toString(), '').join('\r\n'), 'utf8');
}, },
+11 -2
View File
@@ -9,7 +9,16 @@ var Draft75 = function(request, url, options) {
util.inherits(Draft75, Base); util.inherits(Draft75, Base);
var instance = { 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) { parse: function(buffer) {
if (this.readyState > 1) return;
var data, message, value; var data, message, value;
for (var i = 0, n = buffer.length; i < n; i++) { for (var i = 0, n = buffer.length; i < n; i++) {
data = buffer[i]; data = buffer[i];
@@ -29,8 +38,7 @@ var instance = {
this._length = value + 128 * this._length; this._length = value + 128 * this._length;
if (this._closing && this._length === 0) { if (this._closing && this._length === 0) {
this.readyState = 3; return this.close();
this.emit('close', new Base.CloseEvent(null, null));
} }
else if ((0x80 & data) !== 0x80) { else if ((0x80 & data) !== 0x80) {
if (this._length === 0) { if (this._length === 0) {
@@ -56,6 +64,7 @@ var instance = {
this._stage = 0; this._stage = 0;
} else { } else {
this._buffer.push(data); this._buffer.push(data);
if (this._buffer.length > this._maxLength) return this.close();
} }
} }
break; break;
+1 -1
View File
@@ -48,7 +48,7 @@ var instance = {
}, },
_handshakeResponse: function() { _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' + 'Upgrade: WebSocket\r\n' +
'Connection: Upgrade\r\n' + 'Connection: Upgrade\r\n' +
'Sec-WebSocket-Origin: ' + this._request.headers.origin + '\r\n' + 'Sec-WebSocket-Origin: ' + this._request.headers.origin + '\r\n' +
+27 -3
View File
@@ -22,8 +22,8 @@ var HttpParser = function(type) {
self.headers[current] = b.toString('utf8', start, start + length); self.headers[current] = b.toString('utf8', start, start + length);
}; };
this._parser.onHeadersComplete = function(info) { this._parser.onHeadersComplete = this._parser[HTTPParser.kOnHeadersComplete] = function(info) {
self.method = info.method; self.method = (typeof info.method === 'number') ? HttpParser.METHODS[info.method] : info.method;
self.statusCode = info.statusCode; self.statusCode = info.statusCode;
self.url = info.url; self.url = info.url;
@@ -34,11 +34,35 @@ var HttpParser = function(type) {
self.headers[headers[i].toLowerCase()] = headers[i+1]; self.headers[headers[i].toLowerCase()] = headers[i+1];
}; };
this._parser.onMessageComplete = function() { this._parser.onMessageComplete = this._parser[HTTPParser.kOnMessageComplete] = function() {
self._complete = true; 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() { HttpParser.prototype.isComplete = function() {
return this._complete; return this._complete;
}; };
+78 -89
View File
@@ -1,14 +1,15 @@
var crypto = require('crypto'), var crypto = require('crypto'),
util = require('util'), util = require('util'),
Base = require('./base'), Base = require('./base'),
Reader = require('./hybi/stream_reader'); Concat = require('./hybi/concat'),
Mask = require('./hybi/mask'),
Reader = require('./hybi/stream_reader');
var Hybi = function(request, url, options) { var Hybi = function(request, url, options) {
Base.apply(this, arguments); Base.apply(this, arguments);
this._reset(); this._reset();
this._reader = new Reader(); this._reader = new Reader({context: this});
this._stage = 0;
this._masking = this._options.masking; this._masking = this._options.masking;
this._protocols = this._options.protocols || []; this._protocols = this._options.protocols || [];
@@ -22,19 +23,11 @@ var Hybi = function(request, url, options) {
var version = this._request.headers['sec-websocket-version']; var version = this._request.headers['sec-websocket-version'];
this.version = 'hybi-' + version; this.version = 'hybi-' + version;
} }
this._reader.read(1, this._parseOpcode);
}; };
util.inherits(Hybi, Base); util.inherits(Hybi, Base);
Hybi.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;
};
Hybi.generateAccept = function(key) { Hybi.generateAccept = function(key) {
var sha1 = crypto.createHash('sha1'); var sha1 = crypto.createHash('sha1');
sha1.update(key + Hybi.GUID); sha1.update(key + Hybi.GUID);
@@ -66,7 +59,6 @@ var instance = {
FRAGMENTED_OPCODES: [0, 1, 2], FRAGMENTED_OPCODES: [0, 1, 2],
OPENING_OPCODES: [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) }), TWO_POWERS: [0, 1, 2, 3, 4, 5, 6, 7].map(function(n) { return Math.pow(2, 8 * n) }),
ERRORS: { ERRORS: {
@@ -89,43 +81,7 @@ var instance = {
UTF8_MATCH: /^([\x00-\x7F]|[\xC2-\xDF][\x80-\xBF]|\xE0[\xA0-\xBF][\x80-\xBF]|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}|\xED[\x80-\x9F][\x80-\xBF]|\xF0[\x90-\xBF][\x80-\xBF]{2}|[\xF1-\xF3][\x80-\xBF]{3}|\xF4[\x80-\x8F][\x80-\xBF]{2})*$/, UTF8_MATCH: /^([\x00-\x7F]|[\xC2-\xDF][\x80-\xBF]|\xE0[\xA0-\xBF][\x80-\xBF]|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}|\xED[\x80-\x9F][\x80-\xBF]|\xF0[\x90-\xBF][\x80-\xBF]{2}|[\xF1-\xF3][\x80-\xBF]{3}|\xF4[\x80-\x8F][\x80-\xBF]{2})*$/,
parse: function(data) { parse: function(data) {
this._reader.put(data); this._reader.write(data);
var buffer = true;
while (buffer) {
switch (this._stage) {
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: function(data, type, code) { frame: function(data, type, code) {
@@ -152,31 +108,31 @@ var instance = {
frame[1] = masked | length; frame[1] = masked | length;
} else if (length <= 65535) { } else if (length <= 65535) {
frame[1] = masked | 126; frame[1] = masked | 126;
frame[2] = Math.floor(length / 256); frame[2] = ~~(length / 256);
frame[3] = length & BYTE; frame[3] = length & BYTE;
} else { } else {
frame[1] = masked | 127; frame[1] = masked | 127;
frame[2] = Math.floor(length / Math.pow(2,56)) & BYTE; frame[2] = ~~(length / Math.pow(2, 56)) & BYTE;
frame[3] = Math.floor(length / Math.pow(2,48)) & BYTE; frame[3] = ~~(length / Math.pow(2, 48)) & BYTE;
frame[4] = Math.floor(length / Math.pow(2,40)) & BYTE; frame[4] = ~~(length / Math.pow(2, 40)) & BYTE;
frame[5] = Math.floor(length / Math.pow(2,32)) & BYTE; frame[5] = ~~(length / Math.pow(2, 32)) & BYTE;
frame[6] = Math.floor(length / Math.pow(2,24)) & BYTE; frame[6] = ~~(length / Math.pow(2, 24)) & BYTE;
frame[7] = Math.floor(length / Math.pow(2,16)) & BYTE; frame[7] = ~~(length / Math.pow(2, 16)) & BYTE;
frame[8] = Math.floor(length / Math.pow(2,8)) & BYTE; frame[8] = ~~(length / Math.pow(2, 8)) & BYTE;
frame[9] = length & BYTE; frame[9] = length & BYTE;
} }
if (code) { if (code) {
frame[offset] = Math.floor(code / 256) & BYTE; frame[offset] = ~~(code / 256) & BYTE;
frame[offset+1] = code & BYTE; frame[offset + 1] = code & BYTE;
} }
buffer.copy(frame, offset + insert); buffer.copy(frame, offset + insert);
if (this._masking) { if (this._masking) {
mask = [Math.floor(Math.random() * 256), Math.floor(Math.random() * 256), mask = [~~(Math.random() * 256), ~~(Math.random() * 256),
Math.floor(Math.random() * 256), Math.floor(Math.random() * 256)]; ~~(Math.random() * 256), ~~(Math.random() * 256)];
new Buffer(mask).copy(frame, header); new Buffer(mask).copy(frame, header);
Hybi.mask(frame, mask, offset); Mask.mask(frame, mask, offset);
} }
this._write(frame); this._write(frame);
@@ -245,6 +201,7 @@ var instance = {
_shutdown: function(code, reason) { _shutdown: function(code, reason) {
this.frame(reason, 'close', code); this.frame(reason, 'close', code);
this.readyState = 3; this.readyState = 3;
this._reader.end();
this.emit('close', new Base.CloseEvent(code, reason)); this.emit('close', new Base.CloseEvent(code, reason));
}, },
@@ -253,7 +210,9 @@ var instance = {
this._shutdown(this.ERRORS[type], message); this._shutdown(this.ERRORS[type], message);
}, },
_parseOpcode: function(data) { _parseOpcode: function(buffer) {
var data = buffer[0];
var rsvs = [this.RSV1, this.RSV2, this.RSV3].map(function(rsv) { var rsvs = [this.RSV1, this.RSV2, this.RSV3].map(function(rsv) {
return (data & rsv) === rsv; return (data & rsv) === rsv;
}); });
@@ -264,10 +223,9 @@ var instance = {
', reserved2 = ' + (rsvs[1] ? 1 : 0) + ', reserved2 = ' + (rsvs[1] ? 1 : 0) +
', reserved3 = ' + (rsvs[2] ? 1 : 0)); ', reserved3 = ' + (rsvs[2] ? 1 : 0));
this._final = (data & this.FIN) === this.FIN; this._final = (data & this.FIN) === this.FIN;
this._opcode = (data & this.OPCODE); this._opcode = (data & this.OPCODE);
this._mask = []; this._mask = null;
this._payload = [];
if (this.OPCODE_CODES.indexOf(this._opcode) < 0) if (this.OPCODE_CODES.indexOf(this._opcode) < 0)
return this._fail('protocol_error', 'Unrecognized frame opcode: ' + this._opcode); return this._fail('protocol_error', 'Unrecognized frame opcode: ' + this._opcode);
@@ -278,10 +236,12 @@ var instance = {
if (this._mode && this.OPENING_OPCODES.indexOf(this._opcode) >= 0) if (this._mode && this.OPENING_OPCODES.indexOf(this._opcode) >= 0)
return this._fail('protocol_error', 'Received new data frame but previous continuous frame is unfinished'); return this._fail('protocol_error', 'Received new data frame but previous continuous frame is unfinished');
this._stage = 1; this._reader.read(1, this._parseLength);
}, },
_parseLength: function(data) { _parseLength: function(buffer) {
var data = buffer[0];
this._masked = (data & this.MASK) === this.MASK; this._masked = (data & this.MASK) === this.MASK;
if (this._requireMasking && !this._masked) if (this._requireMasking && !this._masked)
return this._fail('unacceptable', 'Received unmasked frame but masking is required'); return this._fail('unacceptable', 'Received unmasked frame but masking is required');
@@ -289,11 +249,11 @@ var instance = {
this._length = (data & this.LENGTH); this._length = (data & this.LENGTH);
if (this._length >= 0 && this._length <= 125) { if (this._length >= 0 && this._length <= 125) {
this._stage = this._masked ? 3 : 4; if (!this._checkFrameLength()) return;
this._readMask();
} else { } else {
this._lengthBuffer = []; var lengthSize = (this._length === 126 ? 2 : 8);
this._lengthSize = (this._length === 126 ? 2 : 8); this._reader.read(lengthSize, this._parseExtendedLength);
this._stage = 2;
} }
}, },
@@ -303,21 +263,47 @@ var instance = {
if (this.FRAGMENTED_OPCODES.indexOf(this._opcode) < 0 && this._length > 125) 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); return this._fail('protocol_error', 'Received control frame having too long payload: ' + this._length);
if (this._length > this.MAX_LENGTH) if (!this._checkFrameLength()) return;
return this._fail('too_large', 'WebSocket frame length too large');
this._stage = this._masked ? 3 : 4; this._readMask();
}, },
_emitFrame: function() { _checkFrameLength: function() {
var payload = Hybi.mask(this._payload, this._mask), if (this.__blength + this._length > this._maxLength) {
opcode = this._opcode; this._fail('too_large', 'WebSocket frame length too large');
return false;
} else {
return true;
}
},
_readMask: function() {
if (this._masked)
this._reader.read(4, function(buffer) {
this._mask = new Mask(buffer);
this._readPayload();
});
else
this._readPayload();
},
_readPayload: function() {
var stream = this._reader.fork(this._length);
if (this._mask) {
stream.pipe(this._mask);
stream = this._mask;
}
stream.pipe(new Concat(this._emitFrame, this));
},
_emitFrame: function(payload) {
var opcode = this._opcode;
if (opcode === this.OPCODES.continuation) { if (opcode === this.OPCODES.continuation) {
if (!this._mode) return this._fail('protocol_error', 'Received unexpected continuation frame'); if (!this._mode) return this._fail('protocol_error', 'Received unexpected continuation frame');
this._buffer(payload); this._buffer(payload);
if (this._final) { if (this._final) {
var message = new Buffer(this.__buffer); var message = Concat.concatBuffers(this.__buffer, this.__blength);
if (this._mode === 'text') message = this._encode(message); if (this._mode === 'text') message = this._encode(message);
this._reset(); this._reset();
if (message === null) if (message === null)
@@ -371,16 +357,19 @@ var instance = {
delete callbacks[message]; delete callbacks[message];
if (callback) callback() if (callback) callback()
} }
this._reader.read(1, this._parseOpcode);
}, },
_buffer: function(fragment) { _buffer: function(fragment) {
for (var i = 0, n = fragment.length; i < n; i++) this.__buffer.push(fragment);
this.__buffer.push(fragment[i]); this.__blength += fragment.length;
}, },
_reset: function() { _reset: function() {
this._mode = null; this._mode = null;
this.__buffer = []; this.__buffer = [];
this.__blength = 0;
}, },
_encode: function(buffer) { _encode: function(buffer) {
+45
View File
@@ -0,0 +1,45 @@
var Stream = require('stream').Stream,
util = require('util');
var Concat = function(callback, context) {
this._callback = callback;
this._context = context;
this._chunks = [];
this._size = 0;
this.writable = true;
};
util.inherits(Concat, Stream);
Concat.prototype.write = function(buffer) {
if (!this.writable) return false;
this._chunks.push(buffer);
this._size += buffer.length;
return true;
};
Concat.prototype.end = function(buffer) {
if (buffer) this.write(buffer);
this.writable = false;
this._callback.call(this._context, Concat.concatBuffers(this._chunks, this._size));
};
Concat.concatBuffers = function(chunks, size) {
if (size === undefined) {
size = 0;
var c = chunks.length;
while (c--) size += chunks[c].length;
}
var concat = new Buffer(size),
offset = 0;
for (var i = 0, n = chunks.length; i < n; i++) {
chunks[i].copy(concat, offset);
offset += chunks[i].length;
}
return concat;
};
Concat.prototype.destroy = function() {};
module.exports = Concat;
+43
View File
@@ -0,0 +1,43 @@
var Stream = require('stream').Stream,
util = require('util');
var Mask = function(bytes) {
this.readable = this.writable = true;
this._bytes = bytes;
this._index = 0;
};
util.inherits(Mask, Stream);
Mask.mask = function(payload, mask, offset, index) {
offset = offset || 0;
index = index || 0;
for (var i = 0, n = payload.length - offset; i < n; i++)
payload[offset + i] ^= mask[(index + i) % 4];
};
Mask.prototype.write = function(chunk) {
Mask.mask(chunk, this._bytes, 0, this._index);
this._index = (this._index + chunk.length) % 4;
this.emit('data', chunk);
return !this._paused;
};
Mask.prototype.end = function(chunk) {
if (chunk) this.write(chunk);
this.readable = this.writable = false;
this.emit('end');
};
Mask.prototype.pause = function() {
this._paused = true;
};
Mask.prototype.resume = function() {
this._paused = false;
this.emit('drain');
};
module.exports = Mask;
+117 -22
View File
@@ -1,41 +1,136 @@
var StreamReader = function() { var Stream = require('stream').Stream,
this._queue = []; Concat = require('./concat'),
this._cursor = 0; util = require('util');
};
StreamReader.prototype.read = function(bytes) { var defer = (typeof setImmediate === 'function')
return this._readBuffer(bytes); ? setImmediate
}; : process.nextTick;
var StreamReader = function(options, parent) {
this.readable = !!parent;
this.writable = !parent;
this._streams = [];
this._context = options.context;
this._parent = parent;
this._queue = [];
this._queueSize = 0;
this._cursor = 0;
};
util.inherits(StreamReader, Stream);
StreamReader.prototype.write = function(buffer) {
if (!this.writable) return false;
if (!buffer || buffer.length === 0) return !this._paused;
StreamReader.prototype.put = function(buffer) {
if (!buffer || buffer.length === 0) return;
if (!buffer.copy) buffer = new Buffer(buffer); if (!buffer.copy) buffer = new Buffer(buffer);
this._queue.push(buffer); this._queue.push(buffer);
this._queueSize += buffer.length;
this._flush();
return !this._paused;
}; };
StreamReader.prototype._readBuffer = function(length) { StreamReader.prototype.end = function(buffer) {
var buffer = new Buffer(length), if (buffer) this.write(buffer);
queue = this._queue, this.writable = false;
for (var i = 0, n = this._streams.length; i < n; i++) {
this._streams[i].emit('end');
this._streams[i].readable = false;
}
this._context = this._streams = this._queue = [];
};
StreamReader.prototype.pause = function() {
this._paused = true;
if (this._parent) this._parent.pause();
};
StreamReader.prototype.resume = function() {
this._paused = false;
this.emit('drain');
if (this._parent) this._parent.resume();
};
StreamReader.prototype.fork = function(length) {
if (!this.writable) return null;
var stream = new StreamReader({context: this._context}, this),
self = this;
stream._remaining = length;
this._streams.push(stream);
defer(function() { self._flush() });
return stream;
};
StreamReader.prototype.read = function(length, callback) {
if (!this.writable) return;
if (this._queueSize >= length)
return callback.call(this._context, this._readBytes(length));
this.fork(length).pipe(new Concat(callback, this._context));
};
StreamReader.prototype._flush = function() {
var streams = this._streams, stream, size, buffer;
while (streams.length > 0) {
stream = streams[0];
size = Math.min(stream._remaining, this._queueSize);
buffer = this._readBytes(size);
if (size > 0) stream.emit('data', buffer);
stream._remaining -= size;
if (stream._remaining > 0) break;
stream.readable = false;
stream.emit('end');
streams.shift();
}
};
StreamReader.prototype._readBytes = function(length) {
var queue = this._queue,
remain = length, remain = length,
n = queue.length, n = queue.length,
first = queue[0],
i = 0, i = 0,
chunk, offset, size; buffer, chunk, size;
if (remain === 0) return buffer; if (length === 0) return new Buffer(0);
if (remain <= first.length - this._cursor) {
buffer = first.slice(this._cursor, this._cursor + remain);
this._queueSize -= remain;
this._cursor = (this._cursor + remain) % first.length;
if (this._cursor === 0) this._queue.shift();
return buffer;
}
buffer = new Buffer(length);
while (remain > 0 && i < n) { while (remain > 0 && i < n) {
chunk = queue[i]; chunk = queue[i];
offset = (i === 0) ? this._cursor : 0; size = Math.min(remain, chunk.length - this._cursor);
size = Math.min(remain, chunk.length - offset);
chunk.copy(buffer, length - remain, offset, offset + size); chunk.copy(buffer, length - remain, this._cursor, this._cursor + size);
remain -= size;
remain -= size;
this._queueSize -= size;
this._cursor = (this._cursor + size) % chunk.length;
i += 1; i += 1;
} }
if (remain > 0) return null; queue.splice(0, this._cursor === 0 ? i : i - 1);
queue.splice(0, i-1);
this._cursor = (i === 1 ? this._cursor : 0) + size;
return buffer; return buffer;
}; };
+8 -13
View File
@@ -3,24 +3,19 @@
, "homepage" : "http://github.com/faye/websocket-driver-node" , "homepage" : "http://github.com/faye/websocket-driver-node"
, "author" : "James Coglan <jcoglan@gmail.com> (http://jcoglan.com/)" , "author" : "James Coglan <jcoglan@gmail.com> (http://jcoglan.com/)"
, "keywords" : ["websocket"] , "keywords" : ["websocket"]
, "license" : "MIT"
, "version" : "0.2.1" , "version" : "0.3.4"
, "engines" : {"node": ">=0.4.0"} , "engines" : {"node": ">=0.4.0"}
, "main" : "./lib/websocket/driver" , "main" : "./lib/websocket/driver"
, "devDependencies" : {"jsclass": ""} , "devDependencies" : {"jstest": ""}
, "scripts" : {"test": "node spec/runner.js"} , "scripts" : {"test": "jstest spec/runner.js"}
, "repository" : { "type" : "git"
, "url" : "git://github.com/faye/websocket-driver-node.git"
}
, "bugs" : "http://github.com/faye/websocket-driver-node/issues" , "bugs" : "http://github.com/faye/websocket-driver-node/issues"
, "licenses" : [ { "type" : "MIT"
, "url" : "http://www.opensource.org/licenses/mit-license.php"
}
]
, "repositories" : [ { "type" : "git"
, "url" : "git://github.com/faye/websocket-driver-node.git"
}
]
} }
+15 -19
View File
@@ -1,6 +1,5 @@
require('jsclass') var test = require('jstest').Test,
Stream = require('stream').Stream,
var Stream = require('stream').Stream,
util = require('util') util = require('util')
var BufferMatcher = function(data) { var BufferMatcher = function(data) {
@@ -29,21 +28,18 @@ Collector.prototype.write = function(buffer) {
return true return true
} }
JS.require('JS.Test', function() { test.Unit.TestCase.include({
JS.Test.Unit.TestCase.include({ buffer: function(data) {
buffer: function(data) { return new BufferMatcher(data)
return new BufferMatcher(data) },
}, collector: function() {
collector: function() { return this._collector = this._collector || new Collector()
return this._collector = this._collector || new Collector() }
}
})
require('./websocket/driver/draft75_examples')
require('./websocket/driver/draft75_spec')
require('./websocket/driver/draft76_spec')
require('./websocket/driver/hybi_spec')
require('./websocket/driver/client_spec')
JS.Test.autorun()
}) })
require('./websocket/driver/draft75_examples')
require('./websocket/driver/draft75_spec')
require('./websocket/driver/draft76_spec')
require('./websocket/driver/hybi_spec')
require('./websocket/driver/client_spec')
+56 -25
View File
@@ -1,6 +1,7 @@
var Client = require("../../../lib/websocket/driver/client") var Client = require("../../../lib/websocket/driver/client"),
test = require('jstest').Test
JS.Test.describe("Client", function() { with(this) { test.describe("Client", function() { with(this) {
define("options", function() { define("options", function() {
return this._options = this._options || {protocols: this.protocols()} return this._options = this._options || {protocols: this.protocols()}
}) })
@@ -9,9 +10,13 @@ JS.Test.describe("Client", function() { with(this) {
null null
}) })
define("url", function() {
return "ws://www.example.com/socket"
})
define("driver", function() { define("driver", function() {
if (this._driver) return this._driver if (this._driver) return this._driver
this._driver = new Client("ws://www.example.com/socket", this.options()) this._driver = new Client(this.url(), this.options())
var self = this var self = this
this._driver.on('open', function(e) { self.open = true }) this._driver.on('open', function(e) { self.open = true })
this._driver.on('message', function(e) { self.message += e.data }) this._driver.on('message', function(e) { self.message += e.data })
@@ -79,6 +84,23 @@ JS.Test.describe("Client", function() { with(this) {
}}) }})
}}) }})
describe("with basic auth", function() { with(this) {
define("url", function() { return "ws://user:pass@www.example.com/socket" })
it("writes the handshake with Authorization", function() { with(this) {
expect(driver().io, "emit").given("data", buffer(
"GET /socket HTTP/1.1\r\n" +
"Host: www.example.com\r\n" +
"Upgrade: websocket\r\n" +
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Key: 2vBVWg4Qyk3ZoM/5d3QD9Q==\r\n" +
"Sec-WebSocket-Version: 13\r\n" +
"Authorization: Basic dXNlcjpwYXNz\r\n" +
"\r\n"))
driver().start()
}})
}})
describe("with custom headers", function() { with(this) { describe("with custom headers", function() { with(this) {
before(function() { with(this) { before(function() { with(this) {
driver().setHeader("User-Agent", "Chrome") driver().setHeader("User-Agent", "Chrome")
@@ -109,7 +131,10 @@ JS.Test.describe("Client", function() { with(this) {
before(function() { this.driver().start() }) before(function() { this.driver().start() })
describe("with a valid response", function() { with(this) { describe("with a valid response", function() { with(this) {
before(function() { this.driver().parse(new Buffer(this.response())) }) before(function(resume) { with(this) {
driver().parse(new Buffer(response()))
setTimeout(resume, 10)
}})
it("changes the state to open", function() { with(this) { it("changes the state to open", function() { with(this) {
assertEqual( true, open ) assertEqual( true, open )
@@ -127,11 +152,12 @@ JS.Test.describe("Client", function() { with(this) {
}}) }})
describe("with a valid response followed by a frame", function() { with(this) { describe("with a valid response followed by a frame", function() { with(this) {
before(function() { with(this) { before(function(resume) { with(this) {
var resp = new Buffer(response().length + 4) var resp = new Buffer(response().length + 4)
new Buffer(response()).copy(resp) new Buffer(response()).copy(resp)
new Buffer([0x81, 0x02, 72, 105]).copy(resp, resp.length - 4) new Buffer([0x81, 0x02, 72, 105]).copy(resp, resp.length - 4)
driver().parse(resp) driver().parse(resp)
setTimeout(resume, 10)
}}) }})
it("changes the state to open", function() { with(this) { it("changes the state to open", function() { with(this) {
@@ -146,10 +172,11 @@ JS.Test.describe("Client", function() { with(this) {
}}) }})
describe("with a bad status line", function() { with(this) { describe("with a bad status line", function() { with(this) {
before(function() { before(function(resume) { with(this) {
var resp = this.response().replace(/101/g, "4") var resp = response().replace(/101/g, "4")
this.driver().parse(new Buffer(resp)) driver().parse(new Buffer(resp))
}) setTimeout(resume, 10)
}})
it("changes the state to closed", function() { with(this) { it("changes the state to closed", function() { with(this) {
assertEqual( false, open ) assertEqual( false, open )
@@ -160,10 +187,11 @@ JS.Test.describe("Client", function() { with(this) {
}}) }})
describe("with a bad Upgrade header", function() { with(this) { describe("with a bad Upgrade header", function() { with(this) {
before(function() { before(function(resume) { with(this) {
var resp = this.response().replace(/websocket/g, "wrong") var resp = response().replace(/websocket/g, "wrong")
this.driver().parse(new Buffer(resp)) driver().parse(new Buffer(resp))
}) setTimeout(resume, 10)
}})
it("changes the state to closed", function() { with(this) { it("changes the state to closed", function() { with(this) {
assertEqual( false, open ) assertEqual( false, open )
@@ -174,10 +202,11 @@ JS.Test.describe("Client", function() { with(this) {
}}) }})
describe("with a bad Accept header", function() { with(this) { describe("with a bad Accept header", function() { with(this) {
before(function() { before(function(resume) { with(this) {
var resp = this.response().replace(/QV3/g, "wrong") var resp = response().replace(/QV3/g, "wrong")
this.driver().parse(new Buffer(resp)) driver().parse(new Buffer(resp))
}) setTimeout(resume, 10)
}})
it("changes the state to closed", function() { with(this) { it("changes the state to closed", function() { with(this) {
assertEqual( false, open ) assertEqual( false, open )
@@ -190,10 +219,11 @@ JS.Test.describe("Client", function() { with(this) {
describe("with valid subprotocols", function() { with(this) { describe("with valid subprotocols", function() { with(this) {
define("protocols", function() { return ["foo", "xmpp"] }) define("protocols", function() { return ["foo", "xmpp"] })
before(function() { before(function(resume) { with(this) {
var resp = this.response().replace(/\r\n\r\n/, "\r\nSec-WebSocket-Protocol: xmpp\r\n\r\n") var resp = response().replace(/\r\n\r\n/, "\r\nSec-WebSocket-Protocol: xmpp\r\n\r\n")
this.driver().parse(new Buffer(resp)) driver().parse(new Buffer(resp))
}) setTimeout(resume, 10)
}})
it("changs the state to open", function() { with(this) { it("changs the state to open", function() { with(this) {
assertEqual( true, open ) assertEqual( true, open )
@@ -209,10 +239,11 @@ JS.Test.describe("Client", function() { with(this) {
describe("with invalid subprotocols", function() { with(this) { describe("with invalid subprotocols", function() { with(this) {
define("protocols", function() { return ["foo", "xmpp"] }) define("protocols", function() { return ["foo", "xmpp"] })
before(function() { before(function(resume) { with(this) {
var resp = this.response().replace(/\r\n\r\n/, "\r\nSec-WebSocket-Protocol: irc\r\n\r\n") var resp = response().replace(/\r\n\r\n/, "\r\nSec-WebSocket-Protocol: irc\r\n\r\n")
this.driver().parse(new Buffer(resp)) driver().parse(new Buffer(resp))
}) setTimeout(resume, 10)
}})
it("changs the state to closed", function() { with(this) { it("changs the state to closed", function() { with(this) {
assertEqual( false, open ) assertEqual( false, open )
+3 -1
View File
@@ -1,4 +1,6 @@
JS.Test.describe("draft-75", function() { with(this) { var test = require('jstest').Test
test.describe("draft-75", function() { with(this) {
sharedExamplesFor("draft-75 protocol", function() { with(this) { sharedExamplesFor("draft-75 protocol", function() { with(this) {
describe("in the open state", function() { with(this) { describe("in the open state", function() { with(this) {
before(function() { this.driver().start() }) before(function() { this.driver().start() })
+3 -2
View File
@@ -1,6 +1,7 @@
var Draft75 = require("../../../lib/websocket/driver/draft75") var Draft75 = require("../../../lib/websocket/driver/draft75"),
test = require('jstest').Test
JS.Test.describe("Draft75", function() { with(this) { test.describe("Draft75", function() { with(this) {
define("request", function() { define("request", function() {
return this._request = this._request || { return this._request = this._request || {
headers: { headers: {
+6 -5
View File
@@ -1,6 +1,7 @@
var Draft76 = require("../../../lib/websocket/driver/draft76") var Draft76 = require("../../../lib/websocket/driver/draft76"),
test = require('jstest').Test
JS.Test.describe("Draft76", function() { with(this) { test.describe("Draft76", function() { with(this) {
BODY = new Buffer([0x91, 0x25, 0x3e, 0xd3, 0xa9, 0xe7, 0x6a, 0x88]) BODY = new Buffer([0x91, 0x25, 0x3e, 0xd3, 0xa9, 0xe7, 0x6a, 0x88])
define("body", function() { define("body", function() {
@@ -52,7 +53,7 @@ JS.Test.describe("Draft76", function() { with(this) {
describe("start", function() { with(this) { describe("start", function() { with(this) {
it("writes the handshake response to the socket", function() { with(this) { it("writes the handshake response to the socket", function() { with(this) {
expect(driver().io, "emit").given("data", buffer( 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" + "Upgrade: WebSocket\r\n" +
"Connection: Upgrade\r\n" + "Connection: Upgrade\r\n" +
"Sec-WebSocket-Origin: http://www.example.com\r\n" + "Sec-WebSocket-Origin: http://www.example.com\r\n" +
@@ -94,7 +95,7 @@ JS.Test.describe("Draft76", function() { with(this) {
it("queues the frames until the handshake has been sent", function() { with(this) { it("queues the frames until the handshake has been sent", function() { with(this) {
expect(driver().io, "emit").given("data", buffer( 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" + "Upgrade: WebSocket\r\n" +
"Connection: Upgrade\r\n" + "Connection: Upgrade\r\n" +
"Sec-WebSocket-Origin: http://www.example.com\r\n" + "Sec-WebSocket-Origin: http://www.example.com\r\n" +
@@ -115,7 +116,7 @@ JS.Test.describe("Draft76", function() { with(this) {
it("writes the handshake response with no body", function() { with(this) { it("writes the handshake response with no body", function() { with(this) {
expect(driver().io, "emit").given("data", buffer( 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" + "Upgrade: WebSocket\r\n" +
"Connection: Upgrade\r\n" + "Connection: Upgrade\r\n" +
"Sec-WebSocket-Origin: http://www.example.com\r\n" + "Sec-WebSocket-Origin: http://www.example.com\r\n" +
+114 -57
View File
@@ -1,6 +1,7 @@
var Hybi = require("../../../lib/websocket/driver/hybi") var Hybi = require("../../../lib/websocket/driver/hybi"),
test = require('jstest').Test
JS.Test.describe("Hybi", function() { with(this) { test.describe("Hybi", function() { with(this) {
define("request", function() { define("request", function() {
return this._request = this._request || { return this._request = this._request || {
headers: { headers: {
@@ -198,42 +199,54 @@ JS.Test.describe("Hybi", function() { with(this) {
return output return output
}) })
it("parses unmasked text frames", function() { with(this) { it("parses unmasked text frames", function(resume) { with(this) {
driver().parse([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f]) driver().parse([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f])
assertEqual( "Hello", message ) setTimeout(function() {
resume(function() { assertEqual( "Hello", message ) })
}, 10)
}}) }})
it("parses multiple frames from the same packet", function() { with(this) { it("parses multiple frames from the same packet", function(resume) { with(this) {
driver().parse([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f]) driver().parse([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f])
assertEqual( "HelloHello", message ) setTimeout(function() {
resume(function() { assertEqual( "HelloHello", message ) })
}, 10)
}}) }})
it("parses empty text frames", function() { with(this) { it("parses empty text frames", function(resume) { with(this) {
driver().parse([0x81, 0x00]) driver().parse([0x81, 0x00])
assertEqual( "", message ) setTimeout(function() {
resume(function() { assertEqual( "", message ) })
}, 10)
}}) }})
it("parses fragmented text frames", function() { with(this) { it("parses fragmented text frames", function(resume) { with(this) {
driver().parse([0x01, 0x03, 0x48, 0x65, 0x6c]) driver().parse([0x01, 0x03, 0x48, 0x65, 0x6c])
driver().parse([0x80, 0x02, 0x6c, 0x6f]) driver().parse([0x80, 0x02, 0x6c, 0x6f])
assertEqual( "Hello", message ) setTimeout(function() {
resume(function() { assertEqual( "Hello", message ) })
}, 10)
}}) }})
it("parses masked text frames", function() { with(this) { it("parses masked text frames", function(resume) { with(this) {
driver().parse([0x81, 0x85]) driver().parse([0x81, 0x85])
driver().parse(mask()) driver().parse(mask())
driver().parse(maskMessage([0x48, 0x65, 0x6c, 0x6c, 0x6f])) driver().parse(maskMessage([0x48, 0x65, 0x6c, 0x6c, 0x6f]))
assertEqual( "Hello", message ) setTimeout(function() {
resume(function() { assertEqual( "Hello", message ) })
}, 10)
}}) }})
it("parses masked empty text frames", function() { with(this) { it("parses masked empty text frames", function(resume) { with(this) {
driver().parse([0x81, 0x80]) driver().parse([0x81, 0x80])
driver().parse(mask()) driver().parse(mask())
driver().parse(maskMessage([])) driver().parse(maskMessage([]))
assertEqual( "", message ) setTimeout(function() {
resume(function() { assertEqual( "", message ) })
}, 10)
}}) }})
it("parses masked fragmented text frames", function() { with(this) { it("parses masked fragmented text frames", function(resume) { with(this) {
driver().parse([0x01, 0x81]) driver().parse([0x01, 0x81])
driver().parse(mask()) driver().parse(mask())
driver().parse(maskMessage([0x48])) driver().parse(maskMessage([0x48]))
@@ -242,49 +255,67 @@ JS.Test.describe("Hybi", function() { with(this) {
driver().parse(mask()) driver().parse(mask())
driver().parse(maskMessage([0x65, 0x6c, 0x6c, 0x6f])) driver().parse(maskMessage([0x65, 0x6c, 0x6c, 0x6f]))
assertEqual( "Hello", message ) setTimeout(function() {
resume(function() { assertEqual( "Hello", message ) })
}, 10)
}}) }})
it("closes the socket if the frame has an unrecognized opcode", function() { with(this) { it("closes the socket if the frame has an unrecognized opcode", function(resume) { with(this) {
driver().parse([0x83, 0x00]) driver().parse([0x83, 0x00])
assertEqual( [0x88, 0x1e, 0x03, 0xea], collector().bytes.slice(0,4) ) setTimeout(function() {
assertEqual( "Unrecognized frame opcode: 3", error.message ) resume(function() {
assertEqual( [1002, "Unrecognized frame opcode: 3"], close ) assertEqual( [0x88, 0x1e, 0x03, 0xea], collector().bytes.slice(0,4) )
assertEqual( "closed", driver().getState() ) assertEqual( "Unrecognized frame opcode: 3", error.message )
assertEqual( [1002, "Unrecognized frame opcode: 3"], close )
assertEqual( "closed", driver().getState() )
})
}, 10)
}}) }})
it("closes the socket if a close frame is received", function() { with(this) { it("closes the socket if a close frame is received", function(resume) { with(this) {
driver().parse([0x88, 0x07, 0x03, 0xe8, 0x48, 0x65, 0x6c, 0x6c, 0x6f]) driver().parse([0x88, 0x07, 0x03, 0xe8, 0x48, 0x65, 0x6c, 0x6c, 0x6f])
assertEqual( [0x88, 0x07, 0x03, 0xe8, 0x48, 0x65, 0x6c, 0x6c, 0x6f], collector().bytes ) setTimeout(function() {
assertEqual( [1000, "Hello"], close ) resume(function() {
assertEqual( "closed", driver().getState() ) assertEqual( [0x88, 0x07, 0x03, 0xe8, 0x48, 0x65, 0x6c, 0x6c, 0x6f], collector().bytes )
assertEqual( [1000, "Hello"], close )
assertEqual( "closed", driver().getState() )
})
}, 10)
}}) }})
it("parses unmasked multibyte text frames", function() { with(this) { it("parses unmasked multibyte text frames", function(resume) { with(this) {
driver().parse([0x81, 0x0b, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf]) driver().parse([0x81, 0x0b, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf])
assertEqual( "Apple = ", message ) setTimeout(function() {
resume(function() { assertEqual( "Apple = ", message ) })
}, 10)
}}) }})
it("parses frames received in several packets", function() { with(this) { it("parses frames received in several packets", function(resume) { with(this) {
driver().parse([0x81, 0x0b, 0x41, 0x70, 0x70, 0x6c]) driver().parse([0x81, 0x0b, 0x41, 0x70, 0x70, 0x6c])
driver().parse([0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf]) driver().parse([0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf])
assertEqual( "Apple = ", message ) setTimeout(function() {
resume(function() { assertEqual( "Apple = ", message ) })
}, 10)
}}) }})
it("parses fragmented multibyte text frames", function() { with(this) { it("parses fragmented multibyte text frames", function(resume) { with(this) {
driver().parse([0x01, 0x0a, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3]) driver().parse([0x01, 0x0a, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3])
driver().parse([0x80, 0x01, 0xbf]) driver().parse([0x80, 0x01, 0xbf])
assertEqual( "Apple = ", message ) setTimeout(function() {
resume(function() { assertEqual( "Apple = ", message ) })
}, 10)
}}) }})
it("parse masked multibyte text frames", function() { with(this) { it("parse masked multibyte text frames", function(resume) { with(this) {
driver().parse([0x81, 0x8b]) driver().parse([0x81, 0x8b])
driver().parse(mask()) driver().parse(mask())
driver().parse(maskMessage([0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf])) driver().parse(maskMessage([0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf]))
assertEqual( "Apple = ", message ) setTimeout(function() {
resume(function() { assertEqual( "Apple = ", message ) })
}, 10)
}}) }})
it("parses masked fragmented multibyte text frames", function() { with(this) { it("parses masked fragmented multibyte text frames", function(resume) { with(this) {
driver().parse([0x01, 0x8a]) driver().parse([0x01, 0x8a])
driver().parse(mask()) driver().parse(mask())
driver().parse(maskMessage([0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3])) driver().parse(maskMessage([0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3]))
@@ -293,27 +324,35 @@ JS.Test.describe("Hybi", function() { with(this) {
driver().parse(mask()) driver().parse(mask())
driver().parse(maskMessage([0xbf])) driver().parse(maskMessage([0xbf]))
assertEqual( "Apple = ", message ) setTimeout(function() {
resume(function() { assertEqual( "Apple = ", message ) })
}, 10)
}}) }})
it("parses unmasked medium-length text frames", function() { with(this) { it("parses unmasked medium-length text frames", function(resume) { with(this) {
driver().parse([0x81, 0x7e, 0x00, 0xc8]) driver().parse([0x81, 0x7e, 0x00, 0xc8])
var i = 40, result = "" var i = 40, result = ""
while (i--) { while (i--) {
driver().parse([0x48, 0x65, 0x6c, 0x6c, 0x6f]) driver().parse([0x48, 0x65, 0x6c, 0x6c, 0x6f])
result += "Hello" result += "Hello"
} }
assertEqual( result, message ) setTimeout(function() {
resume(function() { assertEqual( result, message ) })
}, 10)
}}) }})
it("returns an error for too-large frames", function() { with(this) { it("returns an error for too-large frames", function(resume) { 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 ) setTimeout(function() {
assertEqual( [1009, "WebSocket frame length too large"], close ) resume(function() {
assertEqual( "closed", driver().getState() ) assertEqual( "WebSocket frame length too large", error.message )
assertEqual( [1009, "WebSocket frame length too large"], close )
assertEqual( "closed", driver().getState() )
})
}, 10)
}}) }})
it("parses masked medium-length text frames", function() { with(this) { it("parses masked medium-length text frames", function(resume) { with(this) {
driver().parse([0x81, 0xfe, 0x00, 0xc8]) driver().parse([0x81, 0xfe, 0x00, 0xc8])
driver().parse(mask()) driver().parse(mask())
var i = 40, result = "", packet = [] var i = 40, result = "", packet = []
@@ -322,12 +361,18 @@ JS.Test.describe("Hybi", function() { with(this) {
result += "Hello" result += "Hello"
} }
driver().parse(maskMessage(packet)) driver().parse(maskMessage(packet))
assertEqual( result, message ) setTimeout(function() {
resume(function() { assertEqual( result, message ) })
}, 10)
}}) }})
it("replies to pings with a pong", function() { with(this) { it("replies to pings with a pong", function(resume) { with(this) {
driver().parse([0x89, 0x04, 0x4f, 0x48, 0x41, 0x49]) driver().parse([0x89, 0x04, 0x4f, 0x48, 0x41, 0x49])
assertEqual( [0x8a, 0x04, 0x4f, 0x48, 0x41, 0x49], collector().bytes ) setTimeout(function() {
resume(function() {
assertEqual( [0x8a, 0x04, 0x4f, 0x48, 0x41, 0x49], collector().bytes )
})
}, 10)
}}) }})
}}) }})
@@ -378,18 +423,22 @@ JS.Test.describe("Hybi", function() { with(this) {
assertEqual( true, driver().ping() ) assertEqual( true, driver().ping() )
}}) }})
it("runs the given callback on mathing pong", function() { with(this) { it("runs the given callback on mathing pong", function(resume) { with(this) {
var reply = null var reply = null
driver().ping("Hi", function() { reply = true }) driver().ping("Hi", function() { reply = true })
driver().parse([0x8a, 0x02, 72, 105]) driver().parse([0x8a, 0x02, 72, 105])
assert( reply ) setTimeout(function() {
resume(function() { assert( reply ) })
}, 10)
}}) }})
it("does not run the callback on non-matching pong", function() { with(this) { it("does not run the callback on non-matching pong", function(resume) { with(this) {
var reply = null var reply = null
driver().ping("Hi", function() { reply = true }) driver().ping("Hi", function() { reply = true })
driver().parse([0x8a, 0x03, 119, 97, 116]) driver().parse([0x8a, 0x03, 119, 97, 116])
assert( !reply ) setTimeout(function() {
resume(function() { assert( !reply ) })
}, 10)
}}) }})
}}) }})
@@ -426,15 +475,21 @@ JS.Test.describe("Hybi", function() { with(this) {
this.driver().start() this.driver().start()
}) })
it("does not emit a message", function() { with(this) { it("does not emit a message", function(resume) { with(this) {
driver().parse([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f]) driver().parse([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f])
assertEqual( "", message ) setTimeout(function() {
resume(function() { assertEqual( "", message ) })
}, 10)
}}) }})
it("returns an error", function() { with(this) { it("returns an error", function(resume) { with(this) {
driver().parse([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f]) driver().parse([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f])
assertEqual( "Received unmasked frame but masking is required", error.message ) setTimeout(function() {
assertEqual( [1003, "Received unmasked frame but masking is required"], close ) resume(function() {
assertEqual( "Received unmasked frame but masking is required", error.message )
assertEqual( [1003, "Received unmasked frame but masking is required"], close )
})
}, 10)
}}) }})
}}) }})
@@ -478,8 +533,9 @@ JS.Test.describe("Hybi", function() { with(this) {
}}) }})
describe("receiving a close frame", function() { with(this) { describe("receiving a close frame", function() { with(this) {
before(function() { before(function(resume) {
this.driver().parse([0x88, 0x04, 0x03, 0xe9, 0x4f, 0x4b]) this.driver().parse([0x88, 0x04, 0x03, 0xe9, 0x4f, 0x4b])
setTimeout(resume, 10)
}) })
it("triggers the onclose event", function() { with(this) { it("triggers the onclose event", function() { with(this) {
@@ -493,10 +549,11 @@ JS.Test.describe("Hybi", function() { with(this) {
}}) }})
describe("in the closed state", function() { with(this) { describe("in the closed state", function() { with(this) {
before(function() { before(function(resume) {
this.driver().start() this.driver().start()
this.driver().close() this.driver().close()
this.driver().parse([0x88, 0x02, 0x03, 0xea]) this.driver().parse([0x88, 0x02, 0x03, 0xea])
setTimeout(resume, 10)
}) })
describe("frame", function() { with(this) { describe("frame", function() { with(this) {