Compare commits

...

9 Commits
0.8.0 ... 0.8.1

Author SHA1 Message Date
Guillermo Rauch
0b61eda84c Release 0.8.1 2011-08-29 09:42:16 -07:00
Guillermo Rauch
23ba929f3f Merge branch 'master' of github.com:LearnBoost/socket.io 2011-08-29 09:37:54 -07:00
einaros
13647075f2 fixed utf8 bug in send framing 2011-08-29 09:43:12 +02:00
Guillermo Rauch
4c9414c4c1 Merge pull request #486 from Znarkus/patch-1
Fixed typo.
2011-08-28 23:57:28 -07:00
Markus Hedlund
ec88f95722 Fixed typo. 2011-08-29 20:23:38 +03:00
einaros
f377cd631e fixed bug in send framing for over 64kB of data 2011-08-29 08:33:30 +02:00
einaros
e41aab84f8 corrected ping handling from websocket transport, and added warning output on parser error 2011-08-29 08:00:03 +02:00
einaros
0a6c78cbb8 joined compatible hybi protocol handlers and updated test reference 2011-08-29 07:56:40 +02:00
einaros
004130cb11 fixed Parser library path and did some code cleanup 2011-08-29 07:30:29 +02:00
9 changed files with 78 additions and 726 deletions

View File

@@ -1,4 +1,12 @@
0.8.1 / 2011-08-29
==================
* Fixed utf8 bug in send framing in websocket [einaros]
* Fixed typo in docs [Znarkus]
* Fixed bug in send framing for over 64kB of data in websocket [einaros]
* Corrected ping handling in websocket transport [einaros]
0.8.0 / 2011-08-28
==================

View File

@@ -60,7 +60,7 @@ Besides `connect`, `message` and `disconnect`, you can emit custom events:
var io = require('socket.io').listen(80);
io.sockets.on('connection', function (socket) {
io.sockets.emit('this', { will: 'be received by everyone');
io.sockets.emit('this', { will: 'be received by everyone' });
socket.on('private message', function (from, msg) {
console.log('I received a private message by ', from, ' saying ', msg);

View File

@@ -15,7 +15,7 @@ var client = require('socket.io-client');
* Version.
*/
exports.version = '0.8.0';
exports.version = '0.8.1';
/**
* Supported protocol version.

View File

@@ -1,461 +0,0 @@
/*!
* socket.io-node
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Module requirements.
*/
var Transport = require('../../transport')
, EventEmitter = process.EventEmitter
, crypto = require('crypto')
, parser = require('../../parser')
, util = require('../../util');
/**
* Export the constructor.
*/
exports = module.exports = WebSocket;
exports.Parser = Parser;
/**
* HTTP interface constructor. Interface compatible with all transports that
* depend on request-response cycles.
*
* @api public
*/
function WebSocket (mng, data, req) {
// parser
var self = this;
this.parser = new Parser();
this.parser.on('data', function (packet) {
self.onMessage(parser.decodePacket(packet));
});
this.parser.on('ping', function () {
// version 8 ping => pong
this.socket.write('\u008a\u0000');
});
this.parser.on('close', function () {
self.end();
});
this.parser.on('error', function () {
self.end();
});
Transport.call(this, mng, data, req);
};
/**
* Inherits from Transport.
*/
WebSocket.prototype.__proto__ = Transport.prototype;
/**
* Transport name
*
* @api public
*/
WebSocket.prototype.name = 'websocket';
/**
* Called when the socket connects.
*
* @api private
*/
WebSocket.prototype.onSocketConnect = function () {
var self = this;
if (this.req.headers.upgrade !== 'websocket') {
this.log.warn(this.name + ' connection invalid');
this.end();
return;
}
var origin = this.req.headers.origin
, location = (this.socket.encrypted ? 'wss' : 'ws')
+ '://' + this.req.headers.host + this.req.url;
if (!this.req.headers['sec-websocket-key']) {
this.log.warn(this.name + ' connection invalid: received no key');
this.end();
return;
}
// calc key
var key = this.req.headers['sec-websocket-key'];
var shasum = crypto.createHash('sha1');
shasum.update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
key = shasum.digest('base64');
var headers = [
'HTTP/1.1 101 Switching Protocols'
, 'Upgrade: websocket'
, 'Connection: Upgrade'
, 'Sec-WebSocket-Accept: ' + key
];
try {
this.socket.write(headers.concat('', '').join('\r\n'));
this.socket.setTimeout(0);
this.socket.setNoDelay(true);
} catch (e) {
this.end();
return;
}
this.socket.on('data', function (data) {
self.parser.add(data);
});
};
/**
* Writes to the socket.
*
* @api private
*/
WebSocket.prototype.write = function (data) {
if (this.open) {
var buf = this.frame(0x81, data);
this.socket.write(buf, 'binary');
this.log.debug(this.name + ' writing', data);
}
};
/**
* Frame server-to-client output as a text packet.
*
* @api private
*/
WebSocket.prototype.frame = function (opcode, data) {
var startOffset = 2, secondByte = data.length, buf;
if (data.length > 125) {
// opcode = 0x81;
startOffset += 2;
secondByte = 126;
}
if (data.length > 65536) {
startOffset += 6;
secondByte = 127;
}
buf = new Buffer(data.length + startOffset, 'utf8');
buf[0] = opcode;
buf[1] = secondByte;
switch (secondByte) {
case 126:
buf[2] = data.length >>> 8;
buf[3] = data.length % 256;
break;
case 127:
for (var i = 6; i > 2; i--) {
buf[i] = secondByte % 256;
secondByte >>>= 8;
}
}
buf.write(data, startOffset, 'utf8');
return buf;
};
/**
* Closes the connection.
*
* @api private
*/
WebSocket.prototype.doClose = function () {
this.socket.end();
};
/**
* WebSocket parser
*
* @api public
*/
function Parser () {
this.state = {
activeFragmentedOperation: null,
lastFragment: false,
masked: false,
opcode: 0
};
this.overflow = null;
this.expectOffset = 0;
this.expectBuffer = null;
this.expectHandler = null;
this.currentMessage = '';
var self = this;
this.opcodeHandlers = {
// text
'1': function(data) {
var finish = function(mask, data) {
self.currentMessage += self.unmask(mask, data);
if (self.state.lastFragment) {
self.emit('data', self.currentMessage);
self.currentMessage = '';
}
self.endPacket();
}
var expectData = function(length) {
if (self.state.masked) {
self.expect('Mask', 4, function(data) {
var mask = data;
self.expect('Data', length, function(data) {
finish(mask, data);
});
});
}
else {
self.expect('Data', length, function(data) {
finish(null, data);
});
}
}
// decode length
var firstLength = data[1] & 0x7f;
if (firstLength < 126) {
expectData(firstLength);
}
else if (firstLength == 126) {
self.expect('Length', 2, function(data) {
expectData(util.unpack(data));
});
}
else if (firstLength == 127) {
self.expect('Length', 8, function(data) {
if (util.unpack(data.slice(0, 4)) != 0) {
self.error('packets with length spanning more than 32 bit is currently not supported');
return;
}
var lengthBytes = data.slice(4); // note: cap to 32 bit length
expectData(util.unpack(data));
});
}
},
// close
'8': function(data) {
self.emit('close');
self.reset();
},
// ping
'9': function(data) {
if (self.state.lastFragment == false) {
self.error('fragmented ping is not supported');
return;
}
var finish = function(mask, data) {
self.emit('ping', self.unmask(mask, data));
self.endPacket();
}
var expectData = function(length) {
if (self.state.masked) {
self.expect('Mask', 4, function(data) {
var mask = data;
self.expect('Data', length, function(data) {
finish(mask, data);
});
});
}
else {
self.expect('Data', length, function(data) {
finish(null, data);
});
}
}
// decode length
var firstLength = data[1] & 0x7f;
if (firstLength == 0) {
finish(null, null);
}
else if (firstLength < 126) {
expectData(firstLength);
}
else if (firstLength == 126) {
self.expect('Length', 2, function(data) {
expectData(util.unpack(data));
});
}
else if (firstLength == 127) {
self.expect('Length', 8, function(data) {
expectData(util.unpack(data));
});
}
}
}
this.expect('Opcode', 2, this.processPacket);
};
/**
* Inherits from EventEmitter.
*/
Parser.prototype.__proto__ = EventEmitter.prototype;
/**
* Add new data to the parser.
*
* @api public
*/
Parser.prototype.add = function(data) {
if (this.expectBuffer == null) {
this.addToOverflow(data);
return;
}
var toRead = Math.min(data.length, this.expectBuffer.length - this.expectOffset);
data.copy(this.expectBuffer, this.expectOffset, 0, toRead);
this.expectOffset += toRead;
if (toRead < data.length) {
// at this point the overflow buffer shouldn't at all exist
this.overflow = new Buffer(data.length - toRead);
data.copy(this.overflow, 0, toRead, toRead + this.overflow.length);
}
if (this.expectOffset == this.expectBuffer.length) {
var bufferForHandler = this.expectBuffer;
this.expectBuffer = null;
this.expectOffset = 0;
this.expectHandler.call(this, bufferForHandler);
}
}
/**
* Adds a piece of data to the overflow.
*
* @api private
*/
Parser.prototype.addToOverflow = function(data) {
if (this.overflow == null) this.overflow = data;
else {
var prevOverflow = this.overflow;
this.overflow = new Buffer(this.overflow.length + data.length);
prevOverflow.copy(this.overflow, 0);
data.copy(this.overflow, prevOverflow.length);
}
}
/**
* Waits for a certain amount of bytes to be available, then fires a callback.
*
* @api private
*/
Parser.prototype.expect = function(what, length, handler) {
this.expectBuffer = new Buffer(length);
this.expectOffset = 0;
this.expectHandler = handler;
if (this.overflow != null) {
var toOverflow = this.overflow;
this.overflow = null;
this.add(toOverflow);
}
}
/**
* Start processing a new packet.
*
* @api private
*/
Parser.prototype.processPacket = function (data) {
if ((data[0] & 0x70) != 0) this.error('reserved fields not empty');
this.state.lastFragment = (data[0] & 0x80) == 0x80;
this.state.masked = (data[1] & 0x80) == 0x80;
var opcode = data[0] & 0xf;
if (opcode == 0) {
// continuation frame
if (this.state.opcode != 1 || this.state.opcode != 2) {
this.error('continuation frame cannot follow current opcode')
return;
}
}
else this.state.opcode = opcode;
this.state.opcode = data[0] & 0xf;
var handler = this.opcodeHandlers[this.state.opcode];
if (typeof handler == 'undefined') this.error('no handler for opcode ' + this.state.opcode);
else handler(data);
}
/**
* Endprocessing a packet.
*
* @api private
*/
Parser.prototype.endPacket = function() {
this.expectOffset = 0;
this.expectBuffer = null;
this.expectHandler = null;
if (this.state.lastFragment && this.state.opcode == this.state.activeFragmentedOperation) {
// end current fragmented operation
this.state.activeFragmentedOperation = null;
}
this.state.lastFragment = false;
this.state.opcode = this.state.activeFragmentedOperation != null ? this.state.activeFragmentedOperation : 0;
this.state.masked = false;
this.expect('Opcode', 2, this.processPacket);
}
/**
* Reset the parser state.
*
* @api private
*/
Parser.prototype.reset = function() {
this.state = {
activeFragmentedOperation: null,
lastFragment: false,
masked: false,
opcode: 0
};
this.expectOffset = 0;
this.expectBuffer = null;
this.expectHandler = null;
this.overflow = null;
this.currentMessage = '';
}
/**
* Unmask received data.
*
* @api private
*/
Parser.prototype.unmask = function (mask, buf) {
if (mask != null) {
for (var i = 0, ll = buf.length; i < ll; i++) {
buf[i] ^= mask[i % 4];
}
}
return buf != null ? buf.toString('utf8') : '';
}
/**
* Handles an error
*
* @api private
*/
Parser.prototype.error = function (reason) {
this.reset();
this.emit('error', reason);
return this;
};

View File

@@ -39,12 +39,13 @@ function WebSocket (mng, data, req) {
});
this.parser.on('ping', function () {
// version 8 ping => pong
this.socket.write('\u008a\u0000');
self.socket.write('\u008a\u0000');
});
this.parser.on('close', function () {
self.end();
});
this.parser.on('error', function () {
this.parser.on('error', function (reason) {
self.log.warn(self.name + ' parser error: ' + reason);
self.end();
});
@@ -137,33 +138,36 @@ WebSocket.prototype.write = function (data) {
* @api private
*/
WebSocket.prototype.frame = function (opcode, data) {
var startOffset = 2, secondByte = data.length, buf;
if (data.length > 125) {
// opcode = 0x81;
startOffset += 2;
secondByte = 126;
}
if (data.length > 65536) {
startOffset += 6;
WebSocket.prototype.frame = function (opcode, str) {
var dataBuffer = new Buffer(str);
var dataLength = dataBuffer.length;
var startOffset = 2
, secondByte = dataLength;
if (dataLength > 65536) {
startOffset = 10;
secondByte = 127;
}
buf = new Buffer(data.length + startOffset, 'utf8');
buf[0] = opcode;
buf[1] = secondByte;
else if (dataLength > 125) {
startOffset = 4;
secondByte = 126;
}
var outputBuffer = new Buffer(dataLength + startOffset);
outputBuffer[0] = opcode;
outputBuffer[1] = secondByte;
dataBuffer.copy(outputBuffer, startOffset);
switch (secondByte) {
case 126:
buf[2] = data.length >>> 8;
buf[3] = data.length % 256;
outputBuffer[2] = dataLength >>> 8;
outputBuffer[3] = dataLength % 256;
break;
case 127:
for (var i = 6; i > 2; i--) {
buf[i] = secondByte % 256;
secondByte >>>= 8;
var l = dataLength;
for (var i = 1; i <= 8; ++i) {
outputBuffer[startOffset - i] = l & 0xff;
l >>>= 8;
}
}
buf.write(data, startOffset, 'utf8');
return buf;
return outputBuffer;
};
/**

View File

@@ -4,7 +4,7 @@
*/
module.exports = {
7: require('./7'),
8: require('./8'),
7: require('./hybi-07-12'),
8: require('./hybi-07-12'),
default: require('./default')
};

View File

@@ -1,6 +1,6 @@
{
"name": "socket.io"
, "version": "0.8.0"
, "version": "0.8.1"
, "description": "Real-time apps made cross-browser & easy with a WebSocket-like API"
, "homepage": "http://socket.io"
, "keywords": ["websocket", "socket", "realtime", "socket.io", "comet", "ajax"]
@@ -16,7 +16,7 @@
, "url": "https://github.com/LearnBoost/Socket.IO-node.git"
}
, "dependencies": {
"socket.io-client": "0.8.0"
"socket.io-client": "0.8.1"
, "policyfile": "0.0.4"
, "redis": "0.6.6"
}

View File

@@ -1,5 +1,13 @@
/**
* Test dependencies.
*/
var assert = require('assert');
var Parser = require('../lib/transports/wsver/7.js').Parser;
var Parser = require('../lib/transports/websocket/hybi-07-12.js').Parser;
/**
* Returns a Buffer from a "ff 00 ff"-type hex string.
*/
function makeBufferFromHexString(byteStr) {
var bytes = byteStr.split(' ');
@@ -10,14 +18,22 @@ function makeBufferFromHexString(byteStr) {
return buf;
}
/**
* Splits a buffer in two parts.
*/
function splitBuffer(buffer) {
var b1 = new Buffer(Math.ceil(buffer.length / 2));
var b2 = new Buffer(Math.floor(buffer.length / 2));
buffer.copy(b1, 0, 0, b1.length);
var b2 = new Buffer(Math.floor(buffer.length / 2));
buffer.copy(b2, 0, b1.length, b1.length + b2.length);
return [b1, b2];
}
/**
* Performs hybi07+ type masking on a hex string.
*/
function mask(str, maskString) {
var buf = new Buffer(str);
var mask = makeBufferFromHexString(maskString || '34 83 a8 68');
@@ -27,6 +43,10 @@ function mask(str, maskString) {
return buf;
}
/**
* Unpacks a Buffer into a number.
*/
function unpack(buffer) {
var n = 0;
for (var i = 0; i < buffer.length; ++i) {
@@ -35,14 +55,26 @@ function unpack(buffer) {
return n;
}
/**
* Returns a hex string, representing a specific byte count 'length', from a number.
*/
function pack(length, number) {
return padl(number.toString(16), length, '0').replace(/(\d\d)/g, '$1 ').trim();
}
function padl(s,n,c) {
/**
* Left pads the string 's' to a total length of 'n' with char 'c'.
*/
function padl(s, n, c) {
return new Array(1 + n - s.length).join(c) + s;
}
/**
* Returns a hex string from a Buffer.
*/
function dump(data) {
var s = '';
for (var i = 0; i < data.length; ++i) {
@@ -51,6 +83,10 @@ function dump(data) {
return s.trim();
}
/**
* Tests.
*/
module.exports = {
'can parse unmasked text message': function() {
var p = new Parser();

View File

@@ -1,235 +0,0 @@
var assert = require('assert');
var Parser = require('../lib/transports/wsver/8.js').Parser;
function makeBufferFromHexString(byteStr) {
var bytes = byteStr.split(' ');
var buf = new Buffer(bytes.length);
for (var i = 0; i < bytes.length; ++i) {
buf[i] = parseInt(bytes[i], 16);
}
return buf;
}
function splitBuffer(buffer) {
var b1 = new Buffer(Math.ceil(buffer.length / 2));
var b2 = new Buffer(Math.floor(buffer.length / 2));
buffer.copy(b1, 0, 0, b1.length);
buffer.copy(b2, 0, b1.length, b1.length + b2.length);
return [b1, b2];
}
function mask(str, maskString) {
var buf = new Buffer(str);
var mask = makeBufferFromHexString(maskString || '34 83 a8 68');
for (var i = 0; i < buf.length; ++i) {
buf[i] ^= mask[i % 4];
}
return buf;
}
function unpack(buffer) {
var n = 0;
for (var i = 0; i < buffer.length; ++i) {
n = (i == 0) ? buffer[i] : (n * 256) + buffer[i];
}
return n;
}
function pack(length, number) {
return padl(number.toString(16), length, '0').replace(/(\d\d)/g, '$1 ').trim();
}
function padl(s,n,c) {
return new Array(1 + n - s.length).join(c) + s;
}
function dump(data) {
var s = '';
for (var i = 0; i < data.length; ++i) {
s += padl(data[i].toString(16), 2, '0') + ' ';
}
return s.trim();
}
module.exports = {
'can parse unmasked text message': function() {
var p = new Parser();
var packet = '81 05 48 65 6c 6c 6f';
var gotData = false;
p.on('data', function(data) {
gotData = true;
assert.equal('Hello', data);
});
p.add(makeBufferFromHexString(packet));
assert.ok(gotData);
},
'can parse close message': function() {
var p = new Parser();
var packet = '88 00';
var gotClose = false;
p.on('close', function(data) {
gotClose = true;
});
p.add(makeBufferFromHexString(packet));
assert.ok(gotClose);
},
'can parse masked text message': function() {
var p = new Parser();
var packet = '81 93 34 83 a8 68 01 b9 92 52 4f a1 c6 09 59 e6 8a 52 16 e6 cb 00 5b a1 d5';
var gotData = false;
p.on('data', function(data) {
gotData = true;
assert.equal('5:::{"name":"echo"}', data);
});
p.add(makeBufferFromHexString(packet));
assert.ok(gotData);
},
'can parse a masked text message longer than 125 bytes': function() {
var p = new Parser();
var message = 'A';
for (var i = 0; i < 300; ++i) message += (i % 5).toString();
var packet = '81 FE ' + pack(4, message.length) + ' 34 83 a8 68 ' + dump(mask(message, '34 83 a8 68'));
var gotData = false;
p.on('data', function(data) {
gotData = true;
assert.equal(message, data);
});
p.add(makeBufferFromHexString(packet));
assert.ok(gotData);
},
'can parse a really long masked text message': function() {
var p = new Parser();
var message = 'A';
for (var i = 0; i < 64*1024; ++i) message += (i % 5).toString();
var packet = '81 FF ' + pack(16, message.length) + ' 34 83 a8 68 ' + dump(mask(message, '34 83 a8 68'));
var gotData = false;
p.on('data', function(data) {
gotData = true;
assert.equal(message, data);
});
p.add(makeBufferFromHexString(packet));
assert.ok(gotData);
},
'can parse a fragmented masked text message of 300 bytes': function() {
var p = new Parser();
var message = 'A';
for (var i = 0; i < 300; ++i) message += (i % 5).toString();
var msgpiece1 = message.substr(0, 150);
var msgpiece2 = message.substr(150);
var packet1 = '01 FE ' + pack(4, msgpiece1.length) + ' 34 83 a8 68 ' + dump(mask(msgpiece1, '34 83 a8 68'));
var packet2 = '81 FE ' + pack(4, msgpiece2.length) + ' 34 83 a8 68 ' + dump(mask(msgpiece2, '34 83 a8 68'));
var gotData = false;
p.on('data', function(data) {
gotData = true;
assert.equal(message, data);
});
p.add(makeBufferFromHexString(packet1));
p.add(makeBufferFromHexString(packet2));
assert.ok(gotData);
},
'can parse a ping message': function() {
var p = new Parser();
var message = 'Hello';
var packet = '89 FE ' + pack(4, message.length) + ' 34 83 a8 68 ' + dump(mask(message, '34 83 a8 68'));
var gotPing = false;
p.on('ping', function(data) {
gotPing = true;
assert.equal(message, data);
});
p.add(makeBufferFromHexString(packet));
assert.ok(gotPing);
},
'can parse a ping with no data': function() {
var p = new Parser();
var packet = '89 00';
var gotPing = false;
p.on('ping', function(data) {
gotPing = true;
});
p.add(makeBufferFromHexString(packet));
assert.ok(gotPing);
},
'can parse a fragmented masked text message of 300 bytes with a ping in the middle': function() {
var p = new Parser();
var message = 'A';
for (var i = 0; i < 300; ++i) message += (i % 5).toString();
var msgpiece1 = message.substr(0, 150);
var packet1 = '01 FE ' + pack(4, msgpiece1.length) + ' 34 83 a8 68 ' + dump(mask(msgpiece1, '34 83 a8 68'));
var pingMessage = 'Hello';
var pingPacket = '89 FE ' + pack(4, pingMessage.length) + ' 34 83 a8 68 ' + dump(mask(pingMessage, '34 83 a8 68'));
var msgpiece2 = message.substr(150);
var packet2 = '81 FE ' + pack(4, msgpiece2.length) + ' 34 83 a8 68 ' + dump(mask(msgpiece2, '34 83 a8 68'));
var gotData = false;
p.on('data', function(data) {
gotData = true;
assert.equal(message, data);
});
var gotPing = false;
p.on('ping', function(data) {
gotPing = true;
assert.equal(pingMessage, data);
});
p.add(makeBufferFromHexString(packet1));
p.add(makeBufferFromHexString(pingPacket));
p.add(makeBufferFromHexString(packet2));
assert.ok(gotData);
assert.ok(gotPing);
},
'can parse a fragmented masked text message of 300 bytes with a ping in the middle, which is delievered over sevaral tcp packets': function() {
var p = new Parser();
var message = 'A';
for (var i = 0; i < 300; ++i) message += (i % 5).toString();
var msgpiece1 = message.substr(0, 150);
var packet1 = '01 FE ' + pack(4, msgpiece1.length) + ' 34 83 a8 68 ' + dump(mask(msgpiece1, '34 83 a8 68'));
var pingMessage = 'Hello';
var pingPacket = '89 FE ' + pack(4, pingMessage.length) + ' 34 83 a8 68 ' + dump(mask(pingMessage, '34 83 a8 68'));
var msgpiece2 = message.substr(150);
var packet2 = '81 FE ' + pack(4, msgpiece2.length) + ' 34 83 a8 68 ' + dump(mask(msgpiece2, '34 83 a8 68'));
var gotData = false;
p.on('data', function(data) {
gotData = true;
assert.equal(message, data);
});
var gotPing = false;
p.on('ping', function(data) {
gotPing = true;
assert.equal(pingMessage, data);
});
var buffers = [];
buffers = buffers.concat(splitBuffer(makeBufferFromHexString(packet1)));
buffers = buffers.concat(splitBuffer(makeBufferFromHexString(pingPacket)));
buffers = buffers.concat(splitBuffer(makeBufferFromHexString(packet2)));
for (var i = 0; i < buffers.length; ++i) {
p.add(buffers[i]);
}
assert.ok(gotData);
assert.ok(gotPing);
},
};