From ca751d7a96056abd1d541f9fbdcb6ef3eabea633 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Thu, 11 Nov 2010 11:40:03 -0300 Subject: [PATCH 01/36] New protocol / data encoding specification Failing tests for new data decoding .gitignore --- .gitignore | 3 ++- README.md | 56 ++++++++++++++++++++++++++++++++++++++------------ tests/utils.js | 31 ++++++++++++++++++++-------- 3 files changed, 68 insertions(+), 22 deletions(-) diff --git a/.gitignore b/.gitignore index 1c8f4f45ab..963b424b8e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ lib-cov *.csv *.dat *.out -*.pid \ No newline at end of file +*.pid.swp +*.swp diff --git a/README.md b/README.md index 6f98dc0d50..ecdbeb8804 100644 --- a/README.md +++ b/README.md @@ -179,21 +179,51 @@ Methods: ## Protocol -One of the design goals is that you should be able to implement whatever protocol you desire without `Socket.IO` getting in the way. `Socket.IO` has a minimal, unobtrusive protocol layer, consisting of two parts: +In order to make polling transports simulate the behavior of a full-duplex WebSocket, a session protocol and a message framing mechanism are required. -* Connection handshake - - This is required to simulate a full duplex socket with transports such as XHR Polling or Server-sent Events (which is a "one-way socket"). The basic idea is that the first message received from the server will be a JSON object that contains a session ID used for further communications exchanged between the client and server. - - The concept of session also naturally benefits a full-duplex WebSocket, in the event of an accidental disconnection and a quick reconnection. Messages that the server intends to deliver to the client are cached temporarily until reconnection. - - The implementation of reconnection logic (potentially with retries) is left for the user. By default, transports that are keep-alive or open all the time (like WebSocket) have a timeout of 0 if a disconnection is detected. - -* Message batching +The session protocol consists of the generation of a session id that is passed to the client when the communication starts. Subsequent connections to the server within that session send that session id in the URI along with the transport type. + +### Message encoding + + (message type)":"(content length)":"(data)"," + +(message type) is a single digit that represents one of the known message types (described below). - Messages are buffered in order to optimize resources. In the event of the server trying to send multiple messages while a client is temporarily disconnected (eg: xhr polling), the messages are stacked and then encoded in a lightweight way, and sent to the client whenever it becomes available. +(content length) is the number of characters of (data) -Despite this extra layer, the messages are delivered unaltered to the various event listeners. You can `JSON.stringify()` objects, send XML, or even plain text. +(data) is the message + + 0 = force disconnection + No data or annotations are sent with this message (it's thus always sent as "0:0:,") + + 1 = message + Data format: + (annotations)":"(message) + + Annotations are meta-information associated with a message to make the Socket.IO protocol extensible. They're conceptually similar to HTTP headers. They take this format: + + [key[:value][\n key[:value][\n ...]]] + + For example, when you `.send('Hello world')` within the realm `'chat'`, Socket.IO really is sending: + + 1:18:r:chat:Hello world, + + Two annotations are used by the Socket.IO client: `r` (for `realm`) and `j` (for automatic `json` encoding / decoding of the message). + + 2 = heartbeat + Data format: + (heartbeat numeric index) + + Example: + 2:1:0, + 2:1:1, + + 3 = session id handshake + Data format: + (session id) + + Example: + 3:3:253, ## Credits @@ -224,4 +254,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 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. \ No newline at end of file +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/tests/utils.js b/tests/utils.js index 6b0072389a..435be85ea7 100644 --- a/tests/utils.js +++ b/tests/utils.js @@ -1,12 +1,27 @@ -var encode = require('socket.io/utils').encode, - decode = require('socket.io/utils').decode; +var encode = require('socket.io/utils').encode + , decode = require('socket.io/utils').decode + , messageEncode = require('socket.io/utils').messageEncode + , messageDecode = require('socket.io/utils').messageDecode; module.exports = { - 'test decoding': function(assert){ - var decoded = decode('~m~5~m~abcde' + '~m~9~m~123456789'); - assert.equal(decoded.length, 2); - assert.equal(decoded[0], 'abcde'); - assert.equal(decoded[1], '123456789'); + + 'test data decoding': function(assert){ + var disconnection = decode('0:0:,') + , message = decode('1:18:r:chat:Hello world,') + , incomplete = decode('5:100') + , incomplete2 = decode('6:3:') + , incomplete3 = decode('7:10:abcdefghi') + , unparseable = decode(':') + , unparseable2 = decode('1::'); + + assert.ok(Array.isArray(disconnection)); + assert.ok(disconnection[0] === '0'); + assert.ok(disconnection[1] === ''); + assert.ok(Array.isArray(message); + assert.ok(message[0] === '1'); + assert.ok(message[1] === 'Hello world'); + assert.ok(-1 === incomplete === incomplete2 === incomplete3); + assert.ok(false === unparseable === unparseable2); }, 'test decoding of bad framed messages': function(assert){ @@ -22,4 +37,4 @@ module.exports = { assert.equal(encode(''), '~m~0~m~'); assert.equal(encode(null), '~m~0~m~'); } -}; \ No newline at end of file +}; From 3a79b027299626328ff5c2c6a5bc7ab3bc832263 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Thu, 11 Nov 2010 11:42:15 -0300 Subject: [PATCH 02/36] Fixed syntax error --- tests/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils.js b/tests/utils.js index 435be85ea7..bd4ae53f60 100644 --- a/tests/utils.js +++ b/tests/utils.js @@ -17,7 +17,7 @@ module.exports = { assert.ok(Array.isArray(disconnection)); assert.ok(disconnection[0] === '0'); assert.ok(disconnection[1] === ''); - assert.ok(Array.isArray(message); + assert.ok(Array.isArray(message)); assert.ok(message[0] === '1'); assert.ok(message[1] === 'Hello world'); assert.ok(-1 === incomplete === incomplete2 === incomplete3); From 606bc70c506c2dbe7a55be840c25427f95d7e706 Mon Sep 17 00:00:00 2001 From: Brian McKelvey Date: Thu, 11 Nov 2010 20:15:48 -0800 Subject: [PATCH 03/36] Applying patch to allow for Draft-76 websocket connections through an HAProxy layer. --- lib/socket.io/transports/websocket.js | 59 +++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/lib/socket.io/transports/websocket.js b/lib/socket.io/transports/websocket.js index d3798444c2..818ca90de6 100644 --- a/lib/socket.io/transports/websocket.js +++ b/lib/socket.io/transports/websocket.js @@ -28,6 +28,22 @@ WebSocket.prototype._onConnect = function(req, socket){ + '://' + this.request.headers.host + this.request.url; if ('sec-websocket-key1' in this.request.headers){ + /* We need to send the 101 response immediately when using Draft 76 with + a load balancing proxy, such as HAProxy. In order to protect an + unsuspecting non-websocket HTTP server, HAProxy will not send the + 8-byte nonce through the connection until the Upgrade: WebSocket + request has been confirmed by the WebSocket server by a 101 response + indicating that the server can handle the upgraded protocol. We + therefore must send the 101 response immediately, and then wait for + the nonce to be forwarded to us afterward in order to finish the + Draft 76 handshake. + */ + + // If we don't have the nonce yet, wait for it. + if (!(this.upgradeHead && this.upgradeHead.length >= 8)) { + this.waitingForNonce = true; + } + headers = [ 'HTTP/1.1 101 WebSocket Protocol Handshake', 'Upgrade: WebSocket', @@ -47,12 +63,12 @@ WebSocket.prototype._onConnect = function(req, socket){ 'WebSocket-Origin: ' + origin, 'WebSocket-Location: ' + location ]; - - try { - this.connection.write(headers.concat('', '').join('\r\n')); - } catch(e){ - this._onClose(); - } + } + + try { + this.connection.write(headers.concat('', '').join('\r\n')); + } catch(e){ + this._onClose(); } this.connection.setTimeout(0); @@ -63,12 +79,39 @@ WebSocket.prototype._onConnect = function(req, socket){ self._handle(data); }); - if (this._proveReception(headers)) this._payload(); + req.addListener('error', function(err){ + req.end && req.end() || req.destroy && req.destroy(); + }); + socket.addListener('error', function(data){ + socket && (socket.end && socket.end() || socket.destroy && socket.destroy()); + }); + + if (this.waitingForNonce) { + // Since we will be receiving the binary nonce through the normal HTTP + // data event, set the connection to 'binary' temporarily + this.connection.setEncoding('binary'); + this._headers = headers; + } + else { + if (this._proveReception(headers)) this._payload(); + } }; WebSocket.prototype._handle = function(data){ var chunk, chunks, chunk_count; this.data += data; + if (this.waitingForNonce) { + if (this.data.length < 8) { return; } + // Restore the connection to utf8 encoding after receiving the nonce + this.connection.setEncoding('utf8'); + this.waitingForNonce = false; + // Stuff the nonce into the location where it's expected to be + this.upgradeHead = this.data.substr(0,8); + this.data = this.data.substr(8); + if (this._proveReception(this._headers)) { this._payload(); } + return; + } + chunks = this.data.split('\ufffd'); chunk_count = chunks.length - 1; for (var i = 0; i < chunk_count; i++){ @@ -114,7 +157,7 @@ WebSocket.prototype._proveReception = function(headers){ md5.update(this.upgradeHead.toString('binary')); try { - this.connection.write(headers.concat('', '').join('\r\n') + md5.digest('binary'), 'binary'); + this.connection.write(md5.digest('binary'), 'binary'); } catch(e){ this._onClose(); } From db03ea2a42a02b7980d21c2026a2edd2a2098d95 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Fri, 12 Nov 2010 07:54:18 -0300 Subject: [PATCH 04/36] Added new evented streaming decoder - Progressive data buffering support - Error reporting - Efficient, low memory footprint - Support for the new encoding - Message types - Termination character Tests for decoder Updated utils/utils tests. --- Makefile | 2 +- lib/socket.io/data.js | 131 +++++++++++++++++++++++++++++++ lib/socket.io/utils.js | 41 ---------- tests/data.js | 170 +++++++++++++++++++++++++++++++++++++++++ tests/utils.js | 44 ++--------- 5 files changed, 310 insertions(+), 78 deletions(-) create mode 100644 lib/socket.io/data.js create mode 100644 tests/data.js diff --git a/Makefile b/Makefile index c925f12feb..ec7c774509 100644 --- a/Makefile +++ b/Makefile @@ -7,4 +7,4 @@ test-cov: example: node ./example/server.js -.PHONY: example \ No newline at end of file +.PHONY: example diff --git a/lib/socket.io/data.js b/lib/socket.io/data.js new file mode 100644 index 0000000000..f03bb6859d --- /dev/null +++ b/lib/socket.io/data.js @@ -0,0 +1,131 @@ +/** + * Module dependencies + */ + +var EventEmitter = require('events').EventEmitter; + +/** + * Data decoder class + * + * @api public + */ + +function Decoder(){ + this.reset(); + this.buffer = ''; +} + +Decoder.prototype = { + + /** + * Add data to the buffer for parsing + * + * @param {String} data + * @api public + */ + add: function(data){ + this.buffer += data; + this.parse(); + }, + + /** + * Parse the current buffer + * + * @api private + */ + parse: function(){ + for (var l = this.buffer.length; this.i < l; this.i++){ + var chr = this.buffer[this.i]; + if (this.type === undefined){ + if (chr == ':') return this.error('Data type not specified'); + this.type = '' + chr; + continue; + } + if (this.length === undefined && chr == ':'){ + this.length = ''; + continue; + } + if (this.data === undefined){ + if (chr != ':'){ + this.length += chr; + } else { + if (this.length.length === 0) + return this.error('Data length not specified'); + this.length = Number(this.length); + this.data = ''; + } + continue; + } + if (this.data.length === this.length){ + if (chr == ','){ + this.emit('data', this.type, this.data); + this.buffer = this.buffer.substr(this.i + 1); + this.reset(); + return this.parse(); + } else { + return this.error('Termination character "," expected'); + } + } else { + this.data += chr; + } + } + }, + + /** + * Reset the parser state + * + * @api private + */ + + reset: function(){ + this.i = 0; + this.type = this.data = this.length = undefined; + }, + + /** + * Error handling functions + * + * @api private + */ + + error: function(reason){ + this.reset(); + this.emit('error', reason); + } + +}; + +/** + * Inherit from EventEmitter + */ + +Decoder.prototype.__proto__ = EventEmitter.prototype; + +/** + * Encode function + * + * Examples: + * encode([3, 'Message of type 3']); + * encode([[1, 'Message of type 1], [2, 'Message of type 2]]); + * + * @param {Array} list of messages + * @api public + */ + +function encode(messages){ + messages = Array.isArray(messages[0]) ? messages : [messages]; + var ret = ''; + for (var i = 0, str; i < messages.length; i++){ + str = messages[i][1]; + if (str === undefined || str === null || str === false) str = ''; + ret += msg[0] + ':' + msg[1].length + ':' + msg[1] + ','; + } + return ret; +} + +/** + * Export APIs + */ + +exports.Decoder = Decoder; +exports.encode = encode; diff --git a/lib/socket.io/utils.js b/lib/socket.io/utils.js index ba2fdeca20..979ee0b7a0 100644 --- a/lib/socket.io/utils.js +++ b/lib/socket.io/utils.js @@ -9,44 +9,3 @@ exports.merge = function(source, merge){ return source; }; -var frame = '~m~'; - -function stringify(message){ - if (Object.prototype.toString.call(message) == '[object Object]'){ - return '~j~' + JSON.stringify(message); - } else { - return String(message); - } -}; - -exports.encode = function(messages){ - var ret = '', message, - messages = Array.isArray(messages) ? messages : [messages]; - for (var i = 0, l = messages.length; i < l; i++){ - message = messages[i] === null || messages[i] === undefined ? '' : stringify(messages[i]); - ret += frame + message.length + frame + message; - } - return ret; -}; - -exports.decode = function(data){ - var messages = [], number, n; - do { - if (data.substr(0, 3) !== frame) return messages; - data = data.substr(3); - number = '', n = ''; - for (var i = 0, l = data.length; i < l; i++){ - n = Number(data.substr(i, 1)); - if (data.substr(i, 1) == n){ - number += n; - } else { - data = data.substr(number.length + frame.length) - number = Number(number); - break; - } - } - messages.push(data.substr(0, number)); // here - data = data.substr(number); - } while(data !== ''); - return messages; -}; \ No newline at end of file diff --git a/tests/data.js b/tests/data.js new file mode 100644 index 0000000000..86ebc4ad90 --- /dev/null +++ b/tests/data.js @@ -0,0 +1,170 @@ +var Decoder = require('socket.io/data').Decoder + , encode = require('socket.io/data').encode; + +module.exports = { + + 'test data decoding of message feed all at once': function(assert, beforeExit){ + var a = new Decoder() + , parsed = 0 + , errors = 0; + + a.on('error', function(){ + errors++; + }); + + a.on('data', function(type, message){ + parsed++; + if (parsed === 1){ + assert.ok(type === '0'); + assert.ok(message === ''); + } else if (parsed === 2){ + assert.ok(type === '1'); + assert.ok(message === 'r:chat:Hello world'); + } + }); + + a.add('0:0:,'); + a.add('1:18:r:chat:Hello world,'); + + beforeExit(function(){ + assert.ok(parsed === 2); + assert.ok(errors === 0); + }); + }, + + 'test data decoding by parts': function(assert, beforeExit){ + var a = new Decoder() + , parsed = 0 + , errors = 0; + + a.on('error', function(){ + errors++; + }); + + a.on('data', function(type, message){ + parsed++; + assert.ok(type === '5'); + assert.ok(message = '123456789'); + }); + + a.add('5'); + a.add(':9'); + a.add(':12345'); + a.add('678'); + a.add('9'); + a.add(',typefornextmessagewhichshouldbeignored'); + + beforeExit(function(){ + assert.ok(parsed === 1); + assert.ok(errors === 0); + }); + }, + + 'test data decoding of many messages at once': function(assert, beforeExit){ + var a = new Decoder() + , parsed = 0 + , errors = 0; + + a.on('error', function(){ + errors++; + }); + + a.on('data', function(type, message){ + parsed++; + switch (parsed){ + case 1: + assert.ok(type === '3'); + assert.ok(message === 'COOL,'); + break; + case 2: + assert.ok(type === '4'); + assert.ok(message === ''); + break; + case 3: + assert.ok(type === '5'); + assert.ok(message === ':∞…:'); + break; + } + }); + + a.add('3:5:COOL,,4:0:,5:4::∞…:,'); + + beforeExit(function(){ + assert.ok(parsed === 3); + assert.ok(errors === 0); + }); + }, + + 'test erroneous data decoding on undefined type': function(assert, beforeExit){ + var a = new Decoder() + , parsed = 0 + , errors = 0 + , error; + + a.on('data', function(){ + parsed++; + }); + + a.on('error', function(reason){ + errors++; + error = reason; + }); + + a.add(':'); + + beforeExit(function(){ + assert.ok(parsed === 0); + assert.ok(errors === 1); + assert.ok(error === 'Data type not specified'); + }); + }, + + 'test erroneous data decoding on undefined length': function(assert, beforeExit){ + var a = new Decoder() + , parsed = 0 + , errors = 0 + , error; + + a.on('data', function(){ + parsed++; + }); + + a.on('error', function(reason){ + errors++; + error = reason; + }); + + a.add('1::'); + + beforeExit(function(){ + assert.ok(parsed === 0); + assert.ok(errors === 1); + assert.ok(error === 'Data length not specified'); + }); + }, + + 'test erroneous data decoding on incorrect length': function(assert, beforeExit){ + var a = new Decoder() + , parsed = 0 + , errors = 0 + , error; + + a.on('data', function(){ + parsed++; + }); + + a.on('error', function(reason){ + errors++; + error = reason; + }); + + a.add('1:5:123456,'); + + beforeExit(function(){ + assert.ok(parsed === 0); + assert.ok(errors === 1); + assert.ok(error === 'Termination character "," expected'); + }); + } + +}; diff --git a/tests/utils.js b/tests/utils.js index bd4ae53f60..8a3c0068be 100644 --- a/tests/utils.js +++ b/tests/utils.js @@ -1,40 +1,12 @@ -var encode = require('socket.io/utils').encode - , decode = require('socket.io/utils').decode - , messageEncode = require('socket.io/utils').messageEncode - , messageDecode = require('socket.io/utils').messageDecode; +var merge = require('socket.io/utils').merge; module.exports = { - - 'test data decoding': function(assert){ - var disconnection = decode('0:0:,') - , message = decode('1:18:r:chat:Hello world,') - , incomplete = decode('5:100') - , incomplete2 = decode('6:3:') - , incomplete3 = decode('7:10:abcdefghi') - , unparseable = decode(':') - , unparseable2 = decode('1::'); - assert.ok(Array.isArray(disconnection)); - assert.ok(disconnection[0] === '0'); - assert.ok(disconnection[1] === ''); - assert.ok(Array.isArray(message)); - assert.ok(message[0] === '1'); - assert.ok(message[1] === 'Hello world'); - assert.ok(-1 === incomplete === incomplete2 === incomplete3); - assert.ok(false === unparseable === unparseable2); - }, - - 'test decoding of bad framed messages': function(assert){ - var decoded = decode('~m~5~m~abcde' + '~m\uffsdaasdfd9~m~1aaa23456789'); - assert.equal(decoded.length, 1); - assert.equal(decoded[0], 'abcde'); - assert.equal(decoded[1], undefined); - }, - - 'test encoding': function(assert){ - assert.equal(encode(['abcde', '123456789']), '~m~5~m~abcde' + '~m~9~m~123456789'); - assert.equal(encode('asdasdsad'), '~m~9~m~asdasdsad'); - assert.equal(encode(''), '~m~0~m~'); - assert.equal(encode(null), '~m~0~m~'); + 'test that merging an object works': function(assert){ + var a = { a: 'b', c: 'd' } + , b = { c: 'b' }; + assert.ok(merge(a,b).a === 'b'); + assert.ok(merge(a,b).c === 'b'); } -}; + +} From 07e07442f3ed963d3fd0c37a6b8f8d5219a2f821 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Fri, 12 Nov 2010 08:06:49 -0300 Subject: [PATCH 05/36] Fixed encoder and added encoding tests. Now at 100% test coverage for data.js Added .swo to gitignore --- .gitignore | 1 + lib/socket.io/data.js | 2 +- tests/data.js | 5 +++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 963b424b8e..f949aac625 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ lib-cov *.out *.pid.swp *.swp +*.swo diff --git a/lib/socket.io/data.js b/lib/socket.io/data.js index f03bb6859d..9422af514c 100644 --- a/lib/socket.io/data.js +++ b/lib/socket.io/data.js @@ -118,7 +118,7 @@ function encode(messages){ for (var i = 0, str; i < messages.length; i++){ str = messages[i][1]; if (str === undefined || str === null || str === false) str = ''; - ret += msg[0] + ':' + msg[1].length + ':' + msg[1] + ','; + ret += messages[i][0] + ':' + str.length + ':' + str + ','; } return ret; } diff --git a/tests/data.js b/tests/data.js index 86ebc4ad90..7630903204 100644 --- a/tests/data.js +++ b/tests/data.js @@ -165,6 +165,11 @@ module.exports = { assert.ok(errors === 1); assert.ok(error === 'Termination character "," expected'); }); + }, + + 'test encoding': function(assert){ + assert.ok(encode([3,'Testing']) == '3:7:Testing,'); + assert.ok(encode([[1,''],[2,'tobi']]) == '1:0:,2:4:tobi,'); } }; From efe074bccec73fa287b0b6a29eb9d0acd4851f31 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Fri, 12 Nov 2010 22:57:10 -0300 Subject: [PATCH 06/36] Started implementing the new Data API in the Client Message encoding function + tests Message decoding function started --- lib/socket.io/client.js | 19 ++++++++++------- lib/socket.io/data.js | 47 ++++++++++++++++++++++++++++++++++++++++- tests/data.js | 14 +++++++++++- 3 files changed, 70 insertions(+), 10 deletions(-) diff --git a/lib/socket.io/client.js b/lib/socket.io/client.js index 0e21fee502..9a4788a328 100644 --- a/lib/socket.io/client.js +++ b/lib/socket.io/client.js @@ -1,9 +1,10 @@ var urlparse = require('url').parse , OutgoingMessage = require('http').OutgoingMessage , Stream = require('net').Stream + , Decoder = require('./data').Decoder + , encode = require('./data').encode + , encodeMessage = require('./data').encodeMessage , options = require('./utils').options - , encode = require('./utils').encode - , decode = require('./utils').decode , merge = require('./utils').merge; var Client = module.exports = function(listener, req, res, options, head){ @@ -19,19 +20,21 @@ var Client = module.exports = function(listener, req, res, options, head){ this._heartbeats = 0; this.connected = false; this.upgradeHead = head; + this.decoder = new Decoder(); this._onConnect(req, res); }; require('sys').inherits(Client, process.EventEmitter); Client.prototype.send = function(message){ - if (!this._open || !(this.connection.readyState === 'open' || this.connection.readyState === 'writeOnly')){ - return this._queue(message); - } - this._write(encode(message)); - return this; + this.write('2', encodeMessage(message)); }; +Client.prototype.write = function(type, data){ + if (!this._open) return this._queue(type, data); + return this._write(encode([type, data])); +} + Client.prototype.broadcast = function(message){ if (!('sessionId' in this)) return this; this.listener.broadcast(message, this.sessionId); @@ -180,4 +183,4 @@ Client.prototype._verifyOrigin = function(origin){ return false; }; -for (var i in options) Client.prototype[i] = options[i]; \ No newline at end of file +for (var i in options) Client.prototype[i] = options[i]; diff --git a/lib/socket.io/data.js b/lib/socket.io/data.js index 9422af514c..b206b23442 100644 --- a/lib/socket.io/data.js +++ b/lib/socket.io/data.js @@ -117,15 +117,60 @@ function encode(messages){ var ret = ''; for (var i = 0, str; i < messages.length; i++){ str = messages[i][1]; - if (str === undefined || str === null || str === false) str = ''; + if (str === undefined || str === null) str = ''; ret += messages[i][0] + ':' + str.length + ':' + str + ','; } return ret; } +/** + * Encode message function + * + * @param {String} message + * @param {Object} annotations + * @api public + */ + +function encodeMessage(msg, annotations){ + var data = '' + , anns = annotations || {}; + for (var i = 0, v, k = Object.keys(anns), l = k.length; i < l; i++){ + v = anns[k[i]]; + data += k[i] + (v !== null && v !== undefined ? ':' + v : '') + "\n"; + } + data += ':' + (msg === undefined || msg === null ? '' : msg); + return data; +} + +/** + * Decode message function + * + * @param {String} message + * @api public + */ + +function decodeMessage(msg){ + var anns = {} + , data; + for (var i = 0, chr, key, value, l = msg.length; i < l; i++){ + chr = msg[i]; + if (i === 0 && chr === ':'){ + data = msg.substr(1); + break; + } + if (key == null && value == null && chr == ':'){ + data = msg.substr(i + 1); + break; + } + } + return [data, anns]; +} + /** * Export APIs */ exports.Decoder = Decoder; exports.encode = encode; +exports.encodeMessage = encodeMessage; +exports.decodeMessage = decodeMessage; diff --git a/tests/data.js b/tests/data.js index 7630903204..5bc34d90b5 100644 --- a/tests/data.js +++ b/tests/data.js @@ -1,5 +1,7 @@ var Decoder = require('socket.io/data').Decoder - , encode = require('socket.io/data').encode; + , encode = require('socket.io/data').encode + , encodeMessage = require('socket.io/data').encodeMessage + , decodeMessage = require('socket.io/data').decodeMessage; module.exports = { @@ -170,6 +172,16 @@ module.exports = { 'test encoding': function(assert){ assert.ok(encode([3,'Testing']) == '3:7:Testing,'); assert.ok(encode([[1,''],[2,'tobi']]) == '1:0:,2:4:tobi,'); + }, + + 'test message encoding without annotations': function(assert){ + assert.ok(encodeMessage('') === ':'); + assert.ok(encodeMessage('Testing') === ':Testing'); + }, + + 'test message encoding with annotations': function(assert){ + assert.ok(encodeMessage('', {j: null}) === 'j\n:'); + assert.ok(encodeMessage('Test', {j: null, re: 'test'}) === 'j\nre:test\n:Test'); } }; From 67245d0e147e66899c053cad0cb74b2d38aab20e Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sat, 13 Nov 2010 08:48:41 -0300 Subject: [PATCH 07/36] Finished decodeMessage function + tests --- lib/socket.io/data.js | 19 ++++++++++++++++++- tests/data.js | 23 +++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/lib/socket.io/data.js b/lib/socket.io/data.js index b206b23442..6dbb4f17d4 100644 --- a/lib/socket.io/data.js +++ b/lib/socket.io/data.js @@ -159,9 +159,26 @@ function decodeMessage(msg){ break; } if (key == null && value == null && chr == ':'){ - data = msg.substr(i + 1); + data = msg.substr(i + 1); break; } + if (chr === "\n"){ + anns[key] = value; + key = value = undefined; + continue; + } + if (key === undefined){ + key = chr; + continue; + } + if (value === undefined && chr == ':'){ + value = ''; + continue; + } + if (value !== undefined) + value += chr; + else + key += chr; } return [data, anns]; } diff --git a/tests/data.js b/tests/data.js index 5bc34d90b5..16f8a55761 100644 --- a/tests/data.js +++ b/tests/data.js @@ -182,6 +182,29 @@ module.exports = { 'test message encoding with annotations': function(assert){ assert.ok(encodeMessage('', {j: null}) === 'j\n:'); assert.ok(encodeMessage('Test', {j: null, re: 'test'}) === 'j\nre:test\n:Test'); + }, + + 'test message decoding without annotations': function(assert){ + var decoded1 = decodeMessage(':') + , decoded2 = decodeMessage(':Testing'); + + assert.ok(decoded1[0] === ''); + assert.ok(Object.keys(decoded1[1]).length === 0); + + assert.ok(decoded2[0] === 'Testing'); + assert.ok(Object.keys(decoded2[1]).length === 0); + }, + + 'test message decoding with annotations': function(assert){ + var decoded1 = decodeMessage('j\n:') + , decoded2 = decodeMessage('j\nre:test\n:Test'); + + assert.ok(decoded1[0] === ''); + assert.ok('j' in decoded1[1]); + + assert.ok(decoded2[0] === 'Test'); + assert.ok('j' in decoded2[1]); + assert.ok(decoded2[1].re === 'test'); } }; From be057880d96ef7cff68e88de280142e7333667e8 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sat, 13 Nov 2010 08:58:17 -0300 Subject: [PATCH 08/36] Heartbeat ping now uses correct message type Fixed message type index for regular messages --- lib/socket.io/client.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/socket.io/client.js b/lib/socket.io/client.js index 9a4788a328..da7afb073c 100644 --- a/lib/socket.io/client.js +++ b/lib/socket.io/client.js @@ -27,7 +27,7 @@ var Client = module.exports = function(listener, req, res, options, head){ require('sys').inherits(Client, process.EventEmitter); Client.prototype.send = function(message){ - this.write('2', encodeMessage(message)); + this.write('1', encodeMessage(message)); }; Client.prototype.write = function(type, data){ @@ -108,7 +108,7 @@ Client.prototype._payload = function(){ Client.prototype._heartbeat = function(){ var self = this; this._heartbeatInterval = setTimeout(function(){ - self.send('~h~' + ++self._heartbeats); + self.write('2', ++self._heartbeats); self._heartbeatTimeout = setTimeout(function(){ self._onClose(); }, self.options.timeout); From c97264a547d00043bc4d77c5921b1d8173db114a Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sat, 13 Nov 2010 10:27:58 -0300 Subject: [PATCH 09/36] Added _onData to handle data decoding _onMessage added as callback to the decoder Added proper message type handling Implemented _onData in the transports --- lib/socket.io/client.js | 35 ++++++++++++++--------- lib/socket.io/transports/htmlfile.js | 4 +-- lib/socket.io/transports/websocket.js | 4 +-- lib/socket.io/transports/xhr-multipart.js | 4 +-- lib/socket.io/transports/xhr-polling.js | 4 +-- 5 files changed, 29 insertions(+), 22 deletions(-) diff --git a/lib/socket.io/client.js b/lib/socket.io/client.js index da7afb073c..e4cd79a39e 100644 --- a/lib/socket.io/client.js +++ b/lib/socket.io/client.js @@ -4,6 +4,7 @@ var urlparse = require('url').parse , Decoder = require('./data').Decoder , encode = require('./data').encode , encodeMessage = require('./data').encodeMessage + , decodeMessage = require('./data').decodeMessage , options = require('./utils').options , merge = require('./utils').merge; @@ -21,6 +22,7 @@ var Client = module.exports = function(listener, req, res, options, head){ this.connected = false; this.upgradeHead = head; this.decoder = new Decoder(); + this.decoder.on('data', this._onMessage.bind(this)); this._onConnect(req, res); }; @@ -41,20 +43,25 @@ Client.prototype.broadcast = function(message){ return this; }; -Client.prototype._onMessage = function(data){ - var messages = decode(data); - if (messages === false) return this.listener.options.log('Bad message received from client ' + this.sessionId); - for (var i = 0, l = messages.length, frame; i < l; i++){ - frame = messages[i].substr(0, 3); - switch (frame){ - case '~h~': - return this._onHeartbeat(messages[i].substr(3)); - case '~j~': - messages[i] = JSON.parse(messages[i].substr(3)); - break; - } - this.emit('message', messages[i]); - this.listener._onClientMessage(messages[i], this); +Client.prototype._onData = function(data){ + this.decoder.add(data); +} + +Client.prototype._onMessage = function(type, data){ + switch (type){ + case '0': + this._onDisconnect(); + break; + + case '1': + var msg = decodeMessage(data); + // handle json decoding + if ('j' in msg[1]) msg[0] = JSON.parse(msg[0]); + this.emit('message', msg[0], msg[1]); + break; + + case '2': + this._onHeartbeat(data); } }; diff --git a/lib/socket.io/transports/htmlfile.js b/lib/socket.io/transports/htmlfile.js index 9812c101b7..a6d9d79906 100644 --- a/lib/socket.io/transports/htmlfile.js +++ b/lib/socket.io/transports/htmlfile.js @@ -30,7 +30,7 @@ HTMLFile.prototype._onConnect = function(req, res){ req.addListener('end', function(){ try { var msg = qs.parse(body); - self._onMessage(msg.data); + self._onData(msg.data); } catch(e){} res.writeHead(200, {'Content-Type': 'text/plain'}); res.write('ok'); @@ -43,4 +43,4 @@ HTMLFile.prototype._onConnect = function(req, res){ HTMLFile.prototype._write = function(message){ if (this._open) this.response.write(''); //json for escaping -}; \ No newline at end of file +}; diff --git a/lib/socket.io/transports/websocket.js b/lib/socket.io/transports/websocket.js index d3798444c2..637f2cf72e 100644 --- a/lib/socket.io/transports/websocket.js +++ b/lib/socket.io/transports/websocket.js @@ -78,7 +78,7 @@ WebSocket.prototype._handle = function(data){ this._onClose(); return false; } - this._onMessage(chunk.slice(1)); + this._onData(chunk.slice(1)); } this.data = chunks[chunks.length - 1]; }; @@ -133,4 +133,4 @@ WebSocket.prototype._write = function(message){ } }; -WebSocket.httpUpgrade = true; \ No newline at end of file +WebSocket.httpUpgrade = true; diff --git a/lib/socket.io/transports/xhr-multipart.js b/lib/socket.io/transports/xhr-multipart.js index 1c2e9c7253..dd45735613 100644 --- a/lib/socket.io/transports/xhr-multipart.js +++ b/lib/socket.io/transports/xhr-multipart.js @@ -44,7 +44,7 @@ Multipart.prototype._onConnect = function(req, res){ req.addListener('end', function(){ try { var msg = qs.parse(body); - self._onMessage(msg.data); + self._onData(msg.data); } catch(e){} res.writeHead(200, headers); res.write('ok'); @@ -61,4 +61,4 @@ Multipart.prototype._write = function(message){ this.response.write(message + "\n"); this.response.write("--socketio\n"); } -}; \ No newline at end of file +}; diff --git a/lib/socket.io/transports/xhr-polling.js b/lib/socket.io/transports/xhr-polling.js index e85392c5b4..f8c2b3fa23 100644 --- a/lib/socket.io/transports/xhr-polling.js +++ b/lib/socket.io/transports/xhr-polling.js @@ -47,7 +47,7 @@ Polling.prototype._onConnect = function(req, res){ try { // optimization: just strip first 5 characters here? var msg = qs.parse(body); - self._onMessage(msg.data); + self._onData(msg.data); } catch(e){} res.writeHead(200, headers); res.write('ok'); @@ -75,4 +75,4 @@ Polling.prototype._write = function(message){ this.response.end(); this._onClose(); } -}; \ No newline at end of file +}; From e5e84f3dd1f8c3226d82c5f5132caabe3af8da4c Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sat, 13 Nov 2010 11:43:17 -0300 Subject: [PATCH 10/36] Added encode() and decode() utility functions for tests that follow the new encoding/decoding API Adapted tests --- tests/transports.websocket.js | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/tests/transports.websocket.js b/tests/transports.websocket.js index 0496eb81fd..0b05df2842 100644 --- a/tests/transports.websocket.js +++ b/tests/transports.websocket.js @@ -1,6 +1,8 @@ var io = require('socket.io') , encode = require('socket.io/utils').encode - , decode = require('socket.io/utils').decode + , Decoder = require('socket.io/utils').Decoder + , decodeMessage = require('socket.io/utils').decodeMessage + , encodeMessage = require('socket.io/utils').encodeMessage , port = 7200 , Listener = io.Listener , Client = require('socket.io/client') @@ -28,6 +30,18 @@ function client(server, sessid){ return new WebSocket('ws://localhost:' + server._port + '/socket.io/websocket' + sessid, 'borf'); }; +function encode(msg){ + return encode('1', encodeMessage(msg)); +} + +function decode(data, fn){ + var decoder = new Decoder(); + decoder.on('data', function(type, msg){ + fn(type == '1' ? decodeMessage(msg) : msg); + }); + decoder.add(data); +} + module.exports = { 'test connection and handshake': function(assert){ @@ -49,7 +63,9 @@ module.exports = { }; _client.onmessage = function(ev){ if (++messages == 2){ // first message is the session id - assert.ok(decode(ev.data), 'from server'); + decode(ev.data, function(msg){ + assert.ok(msg === 'from server'); + }); --trips || close(); } }; @@ -114,7 +130,9 @@ module.exports = { var _client2 = client(_server, sessionid); _client2.onmessage = function(ev){ assert.ok(Object.keys(_socket.clients).length == 1); - assert.ok(decode(ev.data), 'should get this'); + decode(ev.data, function(msg){ + assert.ok(msg === 'should get this'); + }); _socket.clients[sessionid].options.closeTimeout = 0; _client2.close(); _server.close(); @@ -154,7 +172,9 @@ module.exports = { _client.onmessage = function(ev){ if (++messages == 2){ assert.ok(decode(ev.data)[0].substr(0, 3) == '~j~'); - assert.ok(JSON.parse(decode(ev.data)[0].substr(3)).from == 'server'); + decode(ev.data, function(msg){ + assert.ok(msg.indexOf("j\n" === 0); + }); _client.send(encode({ from: 'client' })); --trips || close(); } @@ -242,4 +262,4 @@ module.exports = { }); } -}; \ No newline at end of file +}; From 252ff5ba06c482c763a8a76ba0234367a82ffd86 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 15 Nov 2010 00:06:39 -0300 Subject: [PATCH 11/36] Make sure Client#send applies the `j` annotation for JSON encoding Refactored Client#_payload to make sure the sessionid message (3) is sent Fixed _queue to work with message type and data arguments data#encode was failing to compute length for non-string values. Fixed encode() test utility --- lib/socket.io/client.js | 46 +++++++++++------- lib/socket.io/data.js | 10 ++-- tests/transports.websocket.js | 92 ++++++++++++++++++++--------------- 3 files changed, 87 insertions(+), 61 deletions(-) diff --git a/lib/socket.io/client.js b/lib/socket.io/client.js index e4cd79a39e..c39b8f74ab 100644 --- a/lib/socket.io/client.js +++ b/lib/socket.io/client.js @@ -28,8 +28,11 @@ var Client = module.exports = function(listener, req, res, options, head){ require('sys').inherits(Client, process.EventEmitter); -Client.prototype.send = function(message){ - this.write('1', encodeMessage(message)); +Client.prototype.send = function(message, anns){ + anns = anns || {}; + if (typeof message == 'object') anns['j'] = null; + message = typeof message == 'object' ? JSON.stringify(message) : message; + this.write('1', encodeMessage(message, anns)); }; Client.prototype.write = function(type, data){ @@ -92,26 +95,34 @@ Client.prototype._onConnect = function(req, res){ }; Client.prototype._payload = function(){ - var payload = []; - + this._writeQueue = this._writeQueue || []; this.connections++; this.connected = true; this._open = true; if (!this.handshaked){ this._generateSessionId(); - payload.push(this.sessionId); + this._writeQueue.unshift(['3', this.sessionId]); this.handshaked = true; } - payload = payload.concat(this._writeQueue || []); - this._writeQueue = []; + // we dispatch the encoded current queue + // in the future encoding will be handled by _write, that way we can + // avoid framing for protocols with framing built-in (WebSocket) + if (this._writeQueue.length){ + this._write(encode(this._writeQueue)); + this._writeQueue = []; + } + + // if this is the first connection we emit the `connection` ev + if (this.connections === 1) + this.listener._onClientConnect(this); - if (payload.length) this._write(encode(payload)); - if (this.connections === 1) this.listener._onClientConnect(this); - if (this.options.timeout) this._heartbeat(); + // send the timeout + if (this.options.timeout) + this._heartbeat(); }; - + Client.prototype._heartbeat = function(){ var self = this; this._heartbeatInterval = setTimeout(function(){ @@ -119,7 +130,7 @@ Client.prototype._heartbeat = function(){ self._heartbeatTimeout = setTimeout(function(){ self._onClose(); }, self.options.timeout); - }, self.options.heartbeatInterval); + }, this.options.heartbeatInterval); }; Client.prototype._onHeartbeat = function(h){ @@ -163,9 +174,9 @@ Client.prototype._onDisconnect = function(){ } }; -Client.prototype._queue = function(message){ +Client.prototype._queue = function(type, data){ this._writeQueue = this._writeQueue || []; - this._writeQueue.push(message); + this._writeQueue.push([type, data]); return this; }; @@ -176,10 +187,10 @@ Client.prototype._generateSessionId = function(){ Client.prototype._verifyOrigin = function(origin){ var origins = this.listener.options.origins; - if (origins.indexOf('*:*') !== -1) { + if (origins.indexOf('*:*') !== -1) return true; - } - if (origin) { + + if (origin){ try { var parts = urlparse(origin); return origins.indexOf(parts.host + ':' + parts.port) !== -1 || @@ -187,6 +198,7 @@ Client.prototype._verifyOrigin = function(origin){ origins.indexOf('*:' + parts.port) !== -1; } catch (ex) {} } + return false; }; diff --git a/lib/socket.io/data.js b/lib/socket.io/data.js index 6dbb4f17d4..918305b9af 100644 --- a/lib/socket.io/data.js +++ b/lib/socket.io/data.js @@ -13,7 +13,7 @@ var EventEmitter = require('events').EventEmitter; function Decoder(){ this.reset(); this.buffer = ''; -} +}; Decoder.prototype = { @@ -116,12 +116,12 @@ function encode(messages){ messages = Array.isArray(messages[0]) ? messages : [messages]; var ret = ''; for (var i = 0, str; i < messages.length; i++){ - str = messages[i][1]; + str = String(messages[i][1]); if (str === undefined || str === null) str = ''; ret += messages[i][0] + ':' + str.length + ':' + str + ','; } return ret; -} +}; /** * Encode message function @@ -140,7 +140,7 @@ function encodeMessage(msg, annotations){ } data += ':' + (msg === undefined || msg === null ? '' : msg); return data; -} +}; /** * Decode message function @@ -181,7 +181,7 @@ function decodeMessage(msg){ key += chr; } return [data, anns]; -} +}; /** * Export APIs diff --git a/tests/transports.websocket.js b/tests/transports.websocket.js index 0b05df2842..87d9b2f759 100644 --- a/tests/transports.websocket.js +++ b/tests/transports.websocket.js @@ -1,8 +1,8 @@ var io = require('socket.io') - , encode = require('socket.io/utils').encode - , Decoder = require('socket.io/utils').Decoder - , decodeMessage = require('socket.io/utils').decodeMessage - , encodeMessage = require('socket.io/utils').encodeMessage + , Encode = require('socket.io/data').encode + , Decoder = require('socket.io/data').Decoder + , decodeMessage = require('socket.io/data').decodeMessage + , encodeMessage = require('socket.io/data').encodeMessage , port = 7200 , Listener = io.Listener , Client = require('socket.io/client') @@ -31,16 +31,19 @@ function client(server, sessid){ }; function encode(msg){ - return encode('1', encodeMessage(msg)); -} + var atts = {}; + if (typeof msg == 'object') atts['j'] = null; + msg = typeof msg == 'object' ? JSON.stringify(msg) : msg; + return Encode(['1', encodeMessage(msg, atts)]); +}; function decode(data, fn){ var decoder = new Decoder(); decoder.on('data', function(type, msg){ - fn(type == '1' ? decodeMessage(msg) : msg); + fn(type == '1' ? decodeMessage(msg) : msg, type); }); decoder.add(data); -} +}; module.exports = { @@ -62,12 +65,15 @@ module.exports = { _client.send(encode('from client')); }; _client.onmessage = function(ev){ - if (++messages == 2){ // first message is the session id - decode(ev.data, function(msg){ - assert.ok(msg === 'from server'); - }); - --trips || close(); - } + decode(ev.data, function(msg, type){ + messages++; + if (messages == 1){ + assert.ok(type == '3'); + } else if (messages == 2){ + assert.ok(msg[0] === 'from server'); + --trips || close(); + } + }); }; }); @@ -85,7 +91,7 @@ module.exports = { 'test clients tracking': function(assert){ var _server = server() , _socket = socket(_server); - + listen(_server, function(){ var _client = client(_server); _client.onopen = function(){ @@ -131,11 +137,11 @@ module.exports = { _client2.onmessage = function(ev){ assert.ok(Object.keys(_socket.clients).length == 1); decode(ev.data, function(msg){ - assert.ok(msg === 'should get this'); + assert.ok(msg[0] === 'should get this'); + _socket.clients[sessionid].options.closeTimeout = 0; + _client2.close(); + _server.close(); }); - _socket.clients[sessionid].options.closeTimeout = 0; - _client2.close(); - _server.close(); }; runOnce = true; } @@ -171,12 +177,11 @@ module.exports = { _client = client(_server); _client.onmessage = function(ev){ if (++messages == 2){ - assert.ok(decode(ev.data)[0].substr(0, 3) == '~j~'); decode(ev.data, function(msg){ - assert.ok(msg.indexOf("j\n" === 0); + assert.ok('j' in msg[1]); + _client.send(encode({ from: 'client' })); + --trips || close(); }); - _client.send(encode({ from: 'client' })); - --trips || close(); } }; }); @@ -198,15 +203,18 @@ module.exports = { , messages = 0; _client.onmessage = function(ev){ ++messages; - if (decode(ev.data)[0].substr(0, 3) == '~h~'){ - assert.ok(messages === 2); - assert.ok(Object.keys(_socket.clients).length == 1); - setTimeout(function(){ - assert.ok(Object.keys(_socket.clients).length == 0); - _client.close(); - _server.close(); - }, 150); - } + decode(ev.data, function(msg, type){ + if (type === '2'){ + assert.ok(messages === 2); + assert.ok(msg === '1'); + assert.ok(Object.keys(_socket.clients).length == 1); + setTimeout(function(){ + assert.ok(Object.keys(_socket.clients).length == 0); + _client.close(); + _server.close(); + }, 150); + } + }); }; }); }, @@ -225,11 +233,13 @@ module.exports = { _client.onmessage = function(ev){ if (!('messages' in _client)) _client.messages = 0; if (++_client.messages == 2){ - assert.ok(decode(ev.data)[0] == 'not broadcasted'); - _client.close(); - _client2.close(); - _client3.close(); - _server.close(); + decode(ev.data, function(msg){ + assert.ok(msg[0] === 'not broadcasted'); + _client.close(); + _client2.close(); + _client3.close(); + _server.close(); + }); } }; @@ -238,14 +248,18 @@ module.exports = { _client2.onmessage = function(ev){ if (!('messages' in _client2)) _client2.messages = 0; if (++_client2.messages == 2) - assert.ok(decode(ev.data)[0] == 'broadcasted') + decode(ev.data, function(msg){ + assert.ok(msg[0] === 'broadcasted'); + }); }; _client2.onopen = function(){ _client3 = client(_server); _client3.onmessage = function(ev){ if (!('messages' in _client3)) _client3.messages = 0; if (++_client3.messages == 2) - assert.ok(decode(ev.data)[0] == 'broadcasted') + decode(ev.data, function(msg){ + assert.ok(msg[0] === 'broadcasted'); + }); }; }; }; From 3f4c1565d4f05df825309fc1fe7e287b543b4342 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 15 Nov 2010 01:56:20 -0300 Subject: [PATCH 12/36] Added .swn to git ignore Added `tests` utility with common testing globals Refactored htmlfile tests to use the new utilities Refactores websocket tests to use the new utilities Cleaned up flash socket tests --- .gitignore | 1 + lib/socket.io/tests.js | 43 ++++++++++++++ tests/listener.js | 37 ++++-------- tests/transports.flashsocket.js | 29 +-------- tests/transports.htmlfile.js | 100 ++++++++++++++++---------------- tests/transports.websocket.js | 37 +----------- 6 files changed, 110 insertions(+), 137 deletions(-) create mode 100644 lib/socket.io/tests.js diff --git a/.gitignore b/.gitignore index f949aac625..67419b4650 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ lib-cov *.pid.swp *.swp *.swo +*.swn diff --git a/lib/socket.io/tests.js b/lib/socket.io/tests.js new file mode 100644 index 0000000000..887c4c95f2 --- /dev/null +++ b/lib/socket.io/tests.js @@ -0,0 +1,43 @@ +var io = require('socket.io') + , Encode = require('socket.io/data').encode + , Decoder = require('socket.io/data').Decoder + , decodeMessage = require('socket.io/data').decodeMessage + , encodeMessage = require('socket.io/data').encodeMessage + , WebSocket = require('./../../support/node-websocket-client/lib/websocket').WebSocket; + +module.exports = { + + server: function(callback){ + return require('http').createServer(callback || function(){}); + }, + + socket: function(server, options){ + if (!options) options = {}; + options.log = false; + return io.listen(server, options); + }, + + encode: function(msg){ + var atts = {}; + if (typeof msg == 'object') atts['j'] = null; + msg = typeof msg == 'object' ? JSON.stringify(msg) : msg; + return Encode(['1', encodeMessage(msg, atts)]); + }, + + decode: function(data, fn){ + var decoder = new Decoder(); + decoder.on('data', function(type, msg){ + fn(type == '1' ? decodeMessage(msg) : msg, type); + }); + decoder.add(data); + }, + + client: function(server, sessid){ + sessid = sessid ? '/' + sessid : ''; + return new WebSocket('ws://localhost:' + server._port + '/socket.io/websocket' + sessid, 'borf'); + } + +}; + +for (var i in module.exports) + global[i] = module.exports[i]; diff --git a/tests/listener.js b/tests/listener.js index 51b558e8b2..413d090cbc 100644 --- a/tests/listener.js +++ b/tests/listener.js @@ -1,32 +1,11 @@ var http = require('http') , net = require('net') , io = require('socket.io') - , decode = require('socket.io/utils').decode , port = 7100 , Listener = io.Listener , WebSocket = require('./../support/node-websocket-client/lib/websocket').WebSocket; -function server(callback){ - return http.createServer(callback || function(){}); -}; - -function socket(server, options){ - if (!options) options = {}; - if (options.log === undefined) options.log = false; - return io.listen(server, options); -}; - -function listen(s, callback){ - s._port = port; - s.listen(port, callback); - port++; - return s; -}; - -function client(server, sessid){ - sessid = sessid ? '/' + sessid : ''; - return new WebSocket('ws://localhost:' + server._port + '/socket.io/websocket' + sessid, 'borf'); -}; +require('socket.io/tests'); module.exports = { @@ -108,16 +87,20 @@ module.exports = { if (!_client1._first){ _client1._first = true; } else { - assert.ok(decode(ev.data)[0] == 'broadcasted msg'); - --trips || close(); + decode(ev.data, function(msg){ + assert.ok(msg === 'broadcasted msg'); + --trips || close(); + }); } }; _client2.onmessage = function(ev){ if (!_client2._first){ _client2._first = true; } else { - assert.ok(decode(ev.data)[0] == 'broadcasted msg'); - --trips || close(); + decode(ev.data, function(msg){ + assert.ok(msg === 'broadcasted msg'); + --trips || close(); + }): } }; }) @@ -149,4 +132,4 @@ module.exports = { assert.response(_server, { url: '/socket.io/inexistent' }, { body: 'All cool' }); } -}; \ No newline at end of file +}; diff --git a/tests/transports.flashsocket.js b/tests/transports.flashsocket.js index 5f8169a24f..befaf208cd 100644 --- a/tests/transports.flashsocket.js +++ b/tests/transports.flashsocket.js @@ -2,32 +2,9 @@ var io = require('socket.io') , net = require('net') , http = require('http') , querystring = require('querystring') - , port = 7700 - , encode = require('socket.io/utils').encode - , decode = require('socket.io/utils').decode; + , port = 7700; -function server(callback){ - return http.createServer(function(){}); -}; - -function socket(server, options){ - if (!options) options = {}; - options.log = false; - if (!options.transportOptions) options.transportOptions = { - 'flashsocket': { - // disable heartbeats for tests, re-enabled in heartbeat test below - timeout: null - } - }; - return io.listen(server, options); -}; - -function listen(s, callback){ - s._port = port; - s.listen(port, callback); - port++; - return s; -}; +require('socket.io/tests'); module.exports = { @@ -45,4 +22,4 @@ module.exports = { }); } -} \ No newline at end of file +} diff --git a/tests/transports.htmlfile.js b/tests/transports.htmlfile.js index dee623e099..8cc88f20e9 100644 --- a/tests/transports.htmlfile.js +++ b/tests/transports.htmlfile.js @@ -3,13 +3,13 @@ var io = require('socket.io') , http = require('http') , querystring = require('querystring') , port = 7600 - , encode = require('socket.io/utils').encode - , decode = require('socket.io/utils').decode , EventEmitter = require('events').EventEmitter , HTMLFile = require('socket.io/transports/htmlfile'); - -function server(callback){ - return http.createServer(function(){}); + +require('socket.io/tests'); + +function client(s){ + return http.createClient(s._port, 'localhost'); }; function listen(s, callback){ @@ -19,10 +19,6 @@ function listen(s, callback){ return s; }; -function client(s){ - return http.createClient(s._port, 'localhost'); -}; - function socket(server, options){ if (!options) options = {}; options.log = false; @@ -86,19 +82,20 @@ module.exports = { var _client = get(_server, '/socket.io/htmlfile', assert, function(response){ var i = 0; response.on('data', function(data){ - var msg = decode(data); - switch (i++){ - case 0: - assert.ok(Object.keys(_socket.clients).length == 1); - assert.ok(msg == Object.keys(_socket.clients)[0]); - assert.ok(_socket.clients[msg] instanceof HTMLFile); - _socket.clients[msg].send('from server'); - post(client(_server), '/socket.io/htmlfile/' + msg + '/send', {data: encode('from client')}); - break; - case 1: - assert.ok(msg == 'from server'); - --trips || close(); - } + decode(data, function(msg){ + switch (i++){ + case 0: + assert.ok(Object.keys(_socket.clients).length == 1); + assert.ok(msg == Object.keys(_socket.clients)[0]); + assert.ok(_socket.clients[msg] instanceof HTMLFile); + _socket.clients[msg].send('from server'); + post(client(_server), '/socket.io/htmlfile/' + msg + '/send', {data: encode('from client')}); + break; + case 1: + assert.ok(msg[0] === 'from server'); + --trips || close(); + } + }); }); }); @@ -155,26 +152,30 @@ module.exports = { var once = false; response.on('data', function(data){ if (!once){ - var sessid = decode(data); - assert.ok(_socket.clients[sessid]._open === true); - assert.ok(_socket.clients[sessid].connected); - - _socket.clients[sessid].connection.addListener('end', function(){ - assert.ok(_socket.clients[sessid]._open === false); + _client.end(); + once = true; + + decode(data, function(sessid){ + assert.ok(_socket.clients[sessid]._open === true); assert.ok(_socket.clients[sessid].connected); - _socket.clients[sessid].send('from server'); - - _client = get(_server, '/socket.io/htmlfile/' + sessid, assert, function(response){ - response.on('data', function(data){ - assert.ok(decode(data) == 'from server'); - _client.end(); - _server.close(); + _socket.clients[sessid].connection.addListener('end', function(){ + assert.ok(_socket.clients[sessid]._open === false); + assert.ok(_socket.clients[sessid].connected); + + _socket.clients[sessid].send('from server'); + + _client = get(_server, '/socket.io/htmlfile/' + sessid, assert, function(response){ + response.on('data', function(data){ + decode(data, function(msg){ + assert.ok(msg[0] === 'from server'); + _client.end(); + _server.close(); + }); + }); }); - }); + }); }); - _client.end(); - once = true; } }); }); @@ -196,19 +197,20 @@ module.exports = { var messages = 0; response.on('data', function(data){ ++messages; - var msg = decode(data); - if (msg[0].substr(0, 3) == '~h~'){ - assert.ok(messages == 2); - assert.ok(Object.keys(_socket.clients).length == 1); - setTimeout(function(){ - assert.ok(Object.keys(_socket.clients).length == 0); - client.end(); - _server.close(); - }, 150); - } + decode(data, function(msg, type){ + if (type == '2'){ + assert.ok(messages == 2); + assert.ok(Object.keys(_socket.clients).length == 1); + setTimeout(function(){ + assert.ok(Object.keys(_socket.clients).length == 0); + client.end(); + _server.close(); + }, 150); + } + }); }); }); }); } -}; \ No newline at end of file +}; diff --git a/tests/transports.websocket.js b/tests/transports.websocket.js index 87d9b2f759..87ecff99f4 100644 --- a/tests/transports.websocket.js +++ b/tests/transports.websocket.js @@ -1,22 +1,9 @@ var io = require('socket.io') - , Encode = require('socket.io/data').encode - , Decoder = require('socket.io/data').Decoder - , decodeMessage = require('socket.io/data').decodeMessage - , encodeMessage = require('socket.io/data').encodeMessage , port = 7200 , Listener = io.Listener - , Client = require('socket.io/client') - , WebSocket = require('./../support/node-websocket-client/lib/websocket').WebSocket; + , Client = require('socket.io/client'); -function server(){ - return require('http').createServer(function(){}); -}; - -function socket(server, options){ - if (!options) options = {}; - options.log = false; - return io.listen(server, options); -}; +require('socket.io/tests'); function listen(s, callback){ s._port = port; @@ -25,26 +12,6 @@ function listen(s, callback){ return s; }; -function client(server, sessid){ - sessid = sessid ? '/' + sessid : ''; - return new WebSocket('ws://localhost:' + server._port + '/socket.io/websocket' + sessid, 'borf'); -}; - -function encode(msg){ - var atts = {}; - if (typeof msg == 'object') atts['j'] = null; - msg = typeof msg == 'object' ? JSON.stringify(msg) : msg; - return Encode(['1', encodeMessage(msg, atts)]); -}; - -function decode(data, fn){ - var decoder = new Decoder(); - decoder.on('data', function(type, msg){ - fn(type == '1' ? decodeMessage(msg) : msg, type); - }); - decoder.add(data); -}; - module.exports = { 'test connection and handshake': function(assert){ From c0d7f04f3608212708acd33f20ac62c929ef2c3f Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 15 Nov 2010 02:17:48 -0300 Subject: [PATCH 13/36] JSONP-polling tests adapted to new message encoding/decoding --- tests/transports.jsonp-polling.js | 75 +++++++++++++++++-------------- 1 file changed, 42 insertions(+), 33 deletions(-) diff --git a/tests/transports.jsonp-polling.js b/tests/transports.jsonp-polling.js index fef64db5a0..99f79bcb67 100644 --- a/tests/transports.jsonp-polling.js +++ b/tests/transports.jsonp-polling.js @@ -2,18 +2,20 @@ var io = require('socket.io') , http = require('http') , querystring = require('querystring') , port = 7500 - , encode = require('socket.io/utils').encode - , _decode = require('socket.io/utils').decode , Polling = require('socket.io/transports/jsonp-polling'); -function decode(data){ +require('socket.io/tests'); + +function jsonp_decode(data, fn){ var io = { JSONP: [{ - '_': _decode + '_': function(msg){ + decode(msg, fn); + } }] }; // the decode function simulates a browser executing the javascript jsonp call - return eval(data); + eval(data); }; function server(callback){ @@ -81,19 +83,23 @@ module.exports = { listen(_server, function(){ get(client(_server), '/socket.io/jsonp-polling/', function(data){ - var sessid = decode(data); - assert.ok(Object.keys(_socket.clients).length == 1); - assert.ok(sessid == Object.keys(_socket.clients)[0]); - assert.ok(_socket.clients[sessid] instanceof Polling); - - _socket.clients[sessid].send('from server'); - - get(client(_server), '/socket.io/jsonp-polling/' + sessid, function(data){ - assert.ok(decode(data), 'from server'); - --trips || _server.close(); + jsonp_decode(data, function(sessid){ + assert.ok(Object.keys(_socket.clients).length == 1); + assert.ok(sessid == Object.keys(_socket.clients)[0]); + assert.ok(_socket.clients[sessid] instanceof Polling); + + _socket.clients[sessid].send('from server'); + + get(client(_server), '/socket.io/jsonp-polling/' + sessid, function(data){ + jsonp_decode(data, function(msg){ + assert.ok(msg[0] === 'from server'); + --trips || _server.close(); + }); + }); + + post(client(_server), '/socket.io/jsonp-polling/' + sessid + + '/send//0', {data: encode('from client')}); }); - - post(client(_server), '/socket.io/jsonp-polling/' + sessid + '/send//0', {data: encode('from client')}); }); }); }, @@ -124,25 +130,28 @@ module.exports = { listen(_server, function(){ get(client(_server), '/socket.io/jsonp-polling/', function(data){ - var sessid = decode(data); - assert.ok(_socket.clients[sessid]._open === false); - assert.ok(_socket.clients[sessid].connected); - _socket.clients[sessid].send('from server'); - get(client(_server), '/socket.io/jsonp-polling/' + sessid, function(data){ - var durationCheck; - assert.ok(decode(data) == 'from server'); - setTimeout(function(){ - assert.ok(_socket.clients[sessid]._open); - assert.ok(_socket.clients[sessid].connected); - durationCheck = true; - }, 50); - get(client(_server), '/socket.io/jsonp-polling/' + sessid, function(){ - assert.ok(durationCheck); - _server.close(); + jsonp_decode(data, function(sessid){ + assert.ok(_socket.clients[sessid]._open === false); + assert.ok(_socket.clients[sessid].connected); + _socket.clients[sessid].send('from server'); + get(client(_server), '/socket.io/jsonp-polling/' + sessid, function(data){ + var durationCheck; + jsonp_decode(data, function(msg){ + assert.ok(msg[0] === 'from server'); + setTimeout(function(){ + assert.ok(_socket.clients[sessid]._open); + assert.ok(_socket.clients[sessid].connected); + durationCheck = true; + }, 50); + get(client(_server), '/socket.io/jsonp-polling/' + sessid, function(){ + assert.ok(durationCheck); + _server.close(); + }); + }); }); }); }); }); } -}; \ No newline at end of file +}; From 8eecfd9f0e86fb5d0ece82f71a7542c7f8ecc175 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 15 Nov 2010 02:28:21 -0300 Subject: [PATCH 14/36] Adapted xhr-multipart tests to new message encoding/decoding --- tests/transports.xhr-multipart.js | 92 ++++++++++++++++--------------- 1 file changed, 47 insertions(+), 45 deletions(-) diff --git a/tests/transports.xhr-multipart.js b/tests/transports.xhr-multipart.js index 67cc02c092..9ee8a304be 100644 --- a/tests/transports.xhr-multipart.js +++ b/tests/transports.xhr-multipart.js @@ -3,14 +3,10 @@ var io = require('socket.io') , http = require('http') , querystring = require('querystring') , port = 7300 - , encode = require('socket.io/utils').encode - , decode = require('socket.io/utils').decode , EventEmitter = require('events').EventEmitter , Multipart = require('socket.io/transports/xhr-multipart'); - -function server(callback){ - return http.createServer(function(){}); -}; + +require('socket.io/tests'); function listen(s, callback){ s._port = port; @@ -87,19 +83,21 @@ module.exports = { var _client = get(_server, '/socket.io/xhr-multipart', function(response){ var i = 0; response.on('data', function(data){ - var msg = decode(data); - switch (i++){ - case 0: - assert.ok(Object.keys(_socket.clients).length == 1); - assert.ok(msg == Object.keys(_socket.clients)[0]); - assert.ok(_socket.clients[msg] instanceof Multipart); - _socket.clients[msg].send('from server'); - post(client(_server), '/socket.io/xhr-multipart/' + msg + '/send', {data: encode('from client')}); - break; - case 1: - assert.ok(msg == 'from server'); - --trips || close(); - } + decode(data, function(msg){ + switch (i++){ + case 0: + assert.ok(Object.keys(_socket.clients).length == 1); + assert.ok(msg == Object.keys(_socket.clients)[0]); + assert.ok(_socket.clients[msg] instanceof Multipart); + _socket.clients[msg].send('from server'); + post(client(_server), '/socket.io/xhr-multipart/' + msg + '/send', {data: encode('from client')}); + break; + + case 1: + assert.ok(msg[0] === 'from server'); + --trips || close(); + } + }); }); }); @@ -156,26 +154,29 @@ module.exports = { var once = false; response.on('data', function(data){ if (!once){ - var sessid = decode(data); - assert.ok(_socket.clients[sessid]._open === true); - assert.ok(_socket.clients[sessid].connected); - - _socket.clients[sessid].connection.addListener('end', function(){ - assert.ok(_socket.clients[sessid]._open === false); + _client.end(); + once = true; + decode(data, function(sessid){ + assert.ok(_socket.clients[sessid]._open === true); assert.ok(_socket.clients[sessid].connected); - _socket.clients[sessid].send('from server'); - - _client = get(_server, '/socket.io/xhr-multipart/' + sessid, function(response){ - response.on('data', function(data){ - assert.ok(decode(data) == 'from server'); - _client.end(); - _server.close(); + _socket.clients[sessid].connection.addListener('end', function(){ + assert.ok(_socket.clients[sessid]._open === false); + assert.ok(_socket.clients[sessid].connected); + + _socket.clients[sessid].send('from server'); + + _client = get(_server, '/socket.io/xhr-multipart/' + sessid, function(response){ + response.on('data', function(data){ + decode(data, function(msg){ + assert.ok(msg[0] === 'from server'); + _client.end(); + _server.close(); + }); + }); }); }); }); - _client.end(); - once = true; } }); }); @@ -197,19 +198,20 @@ module.exports = { var messages = 0; response.on('data', function(data){ ++messages; - var msg = decode(data); - if (msg[0].substr(0, 3) == '~h~'){ - assert.ok(messages == 2); - assert.ok(Object.keys(_socket.clients).length == 1); - setTimeout(function(){ - assert.ok(Object.keys(_socket.clients).length == 0); - client.end(); - _server.close(); - }, 150); - } + decode(data, function(msg, type){ + if (type == '2'){ + assert.ok(messages == 2); + assert.ok(Object.keys(_socket.clients).length == 1); + setTimeout(function(){ + assert.ok(Object.keys(_socket.clients).length == 0); + client.end(); + _server.close(); + }, 150); + } + }); }); }); }); } -}; \ No newline at end of file +}; From b98f9ceb07a199545122cfa164f3da696464aa7d Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 15 Nov 2010 02:34:53 -0300 Subject: [PATCH 15/36] XHR-Polling tests adapted to new message encoding/decoding --- tests/transports.xhr-polling.js | 68 +++++++++++++++++---------------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/tests/transports.xhr-polling.js b/tests/transports.xhr-polling.js index 2003ff3833..92a35f2e8f 100644 --- a/tests/transports.xhr-polling.js +++ b/tests/transports.xhr-polling.js @@ -2,13 +2,9 @@ var io = require('socket.io') , http = require('http') , querystring = require('querystring') , port = 7400 - , encode = require('socket.io/utils').encode - , decode = require('socket.io/utils').decode , Polling = require('socket.io/transports/xhr-polling'); -function server(callback){ - return http.createServer(function(){}); -}; +require('socket.io/tests'); function listen(s, callback){ s._port = port; @@ -71,19 +67,22 @@ module.exports = { listen(_server, function(){ get(client(_server), '/socket.io/xhr-polling', function(data){ - var sessid = decode(data); - assert.ok(Object.keys(_socket.clients).length == 1); - assert.ok(sessid == Object.keys(_socket.clients)[0]); - assert.ok(_socket.clients[sessid] instanceof Polling); - - _socket.clients[sessid].send('from server'); - - get(client(_server), '/socket.io/xhr-polling/' + sessid, function(data){ - assert.ok(decode(data), 'from server'); - --trips || _server.close(); + decode(data, function(sessid){ + assert.ok(Object.keys(_socket.clients).length == 1); + assert.ok(sessid == Object.keys(_socket.clients)[0]); + assert.ok(_socket.clients[sessid] instanceof Polling); + + _socket.clients[sessid].send('from server'); + + get(client(_server), '/socket.io/xhr-polling/' + sessid, function(data){ + decode(data, function(msg){ + assert.ok(msg[0] === 'from server'); + --trips || _server.close(); + }); + }); + + post(client(_server), '/socket.io/xhr-polling/' + sessid + '/send', {data: encode('from client')}); }); - - post(client(_server), '/socket.io/xhr-polling/' + sessid + '/send', {data: encode('from client')}); }); }); }, @@ -114,25 +113,28 @@ module.exports = { listen(_server, function(){ get(client(_server), '/socket.io/xhr-polling', function(data){ - var sessid = decode(data); - assert.ok(_socket.clients[sessid]._open === false); - assert.ok(_socket.clients[sessid].connected); - _socket.clients[sessid].send('from server'); - get(client(_server), '/socket.io/xhr-polling/' + sessid, function(data){ - var durationCheck; - assert.ok(decode(data) == 'from server'); - setTimeout(function(){ - assert.ok(_socket.clients[sessid]._open); - assert.ok(_socket.clients[sessid].connected); - durationCheck = true; - }, 100); - get(client(_server), '/socket.io/xhr-polling/' + sessid, function(){ - assert.ok(durationCheck); - _server.close(); + decode(data, function(sessid){ + assert.ok(_socket.clients[sessid]._open === false); + assert.ok(_socket.clients[sessid].connected); + _socket.clients[sessid].send('from server'); + get(client(_server), '/socket.io/xhr-polling/' + sessid, function(data){ + var durationCheck; + decode(data, function(msg){ + assert.ok(msg[0] === 'from server'); + setTimeout(function(){ + assert.ok(_socket.clients[sessid]._open); + assert.ok(_socket.clients[sessid].connected); + durationCheck = true; + }, 100); + get(client(_server), '/socket.io/xhr-polling/' + sessid, function(){ + assert.ok(durationCheck); + _server.close(); + }); + }); }); }); }); }); } -}; \ No newline at end of file +}; From eaf900edcc19d2d154ac87475631ae76af3ec652 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 15 Nov 2010 02:38:24 -0300 Subject: [PATCH 16/36] Added listen() helper to listener.js and transports.flashsocket.js tests Make sure to test for msg value (index 0) in listener.js decode() callbacks --- tests/listener.js | 13 ++++++++++--- tests/transports.flashsocket.js | 7 +++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/tests/listener.js b/tests/listener.js index 413d090cbc..10050009ac 100644 --- a/tests/listener.js +++ b/tests/listener.js @@ -7,6 +7,13 @@ var http = require('http') require('socket.io/tests'); +function listen(s, callback){ + s._port = port; + s.listen(port, callback); + port++; + return s; +}; + module.exports = { 'test serving static javascript client': function(assert){ @@ -88,7 +95,7 @@ module.exports = { _client1._first = true; } else { decode(ev.data, function(msg){ - assert.ok(msg === 'broadcasted msg'); + assert.ok(msg[0] === 'broadcasted msg'); --trips || close(); }); } @@ -98,9 +105,9 @@ module.exports = { _client2._first = true; } else { decode(ev.data, function(msg){ - assert.ok(msg === 'broadcasted msg'); + assert.ok(msg[0] === 'broadcasted msg'); --trips || close(); - }): + }); } }; }) diff --git a/tests/transports.flashsocket.js b/tests/transports.flashsocket.js index befaf208cd..47f2515f95 100644 --- a/tests/transports.flashsocket.js +++ b/tests/transports.flashsocket.js @@ -6,6 +6,13 @@ var io = require('socket.io') require('socket.io/tests'); +function listen(s, callback){ + s._port = port; + s.listen(port, callback); + port++; + return s; +}; + module.exports = { 'test xml policy added to connection': function(assert){ From c4314f3c79829b90f80d633c500f69ad6cd5653c Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 15 Nov 2010 02:42:59 -0300 Subject: [PATCH 17/36] Add the ability to pass custom annotations to encode() tests helper --- lib/socket.io/tests.js | 4 ++-- tests/transports.websocket.js | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/socket.io/tests.js b/lib/socket.io/tests.js index 887c4c95f2..af34b5c470 100644 --- a/lib/socket.io/tests.js +++ b/lib/socket.io/tests.js @@ -17,8 +17,8 @@ module.exports = { return io.listen(server, options); }, - encode: function(msg){ - var atts = {}; + encode: function(msg, atts){ + var atts = atts || {}; if (typeof msg == 'object') atts['j'] = null; msg = typeof msg == 'object' ? JSON.stringify(msg) : msg; return Encode(['1', encodeMessage(msg, atts)]); diff --git a/tests/transports.websocket.js b/tests/transports.websocket.js index 87ecff99f4..ef3d39a6d0 100644 --- a/tests/transports.websocket.js +++ b/tests/transports.websocket.js @@ -241,6 +241,9 @@ module.exports = { }); }); + }, + + 'test realms': function(){ } }; From 0c98a79e6c116e7911cf6958a4a0fa606ea57d20 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 15 Nov 2010 03:33:34 -0300 Subject: [PATCH 18/36] Started Changelog for 0.7 Deprecated .clientsIndex and clientConnect/clientDisconnect/clientMessage events Added annotations support to broadcast Added Realm class --- History.md | 13 ++++++++++++- example/server.js | 2 +- lib/socket.io/listener.js | 19 +++++++++++-------- tests/transports.websocket.js | 5 +---- 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/History.md b/History.md index 8eb0a6fc0b..265be5a129 100644 --- a/History.md +++ b/History.md @@ -124,4 +124,15 @@ http://www.lightsphere.com/dev/articles/flash_socket_policy.html - Flash at some point enables us to skip 843 checking altogether - Tests compatibility * Fixed connection timeout, noDelay and socket encoding for draft 76 (had been accidentally moved into the `else` block) -* Some stylistic fixes \ No newline at end of file +* Some stylistic fixes + +0.7.0 / + +* [DEPRECATE] onClientConnect / onClientMessage / onClientDisconnect events. Please use .on('connection', function(conn){ conn.on('message', function(){}); conn.on('disconnect', function(){}); }); instead +* [DEPRECATE} .clientsIndex accessor. Please use .clients intead +* Improved session id generation mechanism +* Implemented new message encoding mechanism (read more about it in the README) + - Implemented message types properly + - Implemented new message encoder/decoder with annotations support +* Added `tests.js` set of testing helpers +* Added `.json()` and `.broadcastJSON()` to send and brodcast messages in JSON. For just encoding objects as json you can continue to use `.send({ your: 'object' })`, but if you wish to force the JSON encoding of other types (like Number), use `.json` diff --git a/example/server.js b/example/server.js index bfd81973b2..70f31ba297 100644 --- a/example/server.js +++ b/example/server.js @@ -60,4 +60,4 @@ io.on('connection', function(client){ client.on('disconnect', function(){ client.broadcast({ announcement: client.sessionId + ' disconnected' }); }); -}); \ No newline at end of file +}); diff --git a/lib/socket.io/listener.js b/lib/socket.io/listener.js index 2a40b9aa31..238266e916 100644 --- a/lib/socket.io/listener.js +++ b/lib/socket.io/listener.js @@ -2,6 +2,7 @@ var url = require('url') , sys = require('sys') , fs = require('fs') , options = require('./utils').options + , Realm = require('./realm') , Client = require('./client') , clientVersion = require('./../../support/socket.io-client/lib/io').io.version , transports = { @@ -28,7 +29,7 @@ var Listener = module.exports = function(server, options){ if (!this.options.log) this.options.log = function(){}; - this.clients = this.clientsIndex = {}; + this.clients = {}; this._clientCount = 0; this._clientFiles = {}; @@ -58,11 +59,11 @@ var Listener = module.exports = function(server, options){ sys.inherits(Listener, process.EventEmitter); for (var i in options) Listener.prototype[i] = options[i]; -Listener.prototype.broadcast = function(message, except){ +Listener.prototype.broadcast = function(message, except, atts){ for (var i = 0, k = Object.keys(this.clients), l = k.length; i < l; i++){ if (!except || ((typeof except == 'number' || typeof except == 'string') && k[i] != except) || (Array.isArray(except) && except.indexOf(k[i]) == -1)){ - this.clients[k[i]].send(message); + this.clients[k[i]].send(message, atts); } } return this; @@ -146,6 +147,12 @@ Listener.prototype._serveClient = function(file, req, res){ return false; }; +Listener.prototype.realm = function(){ + if (!(realm in this._realms)) + this._realms[realm] = new Realm(realm, this); + return this._realms[realm]; +} + Listener.prototype._onClientConnect = function(client){ this.clients[client.sessionId] = client; this.options.log('Client '+ client.sessionId +' connected'); @@ -153,10 +160,6 @@ Listener.prototype._onClientConnect = function(client){ this.emit('connection', client); }; -Listener.prototype._onClientMessage = function(data, client){ - this.emit('clientMessage', data, client); -}; - Listener.prototype._onClientDisconnect = function(client){ delete this.clients[client.sessionId]; this.options.log('Client '+ client.sessionId +' disconnected'); @@ -166,4 +169,4 @@ Listener.prototype._onClientDisconnect = function(client){ Listener.prototype._onConnection = function(transport, req, res, httpUpgrade, head){ this.options.log('Initializing client with transport "'+ transport +'"'); new transports[transport](this, req, res, this.options.transportOptions[transport], head); -}; \ No newline at end of file +}; diff --git a/tests/transports.websocket.js b/tests/transports.websocket.js index ef3d39a6d0..d90ded1758 100644 --- a/tests/transports.websocket.js +++ b/tests/transports.websocket.js @@ -241,9 +241,6 @@ module.exports = { }); }); - }, - - 'test realms': function(){ } - + }; From 67996d82296039533e08a81b56daf3fd31404602 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 15 Nov 2010 03:35:41 -0300 Subject: [PATCH 19/36] Now really deprecated clientConnect/clientDisconnect --- lib/socket.io/listener.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/socket.io/listener.js b/lib/socket.io/listener.js index 238266e916..d8e1955b5c 100644 --- a/lib/socket.io/listener.js +++ b/lib/socket.io/listener.js @@ -156,14 +156,12 @@ Listener.prototype.realm = function(){ Listener.prototype._onClientConnect = function(client){ this.clients[client.sessionId] = client; this.options.log('Client '+ client.sessionId +' connected'); - this.emit('clientConnect', client); this.emit('connection', client); }; Listener.prototype._onClientDisconnect = function(client){ delete this.clients[client.sessionId]; this.options.log('Client '+ client.sessionId +' disconnected'); - this.emit('clientDisconnect', client); }; Listener.prototype._onConnection = function(transport, req, res, httpUpgrade, head){ From 8b3f05339f6c9344143a7285154f915cbae0e506 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 15 Nov 2010 03:53:01 -0300 Subject: [PATCH 20/36] Added Realm --- lib/socket.io/realm.js | 63 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 lib/socket.io/realm.js diff --git a/lib/socket.io/realm.js b/lib/socket.io/realm.js new file mode 100644 index 0000000000..7ceb7fd4e0 --- /dev/null +++ b/lib/socket.io/realm.js @@ -0,0 +1,63 @@ +/** + * Pseudo-listener constructor + * + * @param {String} realm name + * @param {Listener} listener the realm belongs to + * @api public + */ + +function Realm(name, listener){ + this.name = name; + this.listener = listener; +} + +/** + * Override connection event so that client.send() appends the realm annotation + * + * @param {String} ev name + * @param {Function} callback + * @api public + */ + +Realm.prototype.on = function(ev, fn){ + var name = this.name; + if (ev == 'connection') + this.listener.on('connection', function(conn){ + // Monkey-patch conn#send + var oldSend = conn.send; + conn.send = function(msg, atts){ + atts = atts || {}; + atts['r'] = name; + return oldSend.call(conn, msg, atts); + }; + }); + else + this.listener.on(ev, fn); + return this; +}; + +/** + * Broadcast a message annotated for this realm + * + * @param {String} message + * @param {Array/String} except + * @param {Object} message annotations + * @api public + */ + +Realm.prototype.broadcast = function(message, except, atts){ + atts = atts || {}; + atts['r'] = this.name; + this.listener.broadcast(message, except, atts); + return this; +}; + +/** + * List of properties to proxy to the listener + */ + +['clients', 'options', 'server'].forEach(function(p){ + Realm.prototype.__defineGetter__(p, function(){ + return this.listener[p]; + }); +}); From 0ecb583f704589b1509e03ceef886a08a1d20d66 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 15 Nov 2010 04:00:28 -0300 Subject: [PATCH 21/36] Added broadcastJSON method --- lib/socket.io/listener.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/socket.io/listener.js b/lib/socket.io/listener.js index d8e1955b5c..813803695b 100644 --- a/lib/socket.io/listener.js +++ b/lib/socket.io/listener.js @@ -69,6 +69,12 @@ Listener.prototype.broadcast = function(message, except, atts){ return this; }; +Listener.prototype.broadcastJSON = function(message, except, atts){ + atts = atts || {}; + atts['j'] = null; + return this.broadcast(JSON.stringify(message), except, atts); +}; + Listener.prototype.check = function(req, res, httpUpgrade, head){ var path = url.parse(req.url).pathname, parts, cn; if (path && path.indexOf('/' + this.options.resource) === 0){ From 3975761fbe7be2b11ed69b7ce111f138eb96a389 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 15 Nov 2010 04:22:29 -0300 Subject: [PATCH 22/36] Added message handler override for realm listeners Added Client#sendJSON method --- lib/socket.io/client.js | 12 +++++++++--- lib/socket.io/realm.js | 18 ++++++++++++++++-- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/lib/socket.io/client.js b/lib/socket.io/client.js index c39b8f74ab..9634a55c95 100644 --- a/lib/socket.io/client.js +++ b/lib/socket.io/client.js @@ -32,7 +32,13 @@ Client.prototype.send = function(message, anns){ anns = anns || {}; if (typeof message == 'object') anns['j'] = null; message = typeof message == 'object' ? JSON.stringify(message) : message; - this.write('1', encodeMessage(message, anns)); + return this.write('1', encodeMessage(message, anns)); +}; + +Client.prototype.sendJSON = function(message, anns){ + anns = anns || {}; + anns['j'] = null; + return this.send(JSON.stringify(message), anns); }; Client.prototype.write = function(type, data){ @@ -40,9 +46,9 @@ Client.prototype.write = function(type, data){ return this._write(encode([type, data])); } -Client.prototype.broadcast = function(message){ +Client.prototype.broadcast = function(message, anns){ if (!('sessionId' in this)) return this; - this.listener.broadcast(message, this.sessionId); + this.listener.broadcast(message, this.sessionId, anns); return this; }; diff --git a/lib/socket.io/realm.js b/lib/socket.io/realm.js index 7ceb7fd4e0..3547f9a123 100644 --- a/lib/socket.io/realm.js +++ b/lib/socket.io/realm.js @@ -20,16 +20,30 @@ function Realm(name, listener){ */ Realm.prototype.on = function(ev, fn){ - var name = this.name; + var self = this; if (ev == 'connection') this.listener.on('connection', function(conn){ // Monkey-patch conn#send var oldSend = conn.send; conn.send = function(msg, atts){ atts = atts || {}; - atts['r'] = name; + atts['r'] = self.name; return oldSend.call(conn, msg, atts); }; + + // Make sure to only relay messages with the realm annotation + var oldOn = conn.on; + conn.on = function(ev, fn){ + if (ev == 'message') + return oldOn.call(this, ev, function(msg, atts){ + if (atts && 'r' in atts && atts.r == self.name) + fn.call(conn, msg, atts); + }); + else + return oldOn.call(this, ev, fn); + }; + + fn.call(self, conn); }); else this.listener.on(ev, fn); From b3478d1115a2ab5bece97f81551c42a470a4f0e6 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 15 Nov 2010 04:41:21 -0300 Subject: [PATCH 23/36] Removed unnecessary checks for attributes argument in Realm#on Make sure to proxy the `realm` method from the Realm to the Listener --- lib/socket.io/realm.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/socket.io/realm.js b/lib/socket.io/realm.js index 3547f9a123..9d712be018 100644 --- a/lib/socket.io/realm.js +++ b/lib/socket.io/realm.js @@ -36,7 +36,7 @@ Realm.prototype.on = function(ev, fn){ conn.on = function(ev, fn){ if (ev == 'message') return oldOn.call(this, ev, function(msg, atts){ - if (atts && 'r' in atts && atts.r == self.name) + if (atts.r == self.name) fn.call(conn, msg, atts); }); else @@ -75,3 +75,13 @@ Realm.prototype.broadcast = function(message, except, atts){ return this.listener[p]; }); }); + +/** + * List of methods to proxy to the listener + */ + +['realm'].forEach(function(m){ + Realm.prototype[m] = function(){ + return this.listener[m].apply(this.listener, arguments); + }; +}); From b1775316226b73b105708d53f94002c4854aacc7 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 15 Nov 2010 08:50:36 -0300 Subject: [PATCH 24/36] Realms test passing Removed monkey-patching in favor of RealmClient class. Prevents race conditions RealmClient Client override added for Realm Fixed Listener#realm Various other fixes --- lib/socket.io/listener.js | 3 +- lib/socket.io/realm.js | 112 +++++++++++++++++++++++++++++++------- tests/listener.js | 68 ++++++++++++++++++++++- 3 files changed, 160 insertions(+), 23 deletions(-) diff --git a/lib/socket.io/listener.js b/lib/socket.io/listener.js index 813803695b..2c13bdf8fd 100644 --- a/lib/socket.io/listener.js +++ b/lib/socket.io/listener.js @@ -32,6 +32,7 @@ var Listener = module.exports = function(server, options){ this.clients = {}; this._clientCount = 0; this._clientFiles = {}; + this._realms = {}; var listeners = this.server.listeners('request'); this.server.removeAllListeners('request'); @@ -153,7 +154,7 @@ Listener.prototype._serveClient = function(file, req, res){ return false; }; -Listener.prototype.realm = function(){ +Listener.prototype.realm = function(realm){ if (!(realm in this._realms)) this._realms[realm] = new Realm(realm, this); return this._realms[realm]; diff --git a/lib/socket.io/realm.js b/lib/socket.io/realm.js index 9d712be018..4bd6ae3b47 100644 --- a/lib/socket.io/realm.js +++ b/lib/socket.io/realm.js @@ -23,27 +23,7 @@ Realm.prototype.on = function(ev, fn){ var self = this; if (ev == 'connection') this.listener.on('connection', function(conn){ - // Monkey-patch conn#send - var oldSend = conn.send; - conn.send = function(msg, atts){ - atts = atts || {}; - atts['r'] = self.name; - return oldSend.call(conn, msg, atts); - }; - - // Make sure to only relay messages with the realm annotation - var oldOn = conn.on; - conn.on = function(ev, fn){ - if (ev == 'message') - return oldOn.call(this, ev, function(msg, atts){ - if (atts.r == self.name) - fn.call(conn, msg, atts); - }); - else - return oldOn.call(this, ev, fn); - }; - - fn.call(self, conn); + fn.call(self, new RealmClient(self.name, conn)); }); else this.listener.on(ev, fn); @@ -85,3 +65,93 @@ Realm.prototype.broadcast = function(message, except, atts){ return this.listener[m].apply(this.listener, arguments); }; }); + +/** + * Pseudo-client constructor + * + * @param {Client} Actual client + * @api public + */ + +function RealmClient(name, client){ + this.name = name; + this.client = client; +}; + +/** + * Override Client#on to filter messages from our realm + * + * @param {String} ev name + * @param {Function) callback + */ + +RealmClient.prototype.on = function(ev, fn){ + var self = this; + if (ev == 'message') + this.client.on('message', function(msg, atts){ + if (atts.r == self.name) + fn.call(self, msg, atts); + }); + else + this.client.on(ev, fn); + return this; +}; + +/** + * Client#send wrapper with realm annotations + * + * @param {String} message + * @param {Object} annotations + * @apu public + */ + +RealmClient.prototype.send = function(message, anns){ + anns = anns || {}; + anns['r'] = this.name; + return this.client.send(message, anns); +}; + +/** + * Client#send wrapper with realm annotations + * + * @param {String} message + * @param {Object} annotations + * @apu public + */ + +RealmClient.prototype.sendJSON = function(message, anns){ + anns = anns || {}; + anns['r'] = this.name; + return this.client.sendJSON(message, anns); +}; + +/** + * Client#send wrapper with realm annotations + * + * @param {String} message + * @param {Object} annotations + * @apu public + */ + +RealmClient.prototype.broadcast = function(message, anns){ + anns = anns || {}; + anns['r'] = this.name; + return this.client.broadcast(message, anns); +}; + +/** + * Proxy some properties to the client + */ + +['connected', 'options', 'connections', 'listener'].forEach(function(p){ + RealmClient.prototype.__defineGetter__(p, function(){ + return this.client[p]; + }); +}); + +/** + * Module exports + */ + +module.exports = Realm; +module.exports.Client = RealmClient; diff --git a/tests/listener.js b/tests/listener.js index 10050009ac..eb645a8f59 100644 --- a/tests/listener.js +++ b/tests/listener.js @@ -137,6 +137,72 @@ module.exports = { , _socket = socket(_server); assert.response(_server, { url: '/socket.io/inexistent' }, { body: 'All cool' }); + }, + + 'test realms': function(assert){ + var _server = server() + , _socket = socket(_server) + , globalMessages = 0 + , messages = 2; + + listen(_server, function(){ + _socket.on('connection', function(conn){ + conn.on('message', function(msg){ + globalMessages++; + if (globalMessages == 1) + assert.ok(msg == 'for first realm'); + if (globalMessages == 2) + assert.ok(msg == 'for second realm'); + }); + }); + + var realm1 = _socket.realm('first-realm') + , realm2 = _socket.realm('second-realm'); + + realm1.on('connection', function(conn){ + conn.on('message', function(msg){ + assert.ok(msg == 'for first realm'); + --messages || close(); + }); + }); + + realm2.on('connection', function(conn){ + conn.on('message', function(msg){ + assert.ok(msg == 'for second realm'); + --messages || close(); + }); + }); + + var _client1 = client(_server) + , _client2; + + _client1.onopen = function(){ + var once = false; + _client1.onmessage = function(){ + if (!once){ + once = true; + _client1.send(encode('for first realm', {r: 'first-realm'})); + + _client2 = client(_server) + _client2.onopen = function(){ + var once = false; + _client2.onmessage = function(){ + if (!once){ + once = true; + _client2.send(encode('for second realm', {r: 'second-realm'})); + } + }; + }; + } + }; + }; + + function close(){ + _client1.close(); + _client2.close(); + _server.close(); + } + }); } - + }; From 387199e73e047eac5d27decb47bcbc6c0e2a6124 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 15 Nov 2010 09:17:57 -0300 Subject: [PATCH 25/36] Added `ignoreEmptyOrigin` option to Client (pending documentation) Fixed syntax style in JSONP Polling transport --- lib/socket.io/client.js | 4 +++- lib/socket.io/transports/jsonp-polling.js | 14 ++++++++------ tests/transports.jsonp-polling.js | 5 +++-- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/lib/socket.io/client.js b/lib/socket.io/client.js index 9634a55c95..02ad33a1ba 100644 --- a/lib/socket.io/client.js +++ b/lib/socket.io/client.js @@ -12,6 +12,7 @@ var Client = module.exports = function(listener, req, res, options, head){ process.EventEmitter.call(this); this.listener = listener; this.options(merge({ + ignoreEmptyOrigin: true, timeout: 8000, heartbeatInterval: 10000, closeTimeout: 0 @@ -193,6 +194,7 @@ Client.prototype._generateSessionId = function(){ Client.prototype._verifyOrigin = function(origin){ var origins = this.listener.options.origins; + if (origins.indexOf('*:*') !== -1) return true; @@ -205,7 +207,7 @@ Client.prototype._verifyOrigin = function(origin){ } catch (ex) {} } - return false; + return this.options.ignoreEmptyOrigin; }; for (var i in options) Client.prototype[i] = options[i]; diff --git a/lib/socket.io/transports/jsonp-polling.js b/lib/socket.io/transports/jsonp-polling.js index 502a9613a0..1673a8be46 100644 --- a/lib/socket.io/transports/jsonp-polling.js +++ b/lib/socket.io/transports/jsonp-polling.js @@ -21,14 +21,16 @@ JSONPPolling.prototype._onConnect = function(req, res){ JSONPPolling.prototype._write = function(message){ if (this._open){ - if (this.request.headers.origin && !this._verifyOrigin(this.request.headers.origin)){ - message = "alert('Cross domain security restrictions not met');"; - } else { + if (this._verifyOrigin(this.request.headers.origin)) message = "io.JSONP["+ this._index +"]._("+ JSON.stringify(message) +");"; - } - this.response.writeHead(200, {'Content-Type': 'text/javascript; charset=UTF-8', 'Content-Length': Buffer.byteLength(message)}); + else + message = "alert('Cross domain security restrictions not met');"; + this.response.writeHead(200, { + 'Content-Type': 'text/javascript; charset=UTF-8', + 'Content-Length': Buffer.byteLength(message) + }); this.response.write(message); this.response.end(); this._onClose(); } -}; \ No newline at end of file +}; diff --git a/tests/transports.jsonp-polling.js b/tests/transports.jsonp-polling.js index 99f79bcb67..83a226161a 100644 --- a/tests/transports.jsonp-polling.js +++ b/tests/transports.jsonp-polling.js @@ -44,8 +44,9 @@ function socket(server, options){ return io.listen(server, options); }; -function get(client, url, callback){ - var request = client.request('GET', url + '/' + (+new Date) + '/0', {host: 'localhost'}); +function get(client, url, callback, headers){ + var headers = {host: 'localhost'} + , request = client.request('GET', url + '/' + (+new Date) + '/0'); request.end(); request.on('response', function(response){ var data = ''; From c2593b2e56503f508eb72fca0617d75f6de5c73a Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 15 Nov 2010 09:23:21 -0300 Subject: [PATCH 26/36] Added origin parameter for jsonp-polling tests get() utility Fixed flashsocket syntax for 85 columns --- lib/socket.io/transports/flashsocket.js | 12 +++++++----- tests/transports.jsonp-polling.js | 6 +++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/socket.io/transports/flashsocket.js b/lib/socket.io/transports/flashsocket.js index 75515ea6b4..d670bcd62e 100644 --- a/lib/socket.io/transports/flashsocket.js +++ b/lib/socket.io/transports/flashsocket.js @@ -41,9 +41,10 @@ Flashsocket.init = function(listener){ netserver.listen(843); } catch(e){ if (e.errno == 13) - listener.options.log('Your node instance does not have root privileges. This means that the flash XML' - + ' policy file will be served inline instead of on port 843. This might slow down' - + ' initial connections slightly.'); + listener.options.log('Your node instance does not have root privileges.' + + ' This means that the flash XML policy file will be' + + ' served inline instead of on port 843. This will slow' + + ' connection time slightly'); netserver = null; } } @@ -71,7 +72,8 @@ Flashsocket.init = function(listener){ function policy(listeners) { var xml = '\n\n\n'; + + ' "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">' + + '\n\n'; listeners.forEach(function(l){ [].concat(l.options.origins).forEach(function(origin){ @@ -82,4 +84,4 @@ function policy(listeners) { xml += '\n'; return xml; -}; \ No newline at end of file +}; diff --git a/tests/transports.jsonp-polling.js b/tests/transports.jsonp-polling.js index 83a226161a..d2ceb5e54e 100644 --- a/tests/transports.jsonp-polling.js +++ b/tests/transports.jsonp-polling.js @@ -44,9 +44,9 @@ function socket(server, options){ return io.listen(server, options); }; -function get(client, url, callback, headers){ - var headers = {host: 'localhost'} - , request = client.request('GET', url + '/' + (+new Date) + '/0'); +function get(client, url, callback, origin){ + var headers = {host: 'localhost', origin: origin} + , request = client.request('GET', url + '/' + (+new Date) + '/0', headers); request.end(); request.on('response', function(response){ var data = ''; From a31680d22e899f313b59441d608bc5a4d9b85856 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 15 Nov 2010 09:24:22 -0300 Subject: [PATCH 27/36] More OCD-related work for 85 columns limit --- lib/socket.io/transports/flashsocket.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/socket.io/transports/flashsocket.js b/lib/socket.io/transports/flashsocket.js index d670bcd62e..522c0e5c0c 100644 --- a/lib/socket.io/transports/flashsocket.js +++ b/lib/socket.io/transports/flashsocket.js @@ -77,8 +77,8 @@ function policy(listeners) { listeners.forEach(function(l){ [].concat(l.options.origins).forEach(function(origin){ - var parts = origin.split(':'); - xml += '\n'; + var p = origin.split(':'); + xml += '\n'; }); }); From aa9cb21d4c0f2322aa271bd43df61a31bae6cc71 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 15 Nov 2010 09:52:45 -0300 Subject: [PATCH 28/36] Added Origin verification for HTMLFile transport --- lib/socket.io/transports/htmlfile.js | 13 +++++++++---- lib/socket.io/transports/jsonp-polling.js | 4 ++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/socket.io/transports/htmlfile.js b/lib/socket.io/transports/htmlfile.js index a6d9d79906..4365047309 100644 --- a/lib/socket.io/transports/htmlfile.js +++ b/lib/socket.io/transports/htmlfile.js @@ -36,11 +36,16 @@ HTMLFile.prototype._onConnect = function(req, res){ res.write('ok'); res.end(); }); - break; } }; -HTMLFile.prototype._write = function(message){ - if (this._open) - this.response.write(''); //json for escaping +HTMLFile.prototype._write = function(msg){ + if (this._open){ + if (this._verifyOrigin(this.request.headers.origin)) + // we leverage json for escaping + msg = ''; + else + msg = ""; + this.response.write(msg); + } }; diff --git a/lib/socket.io/transports/jsonp-polling.js b/lib/socket.io/transports/jsonp-polling.js index 1673a8be46..ef7d8c7a33 100644 --- a/lib/socket.io/transports/jsonp-polling.js +++ b/lib/socket.io/transports/jsonp-polling.js @@ -22,9 +22,9 @@ JSONPPolling.prototype._onConnect = function(req, res){ JSONPPolling.prototype._write = function(message){ if (this._open){ if (this._verifyOrigin(this.request.headers.origin)) - message = "io.JSONP["+ this._index +"]._("+ JSON.stringify(message) +");"; + message = "io.JSONP["+ this._index +"]._("+ JSON.stringify(message) +")"; else - message = "alert('Cross domain security restrictions not met');"; + message = "alert('Cross domain security restrictions not met')"; this.response.writeHead(200, { 'Content-Type': 'text/javascript; charset=UTF-8', 'Content-Length': Buffer.byteLength(message) From 928ecddbc04f50fe96cab1401fa75f45b53c7571 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 15 Nov 2010 09:57:08 -0300 Subject: [PATCH 29/36] Make sure to call verifyOrigin even if Origin header is not sent / is falsy --- lib/socket.io/transports/xhr-multipart.js | 2 +- lib/socket.io/transports/xhr-polling.js | 18 ++++++++---------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/lib/socket.io/transports/xhr-multipart.js b/lib/socket.io/transports/xhr-multipart.js index dd45735613..88d6dd7cca 100644 --- a/lib/socket.io/transports/xhr-multipart.js +++ b/lib/socket.io/transports/xhr-multipart.js @@ -10,7 +10,7 @@ require('sys').inherits(Multipart, Client); Multipart.prototype._onConnect = function(req, res){ var self = this, body = '', headers = {}; // https://developer.mozilla.org/En/HTTP_Access_Control - if (req.headers.origin && this._verifyOrigin(req.headers.origin)){ + if (this._verifyOrigin(req.headers.origin)){ headers['Access-Control-Allow-Origin'] = '*'; headers['Access-Control-Allow-Credentials'] = 'true'; } diff --git a/lib/socket.io/transports/xhr-polling.js b/lib/socket.io/transports/xhr-polling.js index f8c2b3fa23..f39207239c 100644 --- a/lib/socket.io/transports/xhr-polling.js +++ b/lib/socket.io/transports/xhr-polling.js @@ -33,16 +33,14 @@ Polling.prototype._onConnect = function(req, res){ }); req.addListener('end', function(){ var headers = {'Content-Type': 'text/plain'}; - if (req.headers.origin){ - if (self._verifyOrigin(req.headers.origin)){ - headers['Access-Control-Allow-Origin'] = '*'; - if (req.headers.cookie) headers['Access-Control-Allow-Credentials'] = 'true'; - } else { - res.writeHead(401); - res.write('unauthorized'); - res.end(); - return; - } + if (self._verifyOrigin(req.headers.origin)){ + headers['Access-Control-Allow-Origin'] = '*'; + if (req.headers.cookie) headers['Access-Control-Allow-Credentials'] = 'true'; + } else { + res.writeHead(401); + res.write('unauthorized'); + res.end(); + return; } try { // optimization: just strip first 5 characters here? From e72924652694be60e2fdf7a57e261067a3491067 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 15 Nov 2010 12:42:09 -0300 Subject: [PATCH 30/36] Added domain mismatch test --- lib/socket.io/client.js | 6 ++--- lib/socket.io/transports/jsonp-polling.js | 2 +- tests/transports.jsonp-polling.js | 28 +++++++++++++++++++++-- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/lib/socket.io/client.js b/lib/socket.io/client.js index 02ad33a1ba..56456d3218 100644 --- a/lib/socket.io/client.js +++ b/lib/socket.io/client.js @@ -201,9 +201,9 @@ Client.prototype._verifyOrigin = function(origin){ if (origin){ try { var parts = urlparse(origin); - return origins.indexOf(parts.host + ':' + parts.port) !== -1 || - origins.indexOf(parts.host + ':*') !== -1 || - origins.indexOf('*:' + parts.port) !== -1; + return origins.indexOf(parts.host + ':' + parts.port) !== -1 + || origins.indexOf(parts.host + ':*') !== -1 + || origins.indexOf('*:' + parts.port) !== -1; } catch (ex) {} } diff --git a/lib/socket.io/transports/jsonp-polling.js b/lib/socket.io/transports/jsonp-polling.js index ef7d8c7a33..09c2080787 100644 --- a/lib/socket.io/transports/jsonp-polling.js +++ b/lib/socket.io/transports/jsonp-polling.js @@ -5,7 +5,7 @@ JSONPPolling = module.exports = function(){ }; require('sys').inherits(JSONPPolling, XHRPolling); - + JSONPPolling.prototype.getOptions = function(){ return { timeout: null, // no heartbeats diff --git a/tests/transports.jsonp-polling.js b/tests/transports.jsonp-polling.js index d2ceb5e54e..3eee18b419 100644 --- a/tests/transports.jsonp-polling.js +++ b/tests/transports.jsonp-polling.js @@ -6,7 +6,7 @@ var io = require('socket.io') require('socket.io/tests'); -function jsonp_decode(data, fn){ +function jsonp_decode(data, fn, alert){ var io = { JSONP: [{ '_': function(msg){ @@ -45,7 +45,7 @@ function socket(server, options){ }; function get(client, url, callback, origin){ - var headers = {host: 'localhost', origin: origin} + var headers = {host: 'localhost', origin: origin || ''} , request = client.request('GET', url + '/' + (+new Date) + '/0', headers); request.end(); request.on('response', function(response){ @@ -153,6 +153,30 @@ module.exports = { }); }); }); + }, + + 'test origin through domain mismatch': function(assert){ + var _server = server() + , _socket = socket(_server, { + origins: 'localhost:*' + }); + + listen(_server, function(){ + get(client(_server), '/socket.io/jsonp-polling/', + function(data){ + jsonp_decode(data, + function(){ + assert.ok(false); + }, + function(msg){ + assert.ok(/security/i.test(msg)); + _server.close(); + } + ); + }, + 'test.localhost' + ); + }); } }; From 926868f786c1659ae07c83460add674036a97771 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 15 Nov 2010 12:54:58 -0300 Subject: [PATCH 31/36] Test for disallowance of connection because of empty origin --- tests/transports.jsonp-polling.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/transports.jsonp-polling.js b/tests/transports.jsonp-polling.js index 3eee18b419..121f5abe72 100644 --- a/tests/transports.jsonp-polling.js +++ b/tests/transports.jsonp-polling.js @@ -177,6 +177,36 @@ module.exports = { 'test.localhost' ); }); + }, + + 'test disallowance because of empty origin': function(assert){ + var _server = server() + , _socket = socket(_server, { + origins: 'localhost:*', + transportOptions: { + 'jsonp-polling': { + ignoreEmptyOrigin: false, + closeTimeout: 100 + } + } + }); + + listen(_server, function(){ + get(client(_server), '/socket.io/jsonp-polling/', + function(data){ + jsonp_decode(data, + function(){ + assert.ok(false); + }, + function(msg){ + assert.ok(/security/i.test(msg)); + _server.close(); + } + ); + }, + '' + ); + }); } }; From 1594fd782cdec833483b55ed969a7a23eb95bf3f Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 16 Nov 2010 04:24:12 -0300 Subject: [PATCH 32/36] Faster / more efficient WebSocket streaming parser --- example/chat.html | 4 +- lib/socket.io/client.js | 6 ++- lib/socket.io/transports/websocket.js | 62 ++++++++++++++++++--------- 3 files changed, 47 insertions(+), 25 deletions(-) diff --git a/example/chat.html b/example/chat.html index 2c4b8b6831..2ab97f1ae7 100644 --- a/example/chat.html +++ b/example/chat.html @@ -25,7 +25,7 @@ } function esc(msg){ - return msg.replace(//g, '>'); + return String(msg).replace(//g, '>'); }; var socket = new io.Socket(null, {port: 8080, rememberTransport: false}); @@ -58,4 +58,4 @@

Sample chat client

- \ No newline at end of file + diff --git a/lib/socket.io/client.js b/lib/socket.io/client.js index 56456d3218..1bcd25fa18 100644 --- a/lib/socket.io/client.js +++ b/lib/socket.io/client.js @@ -31,8 +31,10 @@ require('sys').inherits(Client, process.EventEmitter); Client.prototype.send = function(message, anns){ anns = anns || {}; - if (typeof message == 'object') anns['j'] = null; - message = typeof message == 'object' ? JSON.stringify(message) : message; + if (typeof message == 'object'){ + anns['j'] = null; + message = JSON.stringify(message); + } return this.write('1', encodeMessage(message, anns)); }; diff --git a/lib/socket.io/transports/websocket.js b/lib/socket.io/transports/websocket.js index 637f2cf72e..dcf02d7262 100644 --- a/lib/socket.io/transports/websocket.js +++ b/lib/socket.io/transports/websocket.js @@ -1,7 +1,8 @@ var Client = require('../client') , Stream = require('net').Stream , url = require('url') - , crypto = require('crypto'); + , crypto = require('crypto') + , EventEmitter = require('events').EventEmitter; WebSocket = module.exports = function(){ Client.apply(this, arguments); @@ -13,10 +14,16 @@ WebSocket.prototype._onConnect = function(req, socket){ var self = this , headers = []; + if (!req.connection.setTimeout){ + req.connection.end(); + return false; + } + + this.parser = new Parser(); + this.parser.on('data', self._onData.bind(this)); + Client.prototype._onConnect.call(this, req); - this.data = ''; - if (this.request.headers.upgrade !== 'WebSocket' || !this._verifyOrigin(this.request.headers.origin)){ this.listener.options.log('WebSocket connection invalid or Origin not verified'); this._onClose(); @@ -60,29 +67,12 @@ WebSocket.prototype._onConnect = function(req, socket){ this.connection.setEncoding('utf-8'); this.connection.addListener('data', function(data){ - self._handle(data); + self.parser.add(data); }); if (this._proveReception(headers)) this._payload(); }; -WebSocket.prototype._handle = function(data){ - var chunk, chunks, chunk_count; - this.data += data; - chunks = this.data.split('\ufffd'); - chunk_count = chunks.length - 1; - for (var i = 0; i < chunk_count; i++){ - chunk = chunks[i]; - if (chunk[0] !== '\u0000'){ - this.listener.options.log('Data incorrectly framed by UA. Dropping connection'); - this._onClose(); - return false; - } - this._onData(chunk.slice(1)); - } - this.data = chunks[chunks.length - 1]; -}; - // http://www.whatwg.org/specs/web-apps/current-work/complete/network.html#opening-handshake WebSocket.prototype._proveReception = function(headers){ var self = this @@ -134,3 +124,33 @@ WebSocket.prototype._write = function(message){ }; WebSocket.httpUpgrade = true; + +function Parser(){ + this.buffer = ''; + this.i = 0; +}; + +Parser.prototype.__proto__ = EventEmitter.prototype; + +Parser.prototype.add = function(data){ + this.buffer += data; + this.parse(); +}; + +Parser.prototype.parse = function(){ + for (var i = this.i, chr, l = this.buffer.length; i < l; i++){ + chr = this.buffer[i]; + if (i === 0){ + if (chr != '\u0000') + this.error('Bad framing. Expected null byte as first frame'); + else + continue; + } + if (chr == '\ufffd'){ + this.emit('data', this.buffer.substr(1, this.buffer.length - 2)); + this.buffer = this.buffer.substr(i + 1); + this.i = 0; + return this.parse(); + } + } +}; From 9d19ae6750278c75c8a14ab1b983a3b0a2c7f85d Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 16 Nov 2010 04:53:00 -0300 Subject: [PATCH 33/36] Updated client --- support/socket.io-client/.gitignore | 3 +- support/socket.io-client/README.md | 70 ++- support/socket.io-client/bin/build | 3 +- support/socket.io-client/lib/data.js | 232 ++++++++++ support/socket.io-client/lib/io.js | 4 +- support/socket.io-client/lib/socket.js | 40 +- support/socket.io-client/lib/transport.js | 104 ++--- .../lib/transports/jsonp-polling.js | 11 +- .../lib/transports/websocket.js | 8 +- .../socket.io-client/lib/transports/xhr.js | 15 +- support/socket.io-client/socket.io.js | 411 ++++++++++++++---- 11 files changed, 719 insertions(+), 182 deletions(-) create mode 100644 support/socket.io-client/lib/data.js diff --git a/support/socket.io-client/.gitignore b/support/socket.io-client/.gitignore index c8da893b2d..51d7b37a11 100644 --- a/support/socket.io-client/.gitignore +++ b/support/socket.io-client/.gitignore @@ -1 +1,2 @@ -s3 \ No newline at end of file +s3 +*.swp diff --git a/support/socket.io-client/README.md b/support/socket.io-client/README.md index 79c849830d..7dc318539c 100644 --- a/support/socket.io-client/README.md +++ b/support/socket.io-client/README.md @@ -183,6 +183,74 @@ Events: * Added io.util.ios which reports if the UA is running on iPhone or iPad * No more loading bar on iPhone: XHR-Polling now connects `onload` for the iOS WebKit, and waits 10 ms to launch the initial connection. +2010 11 01 - **0.6.0** + +* Make sure to only destroy if the _iframe was created +* Removed flashsocket onClose logic since its handled by connectTimeout +* Added socket checks when disconnecting / sending messages +* Fixed semicolons (thanks SINPacifist) +* Added io.util.merge for options merging. Thanks SINPacifist +* Removed unnecessary onClose handling, since this is taken care by Socket (thanks SINPacifist) +* Make sure not to try other transports if the socket.io cookie was there +* Updated web-socket-js +* Make sure not to abort the for loop when skipping the transport +* Connect timeout (fixes #34) +* Try different transports upon connect timeout (fixes #35) +* Restored rememberTransport to default +* Removed io.setPath check +* Make sure IE7 doesn't err on the multipart feature detection. Thanks Davin Lunz +* CORS feature detection. Fixes IE7 attempting cross domain requests through their incomplete XMLHttpRequest implementation. +* Now altering WEB_SOCKET_SWF_LOCATION (this way we don't need the web-socket-js WebSocket object to be there) +* Flashsocket .connect() and .send() call addTask. +* Make sure flashsocket can only be loaded on browsers that don't have a native websocket +* Leveraging __addTask to delay sent messages until WebSocket through SWF is fully loaded. +* Removed __isFlashLite check +* Leverage node.js serving of the client side files +* Make sure we can load io.js from node (window check) +* Fix for XDomain send() on IE8 (thanks Eric Zhang) +* Added a note about cross domain .swf files +* Made sure no errors where thrown in IE if there isn't a flash fallback available. +* Make sure disconnect event is only fired if the socket was completely connected, and it's not a reconnection attempt that was interrupted. +* Force disconnection if .connect() is called and a connection attempt is ongoing +* Upon socket disconnection, also mark `connecting` as false +* .connecting flag in transport instance +* Make sure .connecting is cleared in transport +* Correct sessionid checking +* Clear sessionid upon disconnection +* Remove _xhr and _sendXhr objects +* Moved timeout default into Transport +* Remove callbacks on _onDisconnect and call abort() +* Added placeholder for direct disconnect in XHR +* Timeout logic (fixes #31) +* Don't check for data length to trigger _onData, since most transports are not doing it +* Set timeout defaults based on heartbeat interval and polling duration (since we dont do heartbeats for polling) +* Check for msgs.length _onData +* Removed unused client option (heartbeatInterval) +* Added onDisconnect call if long poll is interrupted +* Polling calls _get directly as opposed to connect() +* Disconnection handling upon failure to send a message through xhr-* transports. +* Clean up internal xhr buffer upon disconnection +* Clean up general buffer in Socket upon disconnection +* Mark socket as disconnected +* Opera 10 support +* Fix for .fire on IE being called without arguments (fixes #28) +* JSONP polling transport +* Android compatibility. +* Automatic JSON decoding support +* Automatic JSON encoding support for objects +* Adding test for android for delaying the connection (fixes spinner) +* Fixing a few dangerous loops that otherwise loop into properties that have been added to the prototype elsewhere. +* Support for initializing io.Socket after the page has been loaded + +2010 11 ?? - **0.7.0** + +* Fixed, improved and added missing Transport#disconnect methods +* Implemented data.js (data and message encoding and decoding with buffering) + - Fixes edge cases with multipart not sending the entirety of a message and + firing the data event +* Implemented forced disconnect call from server +* Added warning if JSON.parse is not available and a JSON message is received + ### Credits Guillermo Rauch <guillermo@learnboost.com> @@ -210,4 +278,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 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. \ No newline at end of file +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/support/socket.io-client/bin/build b/support/socket.io-client/bin/build index f6e3d2b882..33b688424e 100755 --- a/support/socket.io-client/bin/build +++ b/support/socket.io-client/bin/build @@ -19,6 +19,7 @@ var fs = require('fs'), files = [ 'io.js', 'util.js', + 'data.js', 'transport.js', 'transports/xhr.js', 'transports/websocket.js', @@ -47,4 +48,4 @@ sys.log('Generating…'); fs.write(fs.openSync(__dirname + '/../socket.io.js', 'w'), content, 0, 'utf8'); sys.log(' + ' + __dirname + '/../socket.io.js'); -sys.log('All done!'); \ No newline at end of file +sys.log('All done!'); diff --git a/support/socket.io-client/lib/data.js b/support/socket.io-client/lib/data.js new file mode 100644 index 0000000000..f12d982fa7 --- /dev/null +++ b/support/socket.io-client/lib/data.js @@ -0,0 +1,232 @@ +/** + * Socket.IO client + * + * @author Guillermo Rauch + * @license The MIT license. + * @copyright Copyright (c) 2010 LearnBoost + */ + +io.data = {}; + +/** + * Data decoder class + * + * @api public + */ + +io.data.Decoder = function(){ + this.reset(); + this.buffer = ''; + this.events = {}; +}; + +io.data.Decoder.prototype = { + + /** + * Add data to the buffer for parsing + * + * @param {String} data + * @api public + */ + add: function(data){ + this.buffer += data; + this.parse(); + }, + + /** + * Parse the current buffer + * + * @api private + */ + parse: function(){ + for (var l = this.buffer.length; this.i < l; this.i++){ + var chr = this.buffer[this.i]; + if (this.type === undefined){ + if (chr == ':') return this.error('Data type not specified'); + this.type = '' + chr; + continue; + } + if (this.length === undefined && chr == ':'){ + this.length = ''; + continue; + } + if (this.data === undefined){ + if (chr != ':'){ + this.length += chr; + } else { + if (this.length.length === 0) + return this.error('Data length not specified'); + this.length = Number(this.length); + this.data = ''; + } + continue; + } + if (this.data.length === this.length){ + if (chr == ','){ + this.emit('data', this.type, this.data); + this.buffer = this.buffer.substr(this.i + 1); + this.reset(); + return this.parse(); + } else { + return this.error('Termination character "," expected'); + } + } else { + this.data += chr; + } + } + }, + + /** + * Reset the parser state + * + * @api private + */ + + reset: function(){ + this.i = 0; + this.type = this.data = this.length = undefined; + }, + + /** + * Error handling functions + * + * @param {String} reason to report + * @api private + */ + + error: function(reason){ + this.reset(); + this.emit('error', reason); + }, + + /** + * Emits an event + * + * @param {String} ev name + * @api public + */ + + emit: function(ev){ + if (!(ev in this.events)) + return this; + for (var i = 0, l = this.events[ev].length; i < l; i++) + if (this.events[ev][i]) + this.events[ev][i].apply(this, Array.prototype.slice.call(arguments).slice(1)); + return this; + }, + + /** + * Adds an event listener + * + * @param {String} ev name + * @param {Function} callback + * @api public + */ + + on: function(ev, fn){ + if (!(ev in this.events)) + this.events[ev] = []; + this.events[ev].push(fn); + return this; + }, + + /** + * Removes an event listener + * + * @param {String} ev name + * @param {Function} callback + * @api public + */ + + removeListener: function(ev, fn){ + if (!(ev in this.events)) + return this; + for (var i = 0, l = this.events[ev].length; i < l; i++) + if (this.events[ev][i] == fn) + this.events[ev].splice(i, 1); + return this; + } + +}; + +/** + * Encode function + * + * Examples: + * encode([3, 'Message of type 3']); + * encode([[1, 'Message of type 1], [2, 'Message of type 2]]); + * + * @param {Array} list of messages + * @api public + */ + +io.data.encode = function(messages){ + messages = io.util.isArray(messages[0]) ? messages : [messages]; + var ret = ''; + for (var i = 0, str; i < messages.length; i++){ + str = String(messages[i][1]); + if (str === undefined || str === null) str = ''; + ret += messages[i][0] + ':' + str.length + ':' + str + ','; + } + return ret; +}; + +/** + * Encode message function + * + * @param {String} message + * @param {Object} annotations + * @api public + */ + +io.data.encodeMessage = function(msg, annotations){ + var data = '' + , anns = annotations || {}; + for (var k in anns){ + v = anns[k]; + data += k + (v !== null && v !== undefined ? ':' + v : '') + "\n"; + } + data += ':' + (msg === undefined || msg === null ? '' : msg); + return data; +}; + +/** + * Decode message function + * + * @param {String} message + * @api public + */ + +io.data.decodeMessage = function(msg){ + var anns = {} + , data; + for (var i = 0, chr, key, value, l = msg.length; i < l; i++){ + chr = msg[i]; + if (i === 0 && chr === ':'){ + data = msg.substr(1); + break; + } + if (key == null && value == null && chr == ':'){ + data = msg.substr(i + 1); + break; + } + if (chr === "\n"){ + anns[key] = value; + key = value = undefined; + continue; + } + if (key === undefined){ + key = chr; + continue; + } + if (value === undefined && chr == ':'){ + value = ''; + continue; + } + if (value !== undefined) + value += chr; + else + key += chr; + } + return [data, anns]; +}; diff --git a/support/socket.io-client/lib/io.js b/support/socket.io-client/lib/io.js index 2c20fd3915..d0df365879 100644 --- a/support/socket.io-client/lib/io.js +++ b/support/socket.io-client/lib/io.js @@ -7,7 +7,7 @@ */ this.io = { - version: '0.6', + version: '0.7pre', setPath: function(path){ if (window.console && console.error) console.error('io.setPath will be removed. Please set the variable WEB_SOCKET_SWF_LOCATION pointing to WebSocketMain.swf'); @@ -21,4 +21,4 @@ if ('jQuery' in this) jQuery.io = this.io; if (typeof window != 'undefined'){ // WEB_SOCKET_SWF_LOCATION = (document.location.protocol == 'https:' ? 'https:' : 'http:') + '//cdn.socket.io/' + this.io.version + '/WebSocketMain.swf'; WEB_SOCKET_SWF_LOCATION = '/socket.io/lib/vendor/web-socket-js/WebSocketMain.swf'; -} \ No newline at end of file +} diff --git a/support/socket.io-client/lib/socket.js b/support/socket.io-client/lib/socket.js index f414120974..b4f05b4f64 100644 --- a/support/socket.io-client/lib/socket.js +++ b/support/socket.io-client/lib/socket.js @@ -82,12 +82,28 @@ return this; }; - Socket.prototype.send = function(data){ - if (!this.transport || !this.transport.connected) return this._queue(data); - this.transport.send(data); + Socket.prototype.write = function(message, atts){ + if (!this.transport || !this.transport.connected) return this._queue(message, atts); + this.transport.write(message, atts); return this; }; + Socket.prototype.send = function(message, atts){ + atts = atts || {}; + if (typeof message == 'object'){ + atts['j'] = null; + message = JSON.stringify(message); + } + this.write('1', io.data.encodeMessage(message, atts)); + return this; + }; + + Socket.prototype.json = function(obj, atts){ + atts = atts || {}; + atts['j'] = null + return this.send(JSON.stringify(obj), atts); + } + Socket.prototype.disconnect = function(){ this.transport.disconnect(); return this; @@ -99,7 +115,7 @@ return this; }; - Socket.prototype.fire = function(name, args){ + Socket.prototype.emit = function(name, args){ if (name in this._events){ for (var i = 0, ii = this._events[name].length; i < ii; i++) this._events[name][i].apply(this, args === undefined ? [] : args); @@ -115,15 +131,15 @@ return this; }; - Socket.prototype._queue = function(message){ + Socket.prototype._queue = function(message, atts){ if (!('_queueStack' in this)) this._queueStack = []; - this._queueStack.push(message); + this._queueStack.push([message, atts]); return this; }; Socket.prototype._doQueue = function(){ if (!('_queueStack' in this) || !this._queueStack.length) return this; - this.transport.send(this._queueStack); + this.transport.write(this._queueStack); this._queueStack = []; return this; }; @@ -137,11 +153,11 @@ this.connecting = false; this._doQueue(); if (this.options.rememberTransport) this.options.document.cookie = 'socketio=' + encodeURIComponent(this.transport.type); - this.fire('connect'); + this.emit('connect'); }; Socket.prototype._onMessage = function(data){ - this.fire('message', [data]); + this.emit('message', [data]); }; Socket.prototype._onDisconnect = function(){ @@ -149,9 +165,11 @@ this.connected = false; this.connecting = false; this._queueStack = []; - if (wasConnected) this.fire('disconnect'); + if (wasConnected) this.emit('disconnect'); }; Socket.prototype.addListener = Socket.prototype.addEvent = Socket.prototype.addEventListener = Socket.prototype.on; -})(); \ No newline at end of file + Socket.prototype.fire = Socket.prototype.emit; + +})(); diff --git a/support/socket.io-client/lib/transport.js b/support/socket.io-client/lib/transport.js index 8a13c8ec00..097bc464e5 100644 --- a/support/socket.io-client/lib/transport.js +++ b/support/socket.io-client/lib/transport.js @@ -10,30 +10,21 @@ (function(){ - var frame = '~m~', - - stringify = function(message){ - if (Object.prototype.toString.call(message) == '[object Object]'){ - if (!('JSON' in window)){ - if ('console' in window && console.error) console.error('Trying to encode as JSON, but JSON.stringify is missing.'); - return '{ "$error": "Invalid message" }'; - } - return '~j~' + JSON.stringify(message); - } else { - return String(message); - } - }; - Transport = io.Transport = function(base, options){ + var self = this; this.base = base; this.options = { timeout: 15000 // based on heartbeat interval default }; io.util.merge(this.options, options); + this._decoder = new io.data.Decoder(); + this._decoder.on('data', function(type, message){ + self._onMessage(type, message); + }); }; - Transport.prototype.send = function(){ - throw new Error('Missing send() implementation'); + Transport.prototype.write = function(){ + throw new Error('Missing write() implementation'); }; Transport.prototype.connect = function(){ @@ -44,46 +35,9 @@ throw new Error('Missing disconnect() implementation'); }; - Transport.prototype._encode = function(messages){ - var ret = '', message, - messages = io.util.isArray(messages) ? messages : [messages]; - for (var i = 0, l = messages.length; i < l; i++){ - message = messages[i] === null || messages[i] === undefined ? '' : stringify(messages[i]); - ret += frame + message.length + frame + message; - } - return ret; - }; - - Transport.prototype._decode = function(data){ - var messages = [], number, n; - do { - if (data.substr(0, 3) !== frame) return messages; - data = data.substr(3); - number = '', n = ''; - for (var i = 0, l = data.length; i < l; i++){ - n = Number(data.substr(i, 1)); - if (data.substr(i, 1) == n){ - number += n; - } else { - data = data.substr(number.length + frame.length); - number = Number(number); - break; - } - } - messages.push(data.substr(0, number)); // here - data = data.substr(number); - } while(data !== ''); - return messages; - }; - Transport.prototype._onData = function(data){ this._setTimeout(); - var msgs = this._decode(data); - if (msgs && msgs.length){ - for (var i = 0, l = msgs.length; i < l; i++){ - this._onMessage(msgs[i]); - } - } + this._decoder.add(data); }; Transport.prototype._setTimeout = function(){ @@ -98,21 +52,37 @@ this._onDisconnect(); }; - Transport.prototype._onMessage = function(message){ - if (!this.sessionid){ - this.sessionid = message; - this._onConnect(); - } else if (message.substr(0, 3) == '~h~'){ - this._onHeartbeat(message.substr(3)); - } else if (message.substr(0, 3) == '~j~'){ - this.base._onMessage(JSON.parse(message.substr(3))); - } else { - this.base._onMessage(message); - } + Transport.prototype._onMessage = function(type, message){ + switch (type){ + case '0': + this.disconnect(); + break; + + case '1': + var msg = io.data.decodeMessage(message); + // handle json decoding + if ('j' in msg[1]){ + if (!window.JSON || !JSON.parse) + alert('`JSON.parse` is not available, but Socket.IO is trying to parse' + + 'JSON. Please include json2.js in your '); + msg[0] = JSON.parse(msg[0]); + } + this.base._onMessage(msg[0], msg[1]); + break; + + case '2': + this._onHeartbeat(message); + break; + + case '3': + this.sessionid = message; + this._onConnect(); + break; + } }, Transport.prototype._onHeartbeat = function(heartbeat){ - this.send('~h~' + heartbeat); // echo + this.write('2', heartbeat); // echo }; Transport.prototype._onConnect = function(){ @@ -138,4 +108,4 @@ + (this.sessionid ? ('/' + this.sessionid) : '/'); }; -})(); \ No newline at end of file +})(); diff --git a/support/socket.io-client/lib/transports/jsonp-polling.js b/support/socket.io-client/lib/transports/jsonp-polling.js index 33b78ac8d4..db26f652e1 100644 --- a/support/socket.io-client/lib/transports/jsonp-polling.js +++ b/support/socket.io-client/lib/transports/jsonp-polling.js @@ -101,6 +101,15 @@ JSONPPolling.prototype._get = function(){ this._script = script; }; +JSONPPolling.prototype.disconnect = function(){ + if (this._script){ + this._script.parentNode.removeChild(this._script); + this._script = null; + } + io.Transport['xhr-polling'].prototype.disconnect.call(this); + return this; +}; + JSONPPolling.prototype._ = function(){ this._onData.apply(this, arguments); this._get(); @@ -113,4 +122,4 @@ JSONPPolling.check = function(){ JSONPPolling.xdomainCheck = function(){ return true; -}; \ No newline at end of file +}; diff --git a/support/socket.io-client/lib/transports/websocket.js b/support/socket.io-client/lib/transports/websocket.js index f4d2bd345e..6a8def88f7 100644 --- a/support/socket.io-client/lib/transports/websocket.js +++ b/support/socket.io-client/lib/transports/websocket.js @@ -24,13 +24,15 @@ return this; }; - WS.prototype.send = function(data){ - if (this.socket) this.socket.send(this._encode(data)); + WS.prototype.write = function(type, data){ + if (this.socket) + this.socket.send(io.data.encode(io.util.isArray(type) ? type : [type, data])); return this; }; WS.prototype.disconnect = function(){ if (this.socket) this.socket.close(); + this._onDisconnect(); return this; }; @@ -57,4 +59,4 @@ return true; }; -})(); \ No newline at end of file +})(); diff --git a/support/socket.io-client/lib/transports/xhr.js b/support/socket.io-client/lib/transports/xhr.js index 041ab11819..575e80d3c0 100644 --- a/support/socket.io-client/lib/transports/xhr.js +++ b/support/socket.io-client/lib/transports/xhr.js @@ -48,18 +48,17 @@ XHR.prototype._checkSend = function(){ if (!this._posting && this._sendBuffer.length){ - var encoded = this._encode(this._sendBuffer); + var encoded = io.data.encode(this._sendBuffer); this._sendBuffer = []; this._send(encoded); } }; - XHR.prototype.send = function(data){ - if (io.util.isArray(data)){ - this._sendBuffer.push.apply(this._sendBuffer, data); - } else { - this._sendBuffer.push(data); - } + XHR.prototype.write = function(type, data){ + if (io.util.isArray(type)) + this._sendBuffer.push.apply(this._sendBuffer, type); + else + this._sendBuffer.push([type, data]); this._checkSend(); return this; }; @@ -128,4 +127,4 @@ XHR.request = request; -})(); \ No newline at end of file +})(); diff --git a/support/socket.io-client/socket.io.js b/support/socket.io-client/socket.io.js index 7c862ec890..296012cf0c 100644 --- a/support/socket.io-client/socket.io.js +++ b/support/socket.io-client/socket.io.js @@ -1,4 +1,4 @@ -/** Socket.IO 0.6 - Built with build.js */ +/** Socket.IO 0.7pre - Built with build.js */ /** * Socket.IO client * @@ -8,7 +8,7 @@ */ this.io = { - version: '0.6', + version: '0.7pre', setPath: function(path){ if (window.console && console.error) console.error('io.setPath will be removed. Please set the variable WEB_SOCKET_SWF_LOCATION pointing to WebSocketMain.swf'); @@ -23,6 +23,7 @@ if (typeof window != 'undefined'){ // WEB_SOCKET_SWF_LOCATION = (document.location.protocol == 'https:' ? 'https:' : 'http:') + '//cdn.socket.io/' + this.io.version + '/WebSocketMain.swf'; WEB_SOCKET_SWF_LOCATION = '/socket.io/lib/vendor/web-socket-js/WebSocketMain.swf'; } + /** * Socket.IO client * @@ -83,6 +84,239 @@ if (typeof window != 'undefined'){ }); })(); +/** + * Socket.IO client + * + * @author Guillermo Rauch + * @license The MIT license. + * @copyright Copyright (c) 2010 LearnBoost + */ + +io.data = {}; + +/** + * Data decoder class + * + * @api public + */ + +io.data.Decoder = function(){ + this.reset(); + this.buffer = ''; + this.events = {}; +}; + +io.data.Decoder.prototype = { + + /** + * Add data to the buffer for parsing + * + * @param {String} data + * @api public + */ + add: function(data){ + this.buffer += data; + this.parse(); + }, + + /** + * Parse the current buffer + * + * @api private + */ + parse: function(){ + for (var l = this.buffer.length; this.i < l; this.i++){ + var chr = this.buffer[this.i]; + if (this.type === undefined){ + if (chr == ':') return this.error('Data type not specified'); + this.type = '' + chr; + continue; + } + if (this.length === undefined && chr == ':'){ + this.length = ''; + continue; + } + if (this.data === undefined){ + if (chr != ':'){ + this.length += chr; + } else { + if (this.length.length === 0) + return this.error('Data length not specified'); + this.length = Number(this.length); + this.data = ''; + } + continue; + } + if (this.data.length === this.length){ + if (chr == ','){ + this.emit('data', this.type, this.data); + this.buffer = this.buffer.substr(this.i + 1); + this.reset(); + return this.parse(); + } else { + return this.error('Termination character "," expected'); + } + } else { + this.data += chr; + } + } + }, + + /** + * Reset the parser state + * + * @api private + */ + + reset: function(){ + this.i = 0; + this.type = this.data = this.length = undefined; + }, + + /** + * Error handling functions + * + * @param {String} reason to report + * @api private + */ + + error: function(reason){ + this.reset(); + this.emit('error', reason); + }, + + /** + * Emits an event + * + * @param {String} ev name + * @api public + */ + + emit: function(ev){ + if (!(ev in this.events)) + return this; + for (var i = 0, l = this.events[ev].length; i < l; i++) + if (this.events[ev][i]) + this.events[ev][i].apply(this, Array.prototype.slice.call(arguments).slice(1)); + return this; + }, + + /** + * Adds an event listener + * + * @param {String} ev name + * @param {Function} callback + * @api public + */ + + on: function(ev, fn){ + if (!(ev in this.events)) + this.events[ev] = []; + this.events[ev].push(fn); + return this; + }, + + /** + * Removes an event listener + * + * @param {String} ev name + * @param {Function} callback + * @api public + */ + + removeListener: function(ev, fn){ + if (!(ev in this.events)) + return this; + for (var i = 0, l = this.events[ev].length; i < l; i++) + if (this.events[ev][i] == fn) + this.events[ev].splice(i, 1); + return this; + } + +}; + +/** + * Encode function + * + * Examples: + * encode([3, 'Message of type 3']); + * encode([[1, 'Message of type 1], [2, 'Message of type 2]]); + * + * @param {Array} list of messages + * @api public + */ + +io.data.encode = function(messages){ + messages = io.util.isArray(messages[0]) ? messages : [messages]; + var ret = ''; + for (var i = 0, str; i < messages.length; i++){ + str = String(messages[i][1]); + if (str === undefined || str === null) str = ''; + ret += messages[i][0] + ':' + str.length + ':' + str + ','; + } + return ret; +}; + +/** + * Encode message function + * + * @param {String} message + * @param {Object} annotations + * @api public + */ + +io.data.encodeMessage = function(msg, annotations){ + var data = '' + , anns = annotations || {}; + for (var k in anns){ + v = anns[k]; + data += k + (v !== null && v !== undefined ? ':' + v : '') + "\n"; + } + data += ':' + (msg === undefined || msg === null ? '' : msg); + return data; +}; + +/** + * Decode message function + * + * @param {String} message + * @api public + */ + +io.data.decodeMessage = function(msg){ + var anns = {} + , data; + for (var i = 0, chr, key, value, l = msg.length; i < l; i++){ + chr = msg[i]; + if (i === 0 && chr === ':'){ + data = msg.substr(1); + break; + } + if (key == null && value == null && chr == ':'){ + data = msg.substr(i + 1); + break; + } + if (chr === "\n"){ + anns[key] = value; + key = value = undefined; + continue; + } + if (key === undefined){ + key = chr; + continue; + } + if (value === undefined && chr == ':'){ + value = ''; + continue; + } + if (value !== undefined) + value += chr; + else + key += chr; + } + return [data, anns]; +}; + /** * Socket.IO client * @@ -95,30 +329,21 @@ if (typeof window != 'undefined'){ (function(){ - var frame = '~m~', - - stringify = function(message){ - if (Object.prototype.toString.call(message) == '[object Object]'){ - if (!('JSON' in window)){ - if ('console' in window && console.error) console.error('Trying to encode as JSON, but JSON.stringify is missing.'); - return '{ "$error": "Invalid message" }'; - } - return '~j~' + JSON.stringify(message); - } else { - return String(message); - } - }; - Transport = io.Transport = function(base, options){ + var self = this; this.base = base; this.options = { timeout: 15000 // based on heartbeat interval default }; io.util.merge(this.options, options); + this._decoder = new io.data.Decoder(); + this._decoder.on('data', function(type, message){ + self._onMessage(type, message); + }); }; - Transport.prototype.send = function(){ - throw new Error('Missing send() implementation'); + Transport.prototype.write = function(){ + throw new Error('Missing write() implementation'); }; Transport.prototype.connect = function(){ @@ -129,46 +354,9 @@ if (typeof window != 'undefined'){ throw new Error('Missing disconnect() implementation'); }; - Transport.prototype._encode = function(messages){ - var ret = '', message, - messages = io.util.isArray(messages) ? messages : [messages]; - for (var i = 0, l = messages.length; i < l; i++){ - message = messages[i] === null || messages[i] === undefined ? '' : stringify(messages[i]); - ret += frame + message.length + frame + message; - } - return ret; - }; - - Transport.prototype._decode = function(data){ - var messages = [], number, n; - do { - if (data.substr(0, 3) !== frame) return messages; - data = data.substr(3); - number = '', n = ''; - for (var i = 0, l = data.length; i < l; i++){ - n = Number(data.substr(i, 1)); - if (data.substr(i, 1) == n){ - number += n; - } else { - data = data.substr(number.length + frame.length); - number = Number(number); - break; - } - } - messages.push(data.substr(0, number)); // here - data = data.substr(number); - } while(data !== ''); - return messages; - }; - Transport.prototype._onData = function(data){ this._setTimeout(); - var msgs = this._decode(data); - if (msgs && msgs.length){ - for (var i = 0, l = msgs.length; i < l; i++){ - this._onMessage(msgs[i]); - } - } + this._decoder.add(data); }; Transport.prototype._setTimeout = function(){ @@ -183,21 +371,37 @@ if (typeof window != 'undefined'){ this._onDisconnect(); }; - Transport.prototype._onMessage = function(message){ - if (!this.sessionid){ - this.sessionid = message; - this._onConnect(); - } else if (message.substr(0, 3) == '~h~'){ - this._onHeartbeat(message.substr(3)); - } else if (message.substr(0, 3) == '~j~'){ - this.base._onMessage(JSON.parse(message.substr(3))); - } else { - this.base._onMessage(message); - } + Transport.prototype._onMessage = function(type, message){ + switch (type){ + case '0': + this.disconnect(); + break; + + case '1': + var msg = io.data.decodeMessage(message); + // handle json decoding + if ('j' in msg[1]){ + if (!window.JSON || !JSON.parse) + alert('`JSON.parse` is not available, but Socket.IO is trying to parse' + + 'JSON. Please include json2.js in your '); + msg[0] = JSON.parse(msg[0]); + } + this.base._onMessage(msg[0], msg[1]); + break; + + case '2': + this._onHeartbeat(message); + break; + + case '3': + this.sessionid = message; + this._onConnect(); + break; + } }, Transport.prototype._onHeartbeat = function(heartbeat){ - this.send('~h~' + heartbeat); // echo + this.write('2', heartbeat); // echo }; Transport.prototype._onConnect = function(){ @@ -224,6 +428,7 @@ if (typeof window != 'undefined'){ }; })(); + /** * Socket.IO client * @@ -274,18 +479,17 @@ if (typeof window != 'undefined'){ XHR.prototype._checkSend = function(){ if (!this._posting && this._sendBuffer.length){ - var encoded = this._encode(this._sendBuffer); + var encoded = io.data.encode(this._sendBuffer); this._sendBuffer = []; this._send(encoded); } }; - XHR.prototype.send = function(data){ - if (io.util.isArray(data)){ - this._sendBuffer.push.apply(this._sendBuffer, data); - } else { - this._sendBuffer.push(data); - } + XHR.prototype.write = function(type, data){ + if (io.util.isArray(type)) + this._sendBuffer.push.apply(this._sendBuffer, type); + else + this._sendBuffer.push([type, data]); this._checkSend(); return this; }; @@ -355,6 +559,7 @@ if (typeof window != 'undefined'){ XHR.request = request; })(); + /** * Socket.IO client * @@ -381,13 +586,15 @@ if (typeof window != 'undefined'){ return this; }; - WS.prototype.send = function(data){ - if (this.socket) this.socket.send(this._encode(data)); + WS.prototype.write = function(type, data){ + if (this.socket) + this.socket.send(io.data.encode(io.util.isArray(type) ? type : [type, data])); return this; }; WS.prototype.disconnect = function(){ if (this.socket) this.socket.close(); + this._onDisconnect(); return this; }; @@ -415,6 +622,7 @@ if (typeof window != 'undefined'){ }; })(); + /** * Socket.IO client * @@ -748,6 +956,15 @@ JSONPPolling.prototype._get = function(){ this._script = script; }; +JSONPPolling.prototype.disconnect = function(){ + if (this._script){ + this._script.parentNode.removeChild(this._script); + this._script = null; + } + io.Transport['xhr-polling'].prototype.disconnect.call(this); + return this; +}; + JSONPPolling.prototype._ = function(){ this._onData.apply(this, arguments); this._get(); @@ -761,6 +978,7 @@ JSONPPolling.check = function(){ JSONPPolling.xdomainCheck = function(){ return true; }; + /** * Socket.IO client * @@ -845,12 +1063,28 @@ JSONPPolling.xdomainCheck = function(){ return this; }; - Socket.prototype.send = function(data){ - if (!this.transport || !this.transport.connected) return this._queue(data); - this.transport.send(data); + Socket.prototype.write = function(message, atts){ + if (!this.transport || !this.transport.connected) return this._queue(message, atts); + this.transport.write(message, atts); return this; }; + Socket.prototype.send = function(message, atts){ + atts = atts || {}; + if (typeof message == 'object'){ + atts['j'] = null; + message = JSON.stringify(message); + } + this.write('1', io.data.encodeMessage(message, atts)); + return this; + }; + + Socket.prototype.json = function(obj, atts){ + atts = atts || {}; + atts['j'] = null + return this.send(JSON.stringify(obj), atts); + } + Socket.prototype.disconnect = function(){ this.transport.disconnect(); return this; @@ -862,7 +1096,7 @@ JSONPPolling.xdomainCheck = function(){ return this; }; - Socket.prototype.fire = function(name, args){ + Socket.prototype.emit = function(name, args){ if (name in this._events){ for (var i = 0, ii = this._events[name].length; i < ii; i++) this._events[name][i].apply(this, args === undefined ? [] : args); @@ -878,15 +1112,15 @@ JSONPPolling.xdomainCheck = function(){ return this; }; - Socket.prototype._queue = function(message){ + Socket.prototype._queue = function(message, atts){ if (!('_queueStack' in this)) this._queueStack = []; - this._queueStack.push(message); + this._queueStack.push([message, atts]); return this; }; Socket.prototype._doQueue = function(){ if (!('_queueStack' in this) || !this._queueStack.length) return this; - this.transport.send(this._queueStack); + this.transport.write(this._queueStack); this._queueStack = []; return this; }; @@ -900,11 +1134,11 @@ JSONPPolling.xdomainCheck = function(){ this.connecting = false; this._doQueue(); if (this.options.rememberTransport) this.options.document.cookie = 'socketio=' + encodeURIComponent(this.transport.type); - this.fire('connect'); + this.emit('connect'); }; Socket.prototype._onMessage = function(data){ - this.fire('message', [data]); + this.emit('message', [data]); }; Socket.prototype._onDisconnect = function(){ @@ -912,12 +1146,15 @@ JSONPPolling.xdomainCheck = function(){ this.connected = false; this.connecting = false; this._queueStack = []; - if (wasConnected) this.fire('disconnect'); + if (wasConnected) this.emit('disconnect'); }; Socket.prototype.addListener = Socket.prototype.addEvent = Socket.prototype.addEventListener = Socket.prototype.on; + Socket.prototype.fire = Socket.prototype.emit; + })(); + /* SWFObject v2.2 is released under the MIT License */ From b57b00b9ffecaf6e6e06dca0656274966ce1f4d8 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 16 Nov 2010 05:24:53 -0300 Subject: [PATCH 34/36] Added missing Parser#error method Restored previous sessionid generation mechanism temporarily --- lib/socket.io/client.js | 2 +- lib/socket.io/transports/websocket.js | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/socket.io/client.js b/lib/socket.io/client.js index 1bcd25fa18..1dd8ae0082 100644 --- a/lib/socket.io/client.js +++ b/lib/socket.io/client.js @@ -190,7 +190,7 @@ Client.prototype._queue = function(type, data){ }; Client.prototype._generateSessionId = function(){ - this.sessionId = ++this.listener._clientCount; // REFACTORME + this.sessionId = Math.random().toString().substr(2); // REFACTORME return this; }; diff --git a/lib/socket.io/transports/websocket.js b/lib/socket.io/transports/websocket.js index dcf02d7262..3e635d0ba5 100644 --- a/lib/socket.io/transports/websocket.js +++ b/lib/socket.io/transports/websocket.js @@ -21,6 +21,7 @@ WebSocket.prototype._onConnect = function(req, socket){ this.parser = new Parser(); this.parser.on('data', self._onData.bind(this)); + this.parser.on('error', self._onClose.bind(this)); Client.prototype._onConnect.call(this, req); @@ -154,3 +155,10 @@ Parser.prototype.parse = function(){ } } }; + +Parser.prototype.error = function(reason){ + this.buffer = ''; + this.i = 0; + this.emit('error', reason); + return this; +}; From 1639a8f4548b3d35d546b92eb0af9f8377e64f0f Mon Sep 17 00:00:00 2001 From: Brian McKelvey Date: Sat, 4 Dec 2010 12:28:52 -0800 Subject: [PATCH 35/36] Revert "Applying patch to allow for Draft-76 websocket connections through an HAProxy layer." This reverts commit 606bc70c506c2dbe7a55be840c25427f95d7e706. --- lib/socket.io/transports/websocket.js | 59 ++++----------------------- 1 file changed, 8 insertions(+), 51 deletions(-) diff --git a/lib/socket.io/transports/websocket.js b/lib/socket.io/transports/websocket.js index 818ca90de6..d3798444c2 100644 --- a/lib/socket.io/transports/websocket.js +++ b/lib/socket.io/transports/websocket.js @@ -28,22 +28,6 @@ WebSocket.prototype._onConnect = function(req, socket){ + '://' + this.request.headers.host + this.request.url; if ('sec-websocket-key1' in this.request.headers){ - /* We need to send the 101 response immediately when using Draft 76 with - a load balancing proxy, such as HAProxy. In order to protect an - unsuspecting non-websocket HTTP server, HAProxy will not send the - 8-byte nonce through the connection until the Upgrade: WebSocket - request has been confirmed by the WebSocket server by a 101 response - indicating that the server can handle the upgraded protocol. We - therefore must send the 101 response immediately, and then wait for - the nonce to be forwarded to us afterward in order to finish the - Draft 76 handshake. - */ - - // If we don't have the nonce yet, wait for it. - if (!(this.upgradeHead && this.upgradeHead.length >= 8)) { - this.waitingForNonce = true; - } - headers = [ 'HTTP/1.1 101 WebSocket Protocol Handshake', 'Upgrade: WebSocket', @@ -63,12 +47,12 @@ WebSocket.prototype._onConnect = function(req, socket){ 'WebSocket-Origin: ' + origin, 'WebSocket-Location: ' + location ]; - } - - try { - this.connection.write(headers.concat('', '').join('\r\n')); - } catch(e){ - this._onClose(); + + try { + this.connection.write(headers.concat('', '').join('\r\n')); + } catch(e){ + this._onClose(); + } } this.connection.setTimeout(0); @@ -79,39 +63,12 @@ WebSocket.prototype._onConnect = function(req, socket){ self._handle(data); }); - req.addListener('error', function(err){ - req.end && req.end() || req.destroy && req.destroy(); - }); - socket.addListener('error', function(data){ - socket && (socket.end && socket.end() || socket.destroy && socket.destroy()); - }); - - if (this.waitingForNonce) { - // Since we will be receiving the binary nonce through the normal HTTP - // data event, set the connection to 'binary' temporarily - this.connection.setEncoding('binary'); - this._headers = headers; - } - else { - if (this._proveReception(headers)) this._payload(); - } + if (this._proveReception(headers)) this._payload(); }; WebSocket.prototype._handle = function(data){ var chunk, chunks, chunk_count; this.data += data; - if (this.waitingForNonce) { - if (this.data.length < 8) { return; } - // Restore the connection to utf8 encoding after receiving the nonce - this.connection.setEncoding('utf8'); - this.waitingForNonce = false; - // Stuff the nonce into the location where it's expected to be - this.upgradeHead = this.data.substr(0,8); - this.data = this.data.substr(8); - if (this._proveReception(this._headers)) { this._payload(); } - return; - } - chunks = this.data.split('\ufffd'); chunk_count = chunks.length - 1; for (var i = 0; i < chunk_count; i++){ @@ -157,7 +114,7 @@ WebSocket.prototype._proveReception = function(headers){ md5.update(this.upgradeHead.toString('binary')); try { - this.connection.write(md5.digest('binary'), 'binary'); + this.connection.write(headers.concat('', '').join('\r\n') + md5.digest('binary'), 'binary'); } catch(e){ this._onClose(); } From f6821d8b209a1cca519c20f90666c328bbf9ae20 Mon Sep 17 00:00:00 2001 From: Brian McKelvey Date: Sun, 5 Dec 2010 00:42:22 -0800 Subject: [PATCH 36/36] Applying patch to allow Draft-76 websocket to work behind haproxy. --- lib/socket.io/transports/websocket.js | 61 +++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/lib/socket.io/transports/websocket.js b/lib/socket.io/transports/websocket.js index 3e635d0ba5..27e3fea1ca 100644 --- a/lib/socket.io/transports/websocket.js +++ b/lib/socket.io/transports/websocket.js @@ -34,8 +34,25 @@ WebSocket.prototype._onConnect = function(req, socket){ var origin = this.request.headers.origin, location = (origin && origin.substr(0, 5) == 'https' ? 'wss' : 'ws') + '://' + this.request.headers.host + this.request.url; - + + this.waitingForNonce = false; if ('sec-websocket-key1' in this.request.headers){ + /* We need to send the 101 response immediately when using Draft 76 with + a load balancing proxy, such as HAProxy. In order to protect an + unsuspecting non-websocket HTTP server, HAProxy will not send the + 8-byte nonce through the connection until the Upgrade: WebSocket + request has been confirmed by the WebSocket server by a 101 response + indicating that the server can handle the upgraded protocol. We + therefore must send the 101 response immediately, and then wait for + the nonce to be forwarded to us afterward in order to finish the + Draft 76 handshake. + */ + + // If we don't have the nonce yet, wait for it. + if (!(this.upgradeHead && this.upgradeHead.length >= 8)) { + this.waitingForNonce = true; + } + headers = [ 'HTTP/1.1 101 WebSocket Protocol Handshake', 'Upgrade: WebSocket', @@ -56,22 +73,50 @@ WebSocket.prototype._onConnect = function(req, socket){ 'WebSocket-Location: ' + location ]; - try { - this.connection.write(headers.concat('', '').join('\r\n')); - } catch(e){ - this._onClose(); - } + } + + try { + this.connection.write(headers.concat('', '').join('\r\n')); + } catch(e){ + this._onClose(); } this.connection.setTimeout(0); this.connection.setNoDelay(true); this.connection.setEncoding('utf-8'); + if (this.waitingForNonce) { + // Since we will be receiving the binary nonce through the normal HTTP + // data event, set the connection to 'binary' temporarily + this.connection.setEncoding('binary'); + this._headers = headers; + } + else { + if (this._proveReception(headers)) this._payload(); + } + + this.buffer = ""; + this.connection.addListener('data', function(data){ + self.buffer += data; + if (self.waitingForNonce) { + if (self.buffer.length < 8) { return; } + // Restore the connection to utf8 encoding after receiving the nonce + self.connection.setEncoding('utf8'); + self.waitingForNonce = false; + // Stuff the nonce into the location where it's expected to be + self.upgradeHead = self.buffer.substr(0,8); + self.buffer = self.buffer.substr(8); + if (self.buffer.length > 0) { + self.parser.add(self.buffer); + } + if (self._proveReception(self._headers)) { self._payload(); } + return; + } + self.parser.add(data); }); - if (this._proveReception(headers)) this._payload(); }; // http://www.whatwg.org/specs/web-apps/current-work/complete/network.html#opening-handshake @@ -105,7 +150,7 @@ WebSocket.prototype._proveReception = function(headers){ md5.update(this.upgradeHead.toString('binary')); try { - this.connection.write(headers.concat('', '').join('\r\n') + md5.digest('binary'), 'binary'); + this.connection.write(md5.digest('binary'), 'binary'); } catch(e){ this._onClose(); }