diff --git a/.gitignore b/.gitignore index 1c8f4f45ab..67419b4650 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,7 @@ lib-cov *.csv *.dat *.out -*.pid \ No newline at end of file +*.pid.swp +*.swp +*.swo +*.swn 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/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/README.md b/README.md index 6f98dc0d50..0afd675dbf 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,8 @@ On the server: server = http.createServer(function(req, res){ // your normal server code res.writeHeader(200, {'Content-Type': 'text/html'}); - res.writeBody('

Hello world

'); - res.finish(); + res.write('

Hello world

'); + res.end(); }); server.listen(80); @@ -50,8 +50,8 @@ On the server: socket.on('connection', function(client){ // new client is here! - client.on('message', function(){ … }) - client.on('disconnect', function(){ … }) + client.on('message', function(){ //… }) + client.on('disconnect', function(){ //… }) }); On the client: @@ -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/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/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/client.js b/lib/socket.io/client.js index 0e21fee502..1dd8ae0082 100644 --- a/lib/socket.io/client.js +++ b/lib/socket.io/client.js @@ -1,15 +1,18 @@ 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 + , decodeMessage = require('./data').decodeMessage , 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){ process.EventEmitter.call(this); this.listener = listener; this.options(merge({ + ignoreEmptyOrigin: true, timeout: 8000, heartbeatInterval: 10000, closeTimeout: 0 @@ -19,39 +22,58 @@ var Client = module.exports = function(listener, req, res, options, head){ this._heartbeats = 0; this.connected = false; this.upgradeHead = head; + this.decoder = new Decoder(); + this.decoder.on('data', this._onMessage.bind(this)); 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); +Client.prototype.send = function(message, anns){ + anns = anns || {}; + if (typeof message == 'object'){ + anns['j'] = null; + message = JSON.stringify(message); } - this._write(encode(message)); - return this; + 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.broadcast = function(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, anns){ if (!('sessionId' in this)) return this; - this.listener.broadcast(message, this.sessionId); + this.listener.broadcast(message, this.sessionId, anns); 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); } }; @@ -82,34 +104,42 @@ 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(){ - self.send('~h~' + ++self._heartbeats); + self.write('2', ++self._heartbeats); self._heartbeatTimeout = setTimeout(function(){ self._onClose(); }, self.options.timeout); - }, self.options.heartbeatInterval); + }, this.options.heartbeatInterval); }; Client.prototype._onHeartbeat = function(h){ @@ -153,31 +183,33 @@ 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; }; Client.prototype._generateSessionId = function(){ - this.sessionId = ++this.listener._clientCount; // REFACTORME + this.sessionId = Math.random().toString().substr(2); // REFACTORME return this; }; 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 || - 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) {} } - return false; + + return this.options.ignoreEmptyOrigin; }; -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 new file mode 100644 index 0000000000..918305b9af --- /dev/null +++ b/lib/socket.io/data.js @@ -0,0 +1,193 @@ +/** + * 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 = 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 + */ + +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; + } + 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]; +}; + +/** + * Export APIs + */ + +exports.Decoder = Decoder; +exports.encode = encode; +exports.encodeMessage = encodeMessage; +exports.decodeMessage = decodeMessage; diff --git a/lib/socket.io/listener.js b/lib/socket.io/listener.js index 2a40b9aa31..2c13bdf8fd 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,9 +29,10 @@ 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 = {}; + this._realms = {}; var listeners = this.server.listeners('request'); this.server.removeAllListeners('request'); @@ -58,16 +60,22 @@ 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; }; +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){ @@ -146,24 +154,24 @@ Listener.prototype._serveClient = function(file, req, res){ return false; }; +Listener.prototype.realm = function(realm){ + 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'); - this.emit('clientConnect', 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'); - this.emit('clientDisconnect', 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/lib/socket.io/realm.js b/lib/socket.io/realm.js new file mode 100644 index 0000000000..4bd6ae3b47 --- /dev/null +++ b/lib/socket.io/realm.js @@ -0,0 +1,157 @@ +/** + * 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 self = this; + if (ev == 'connection') + this.listener.on('connection', function(conn){ + fn.call(self, new RealmClient(self.name, conn)); + }); + 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]; + }); +}); + +/** + * List of methods to proxy to the listener + */ + +['realm'].forEach(function(m){ + Realm.prototype[m] = function(){ + 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/lib/socket.io/tests.js b/lib/socket.io/tests.js new file mode 100644 index 0000000000..af34b5c470 --- /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, 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)]); + }, + + 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/lib/socket.io/transports/flashsocket.js b/lib/socket.io/transports/flashsocket.js index 75515ea6b4..522c0e5c0c 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,15 +72,16 @@ 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){ - var parts = origin.split(':'); - xml += '\n'; + var p = origin.split(':'); + xml += '\n'; }); }); xml += '\n'; return xml; -}; \ No newline at end of file +}; diff --git a/lib/socket.io/transports/htmlfile.js b/lib/socket.io/transports/htmlfile.js index 9812c101b7..4365047309 100644 --- a/lib/socket.io/transports/htmlfile.js +++ b/lib/socket.io/transports/htmlfile.js @@ -30,17 +30,22 @@ 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'); res.end(); }); - break; } }; -HTMLFile.prototype._write = function(message){ - if (this._open) - this.response.write(''); //json for escaping -}; \ No newline at end of file +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 502a9613a0..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 @@ -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 { - message = "io.JSONP["+ this._index +"]._("+ JSON.stringify(message) +");"; - } - this.response.writeHead(200, {'Content-Type': 'text/javascript; charset=UTF-8', 'Content-Length': Buffer.byteLength(message)}); + if (this._verifyOrigin(this.request.headers.origin)) + message = "io.JSONP["+ this._index +"]._("+ JSON.stringify(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/lib/socket.io/transports/websocket.js b/lib/socket.io/transports/websocket.js index d3798444c2..3e635d0ba5 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,17 @@ 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)); + this.parser.on('error', self._onClose.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 +68,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._onMessage(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 @@ -133,4 +124,41 @@ WebSocket.prototype._write = function(message){ } }; -WebSocket.httpUpgrade = true; \ No newline at end of file +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(); + } + } +}; + +Parser.prototype.error = function(reason){ + this.buffer = ''; + this.i = 0; + this.emit('error', reason); + return this; +}; diff --git a/lib/socket.io/transports/xhr-multipart.js b/lib/socket.io/transports/xhr-multipart.js index 1c2e9c7253..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'; } @@ -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..f39207239c 100644 --- a/lib/socket.io/transports/xhr-polling.js +++ b/lib/socket.io/transports/xhr-polling.js @@ -33,21 +33,19 @@ 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? var msg = qs.parse(body); - self._onMessage(msg.data); + self._onData(msg.data); } catch(e){} res.writeHead(200, headers); res.write('ok'); @@ -75,4 +73,4 @@ Polling.prototype._write = function(message){ this.response.end(); this._onClose(); } -}; \ No newline at end of file +}; 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/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 */ diff --git a/tests/data.js b/tests/data.js new file mode 100644 index 0000000000..16f8a55761 --- /dev/null +++ b/tests/data.js @@ -0,0 +1,210 @@ +var Decoder = require('socket.io/data').Decoder + , encode = require('socket.io/data').encode + , encodeMessage = require('socket.io/data').encodeMessage + , decodeMessage = require('socket.io/data').decodeMessage; + +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'); + }); + }, + + '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'); + }, + + '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'); + } + +}; diff --git a/tests/listener.js b/tests/listener.js index 51b558e8b2..eb645a8f59 100644 --- a/tests/listener.js +++ b/tests/listener.js @@ -1,20 +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); -}; +require('socket.io/tests'); function listen(s, callback){ s._port = port; @@ -23,11 +14,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'); -}; - module.exports = { 'test serving static javascript client': function(assert){ @@ -108,16 +94,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[0] === '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[0] === 'broadcasted msg'); + --trips || close(); + }); } }; }) @@ -147,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(); + } + }); } - -}; \ No newline at end of file + +}; diff --git a/tests/transports.flashsocket.js b/tests/transports.flashsocket.js index 5f8169a24f..47f2515f95 100644 --- a/tests/transports.flashsocket.js +++ b/tests/transports.flashsocket.js @@ -2,25 +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); -}; +require('socket.io/tests'); function listen(s, callback){ s._port = port; @@ -45,4 +29,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.jsonp-polling.js b/tests/transports.jsonp-polling.js index fef64db5a0..121f5abe72 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, alert){ 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){ @@ -42,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, 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 = ''; @@ -81,19 +84,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 +131,82 @@ 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(); + }); + }); }); }); }); }); + }, + + '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' + ); + }); + }, + + '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(); + } + ); + }, + '' + ); + }); } -}; \ No newline at end of file +}; diff --git a/tests/transports.websocket.js b/tests/transports.websocket.js index 0496eb81fd..d90ded1758 100644 --- a/tests/transports.websocket.js +++ b/tests/transports.websocket.js @@ -1,20 +1,9 @@ var io = require('socket.io') - , encode = require('socket.io/utils').encode - , decode = require('socket.io/utils').decode , 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; @@ -23,11 +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'); -}; - module.exports = { 'test connection and handshake': function(assert){ @@ -48,10 +32,15 @@ module.exports = { _client.send(encode('from client')); }; _client.onmessage = function(ev){ - if (++messages == 2){ // first message is the session id - assert.ok(decode(ev.data), '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(); + } + }); }; }); @@ -69,7 +58,7 @@ module.exports = { 'test clients tracking': function(assert){ var _server = server() , _socket = socket(_server); - + listen(_server, function(){ var _client = client(_server); _client.onopen = function(){ @@ -114,10 +103,12 @@ 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'); - _socket.clients[sessionid].options.closeTimeout = 0; - _client2.close(); - _server.close(); + decode(ev.data, function(msg){ + assert.ok(msg[0] === 'should get this'); + _socket.clients[sessionid].options.closeTimeout = 0; + _client2.close(); + _server.close(); + }); }; runOnce = true; } @@ -153,10 +144,11 @@ module.exports = { _client = client(_server); _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'); - _client.send(encode({ from: 'client' })); - --trips || close(); + decode(ev.data, function(msg){ + assert.ok('j' in msg[1]); + _client.send(encode({ from: 'client' })); + --trips || close(); + }); } }; }); @@ -178,15 +170,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); + } + }); }; }); }, @@ -205,11 +200,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(); + }); } }; @@ -218,14 +215,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'); + }); }; }; }; @@ -241,5 +242,5 @@ module.exports = { }); } - -}; \ No newline at end of file + +}; 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 +}; 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 +}; diff --git a/tests/utils.js b/tests/utils.js index 6b0072389a..8a3c0068be 100644 --- a/tests/utils.js +++ b/tests/utils.js @@ -1,25 +1,12 @@ -var encode = require('socket.io/utils').encode, - decode = require('socket.io/utils').decode; +var merge = require('socket.io/utils').merge; 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 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'); } -}; \ No newline at end of file + +}