diff --git a/History.md b/History.md new file mode 100644 index 0000000000..c8aa68fa88 --- /dev/null +++ b/History.md @@ -0,0 +1,5 @@ + +0.0.1 / 2010-01-03 +================== + + * Initial release diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..c304ec2896 --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ + +ALL_TESTS = $(shell find test/ -name '*.test.js') + +run-tests: + @./support/expresso/bin/expresso \ + -I support/should.js/lib \ + -I support \ + -I lib \ + --serial \ + $(TESTS) + +test: + @$(MAKE) TESTS="$(ALL_TESTS)" run-tests + +.PHONY: test diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000000..454e7dd7c8 --- /dev/null +++ b/Readme.md @@ -0,0 +1,283 @@ + +# Socket.IO + +Socket.IO is a Node.JS project that makes WebSockets and realtime possible in +all browsers. It also enhances WebSockets by providing built-in multiplexing, +horizontal scalability, automatic JSON encoding/decoding, and more. + +## How to Install + + npm install socket.io + +## How to use + +First, require `socket.io`: + + var io = require('socket.io'); + +Next, attach it to a HTTP/HTTPS server. If you're using the fantastic `express` +web framework: + + var app = express.createServer(); + , sockets = io.listen(app); + + app.listen(80); + + sockets.on('connection', function (socket) { + socket.send({ hello: 'world' }); + }); + +Finally, load it from the client side code: + + + + +For more thorough examples, look at the `examples/` directory. + +## Short recipes + +### Sending and receiving events. + +Socket.IO allows you to emit and receive custom events. +Besides `connect`, `message` and `disconnect`, you can emit custom events: + + // note, io.listen() will create a http server for you + var io = require('socket.io'); + , sockets = io.listen(80); + + sockets.on('connection', function (socket) { + sockets.emit('this', { will: 'be received by everyone'); + + socket.on('private message', function (from, msg) { + console.log('I received a private message by ', from, ' saying ', msg); + }); + + socket.on('disconnect', function () { + sockets.emit('user disconnected'); + }); + }); + +### Storing data associated to a client + +Sometimes it's necessary to store data associated with a client that's +necessary for the duration of the session. + +#### Server side + + var io = require('socket.io') + , sockets = io.listen(80); + + sockets.on('connection', function (socket) { + socket.on('set nickname', function (name) { + socket.set('nickname', name, function () { socket.emit('ready'); }); + }); + + socket.on('msg', function () { + socket.get('nickname', function (name) { + console.log('Chat message by ', name); + }); + }); + }); + +#### Client side + + + +### Restricting yourself to a namespace + +If you have control over all the messages and events emitted for a particular +application, using the default `/` namespace works. + +If you want to leverage 3rd-party code, or produce code to share with others, +socket.io provides a way of namespacing a `socket`. + +This has the benefit of `multiplexing` a single connection. Instead of +socket.io using two `WebSocket` connections, it'll use one. + +The following example defines a socket that listens on '/chat' and one for +'/news': + +#### Server side + + var io = require('socket.io') + , sockets = io.listen(80); + + var chat = sockets + .for('/chat'); + .on('connection', function (socket) { + socket.emit('a message', { that: 'only', '/chat': 'will get' }); + chat.emit('a message', { everyone: 'in', '/chat': 'will get' }); + }); + + var news = sockets + .for('/news'); + .on('connection', function (socket) { + socket.emit('item', { news: 'item' }); + }); + +#### Client side: + + + +### Sending volatile messages. + +Sometimes certain messages can be dropped. Let's say you have an app that +shows realtime tweets for the keyword `bieber`. + +If a certain client is not ready to receive messages (because of network slowness +or other issues, or because he's connected through long polling and is in the +middle of a request-response cycle), if he doesn't receive ALL the tweets related +to bieber your application won't suffer. + +In that case, you might want to send those messages as volatile messages. + +#### Server side + + var io = require('socket.io') + , sockets = io.listen(80); + + sockets.on('connection', function (socket) { + var tweets = setInterval(function () { + getBieberTweet(function (tweet) { + socket.volatile.emit('bieber tweeet', tweet); + }); + }, 100); + + socket.on('disconnect', function () { + clearInterval(tweets); + }); + }); + +#### Client side + +In the client side, messages are received the same way whether they're volatile +or not. + +### Getting acknowledgements + +Sometimes, you might want to get a callback when the client confirmed the message +receiption. + +To do this, simply pass a function as the last parameter of `.send` or `.emit`. +What's more, you can also perform a manual acknowledgement, like in the example +below. Socket.IO won't perform a manual acknowledgement when the arity of the +function is `0` when you `emit` or `send`. + +#### Server side + + var io = require('socket.io') + , sockets = io.listen(80); + + sockets.on('connection', function (socket) { + socket.on('ferret', function (name, fn) { + fn('woot'); + }); + }); + +#### Client side + + + +### Using it just as a cross-browser WebSocket + +If you just want the WebSocket semantics, you can do that too. +Simply leverage `send` and listen on the `message` event: + +#### Server side + + var io = require('socket.io') + , sockets = io.listen(80); + +#### Client side + + + +### Changing configuration + +Configuration in socket.io is TJ-style: + +#### Server side + + var io = require('socket.io') + , sockets = io.listen(80); + + sockets.configure(function () { + sockets.set('transports', ['websocket', 'flashsocket', 'xhr-polling']); + }); + + sockets.configure('development', function () { + sockets.set('transports', ['websocket', 'xhr-polling']); + sockets.enable('log'); + }); + +## [API docs](http://socket.io/api.html) + +## License + +(The MIT License) + +Copyright (c) 2011 Guillermo Rauch <guillermo@learnboost.com> + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +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. diff --git a/index.js b/index.js new file mode 100644 index 0000000000..cc00c103e2 --- /dev/null +++ b/index.js @@ -0,0 +1,8 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +module.exports = require('./lib/socket.io'); diff --git a/lib/client b/lib/client new file mode 160000 index 0000000000..ec023737a4 --- /dev/null +++ b/lib/client @@ -0,0 +1 @@ +Subproject commit ec023737a4d54df1fe6e6f198cd87ed8677b6676 diff --git a/lib/logger.js b/lib/logger.js new file mode 100644 index 0000000000..8e156ebe5e --- /dev/null +++ b/lib/logger.js @@ -0,0 +1,96 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +const util = require('./util') + , toArray = util.toArray; + +/** + * Log levels. + */ + +const levels = [ + 'error' + , 'warn' + , 'info' + , 'debug' +]; + +/** + * Colors for log levels. + */ + +const colors = [ + 31 + , 33 + , 36 + , 90 +]; + +/** + * Pads the nice output to the longest log level. + */ + +function pad (str) { + var max = 0; + + for (var i = 0, l = levels.length; i < l; i++) + max = Math.max(max, levels[i].length); + + if (str.length < max) + return str + new Array(max - str.length + 1).join(' '); + + return str; +}; + +/** + * Logger (console). + * + * @api public + */ + +var Logger = module.exports = function (opts) { + opts = opts || {} + this.colors = false !== opts.colors; + this.level = 3; +}; + +/** + * Log method. + * + * @api public + */ + +Logger.prototype.log = function (type) { + var index = levels.indexOf(type); + + if (index > this.level) + return this; + + console.error.apply( + console + , [this.colors + ? ' \033[' + colors[index] + 'm' + pad(type) + ' -\033[39m' + : type + ':' + ].concat(toArray(arguments).slice(1)) + ); + + return this; +}; + +/** + * Generate methods. + */ + +levels.forEach(function (name) { + Logger.prototype[name] = function () { + this.log.apply(this, [name].concat(toArray(arguments))); + }; +}); diff --git a/lib/manager.js b/lib/manager.js new file mode 100644 index 0000000000..6822194c4f --- /dev/null +++ b/lib/manager.js @@ -0,0 +1,553 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +const http = require('http') + , https = require('https') + , fs = require('fs') + , qs = require('querystring') + , url = require('url') + , util = require('./util') + , store = require('./store') + , transports = require('./transports') + , Logger = require('./logger') + , Socket = require('./socket') + , MemoryStore = require('./stores/memory') + , SocketNamespace = require('./namespace'); + +/** + * Export the constructor. + */ + +exports = module.exports = Manager; + +/** + * Default transports. + */ + +var defaultTransports = exports.defaultTransports = [ + 'websocket' + , 'flashsocket' + , 'htmlfile' + , 'xhr-polling' + , 'jsonp-polling' +]; + +/** + * Inherited defaults. + */ + +var parent = module.parent.exports + , protocol = parent.protocol + , clientVersion = parent.clientVersion; + +/** + * Manager constructor. + * + * @param {HTTPServer} server + * @param {Object} options, optional + * @api public + */ + +function Manager (server) { + this.server = server; + this.namespaces = {}; + this.sockets = new SocketNamespace(this); + this.settings = { + origins: '*:*' + , log: true + , store: new MemoryStore + , logger: new Logger + , heartbeats: true + , resource: '/socket.io' + , transports: defaultTransports + , authorization: false + , 'log level': 3 + , 'close timeout': 15 + , 'heartbeat timeout': 15 + , 'heartbeat interval': 20 + , 'polling duration': 50 + , 'flash policy server': true + , 'destroy upgrade': true + }; + + // reset listeners + this.oldListeners = server.listeners('request'); + server.removeAllListeners('request'); + + var self = this; + + server.on('request', function (req, res) { + self.handleRequest(req, res); + }); + + server.on('upgrade', function (req, socket, head) { + self.handleUpgrade(req, socket, head); + }); + + this.log.info('socket.io started'); +}; + +/** + * Store accessor shortcut. + * + * @api public + */ + +Manager.prototype.__defineGetter__('store', function () { + var store = this.get('store'); + store.manager = this; + return store; +}); + +/** + * Logger accessor. + * + * @api public + */ + +Manager.prototype.__defineGetter__('log', function () { + if (this.disabled('log')) return; + + var logger = this.get('logger'); + logger.level = this.set('log level'); + + return logger; +}); + +/** + * Get settings. + * + * @api public + */ + +Manager.prototype.get = function (key) { + return this.settings[key]; +}; + +/** + * Set settings + * + * @api public + */ + +Manager.prototype.set = function (key, value) { + if (arguments.length == 1) return this.get(key); + this.settings[key] = value; + return this; +}; + +/** + * Enable a setting + * + * @api public + */ + +Manager.prototype.enable = function (key) { + this.settings[key] = true; + return this; +}; + +/** + * Disable a setting + * + * @api public + */ + +Manager.prototype.disable = function (key) { + this.settings[key] = false; + return this; +}; + +/** + * Checks if a setting is enabled + * + * @api public + */ + +Manager.prototype.enabled = function (key) { + return !!this.settings[key]; +}; + +/** + * Checks if a setting is disabled + * + * @api public + */ + +Manager.prototype.disabled = function (key) { + return !this.settings[key]; +}; + +/** + * Configure callbacks. + * + * @api public + */ + +Manager.prototype.configure = function (env, fn) { + if ('function' == typeof env) + env(); + else if (env == process.env.NODE_ENV) + fn(); + + return this; +}; + +/** + * Handles an HTTP request. + * + * @api private + */ + +Manager.prototype.handleRequest = function (req, res) { + var data = this.checkRequest(req); + + if (!data) { + this.log.debug('ignoring request outside socket.io namespace'); + + for (var i = 0, l = this.oldListeners.length; i < l; i++) + this.oldListeners[i].call(this, req, res); + + return; + } + + if (!data.transport && !data.protocol) { + if (data.path == '/socket.io.js') { + this.handleClientRequest(req, res); + } else { + res.writeHead(200); + res.end('Welcome to socket.io.'); + + this.log.info('unhandled socket.io url'); + } + + return; + } + + if (data.protocol != protocol) { + res.writeHead(500); + res.end('Protocol version not supported.'); + + this.log.info('client protocol version unsupported'); + } else { + // flag the connection + if (!req.connection.__io) + req.connection.__io = 1; + else + req.connection.__io++; + + if (data.id) { + this.handleHTTPRequest(data, req, res); + } else { + this.handleHandshake(data, req, res); + } + } +}; + +/** + * Handles an HTTP Upgrade. + * + * @api private + */ + +Manager.prototype.handleUpgrade = function (req, socket, head) { + var data = this.checkRequest(req) + , self = this; + + if (!data) { + if (this.enabled('destroy upgrade')) { + socket.end(); + this.log.debug('destroying non-socket.io upgrade'); + } + + return; + } + + req.head = head; + this.handleClient(data, req); +}; + +/** + * Handles a normal handshaken HTTP request (eg: long-polling) + * + * @api private + */ + +Manager.prototype.handleHTTPRequest = function (data, req, res) { + req.res = res; + this.handleClient(data, req); +}; + +/** + * Intantiantes a new client. + * + * @api private + */ + +Manager.prototype.handleClient = function (data, req) { + var socket = req.socket + , self = this; + + if (!socket.__ioTransport) + socket.__ioTransport = new transports[data.transport](this, data); + + var transport = socket.__ioTransport; + transport.pause(); + transport.request = req; + + if (!~this.get('transports').indexOf(data.transport)) { + transport.error('transport not supported', 'reconnect'); + return; + } + + if (!this.verifyOrigin(req)) { + transport.error('unauthorized'); + return; + } + + this.store.isHandshaken(data.id, function (err, handshaken) { + if (handshaken) { + if (data.query.disconnect) { + self.log.error('handle forced disconnection here'); + } else { + self.store.client(data.id).count(function (err, count) { + if (count == 1) + self.sockets.emit('connection', new Socket(self, data.id, '')); + + transport.resume(); + }); + } + } else { + transport.error('client not handshaken'); + } + }); +}; + +/** + * Serves the client. + * + * @api private + */ + +Manager.prototype.handleClientRequest = function (req, res) { + var self = this; + + function serve () { + if (!self.clientLength) + self.clientLength = Buffer.byteLength(self.client); + + var headers = { + 'Content-Type': 'application/javascript' + , 'Content-Length': self.clientLength + }; + + if (self.clientEtag) + headers.ETag = self.clientEtag; + + res.writeHead(200, headers); + res.end(self.client); + + self.log.debug('served client'); + }; + + if (!this.client) { + if (this.get('browser client')) { + this.client = this.get('browser client'); + this.clientEtag = this.get('browser client etag'); + + this.log.debug('caching custom client'); + + serve(); + } else { + var self = this; + + fs.readFile(__dirname + '/client/socket.io.min.js', function (err, data) { + if (err) { + res.writeHead(500); + res.end('Error serving socket.io client.'); + + self.log.warn('Can\'t cache socket.io client'); + return; + } + + self.client = data.toString(); + self.clientEtag = clientVersion; + self.log.debug('caching', clientVersion, 'client'); + + serve(); + }); + } + } +}; + +/** + * Handles a handshake request. + * + * @api private + */ + +Manager.prototype.handleHandshake = function (data, req, res) { + var self = this; + + function error (err) { + res.writeHead(500); + res.end('Handshake error'); + + self.log.warn('handshake error ' + err); + }; + + this.authorize(data, function (err, authorized) { + if (err) return error(err); + + self.log.info('handshake ' + (authorized ? 'authorized' : 'unauthorized')); + + if (authorized) { + self.store.handshake(data, function (err, id) { + if (err) return error(err); + + res.writeHead(200); + res.end([ + id + , self.get('heartbeat timeout') || '' + , self.get('close timeout') || '' + , self.transports(data).join(',') + ].join(':')); + }); + } else { + res.writeHead(403); + res.end('Handshake unauthorized'); + + self.log.info('handshake unauthorized'); + } + }) +}; + +/** + * Verifies the origin of a request. + * + * @api private + */ + +Manager.prototype.verifyOrigin = function (request) { + var origin = request.headers.origin + , origins = this.get('origins'); + + if (origin === 'null') origin = '*'; + + if (origins.indexOf('*:*') !== -1) { + return true; + } + + if (origin) { + try { + var parts = url.parse(origin); + + return + ~origins.indexOf(parts.host + ':' + parts.port) || + ~origins.indexOf(parts.host + ':*') || + ~origins.indexOf('*:' + parts.port); + } catch (ex) {} + } + + return false; +}; + +/** + * Performs authentication. + * + * @param Object client request data + * @api private + */ + +Manager.prototype.authorize = function (data, fn) { + if (this.get('authorization')) { + var self = this; + + this.get('authorization').call(this, data, function (err, authorized) { + self.log.debug('client ' + authorized ? 'authorized' : 'unauthorized'); + fn(err, authorized); + }) + } else { + this.log.debug('client authorized'); + fn(null, true); + } + + return this; +}; + +/** + * Retrieves the transports adviced to the user. + * + * @api private + */ + +Manager.prototype.transports = function (data) { + var transp = this.get('transports') + , ret = []; + + for (var i = 0, l = transp.length; i < l; i++) { + var transport = transp[i]; + + if (transport) { + if (!transport.checkClient || !transport.checkClient(data)) { + ret.push(transport); + } + } + } + + return ret; +}; + +/** + * Checks whether a request is a socket.io one. + * + * @return {Object} a client request data object or `false` + * @api private + */ + +Manager.prototype.checkRequest = function (req) { + var resource = this.get('resource') + , url = req.url; + + if (url.substr(0, resource.length) == resource) { + var path = url.substr(resource.length) + , pieces = path.match(/^\/([^\/]+)\/?([^\/]+)?\/?([^\/]+)?\/?$/); + + // client request data + var data = { + query: req.url.query ? qs.parse(req.url.query) : {} + , headers: req.headers + , request: req + , path: path + }; + + if (pieces) { + data.protocol = Number(pieces[1]); + data.transport = pieces[2]; + data.id = pieces[3]; + }; + + return data; + } + + return false; +}; + +/** + * Declares a socket namespace + */ + +Manager.prototype.for = function (nsp) { + if (this.namespaces[nsp]) + return this.namespaces[nsp]; + + return this.namespaces[nsp] = new SocketNamespace(this, nsp); +}; diff --git a/lib/namespace.js b/lib/namespace.js new file mode 100644 index 0000000000..423230daa2 --- /dev/null +++ b/lib/namespace.js @@ -0,0 +1,62 @@ + +/** + * Module dependencies. + */ + +const EventEmitter = process.EventEmitter; + +/** + * Exports the constructor. + */ + +exports = module.exports = SocketNamespace; + +/** + * Constructor. + * + * @api public. + */ + +function SocketNamespace (mgr, nsp) { + this.manager = mgr; + this.nsp = nsp || ''; + this.flags = {}; +}; + +/** + * Inherits from EventEmitter. + */ + +SocketNamespace.prototype.__proto__ = EventEmitter.prototype; + +/** + * JSON message flag. + * + * @api public + */ + +SocketNamespace.prototype.__defineGetter__('json', function () { + this.flags.json = true; + return this; +}); + +/** + * Volatile message flag. + * + * @api public + */ + +SocketNamespace.prototype.__defineGetter__('volatile', function () { + this.flags.volatile = true; + return this; +}); + +/** + * Writes to everyone. + * + * @api public + */ + +SocketNamespace.prototype.send = function () { + this.flags = {}; +}; diff --git a/lib/parser.js b/lib/parser.js new file mode 100644 index 0000000000..fc9d2aeb06 --- /dev/null +++ b/lib/parser.js @@ -0,0 +1,243 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +/** + * Packet types. + */ + +const packets = exports.packets = [ + 'disconnect' + , 'connect' + , 'heartbeat' + , 'message' + , 'json' + , 'event' + , 'ack' + , 'error' +]; + +/** + * Errors reasons. + */ + +const reasons = exports.reasons = [ + 'transport not supported' + , 'client not handshaken' + , 'unauthorized' +]; + +/** + * Errors advice. + */ + +const advice = exports.advice = [ + 'reconnect' +]; + +/** + * Encodes a packet. + * + * @api private + */ + +exports.encodePacket = function (packet) { + var type = packets.indexOf(packet.type) + , id = packet.id || '' + , endpoint = packet.endpoint || '' + , ack = packet.ack + , data = null; + + switch (packet.type) { + case 'error': + var reason = packet.reason ? reasons.indexOf(packet.reason) : '' + , adv = packet.advice ? advice.indexOf(packet.advice) : '' + + if (reason !== '' || adv !== '') + data = reason + (adv !== '' ? ('+' + adv) : '') + + break; + + case 'message': + if (packet.data !== '') + data = packet.data; + break; + + case 'event': + var params = packet.args && packet.args.length + ? JSON.stringify(packet.args) : ''; + data = packet.name + (params !== '' ? ('\ufffd' + params) : ''); + break; + + case 'json': + data = JSON.stringify(packet.data); + break; + + case 'connect': + if (packet.qs) + data = packet.qs; + break; + + case 'ack': + data = packet.ackId + + (packet.args && packet.args.length + ? '+' + JSON.stringify(packet.args) : ''); + break; + + case 'heartbeat': + case 'disconect': + break; + } + + // construct packet with required fragments + var encoded = [ + type + , id + (ack == 'data' ? '+' : '') + , endpoint + ]; + + // data fragment is optional + if (data !== null && data !== undefined) + encoded.push(data); + + return encoded.join(':'); +}; + +/** + * Encodes multiple messages (payload). + * + * @param {Array} messages + * @api private + */ + +exports.encodePayload = function (packets) { + var decoded = ''; + + if (packets.length == 1) + return packets[0]; + + for (var i = 0, l = packets.length; i < l; i++) { + var packet = packets[i]; + decoded += '\ufffd' + packet.length + '\ufffd' + packets[i] + } + + return decoded; +}; + +/** + * Decodes a packet + * + * @api private + */ + +var regexp = /^([^:]+):([0-9]+)?(\+)?:([^:]+)?:?(.*)?$/; + +exports.decodePacket = function (data) { + var pieces = data.match(regexp); + + if (!pieces) return {}; + + var id = pieces[2] || '' + , data = pieces[5] || '' + , packet = { + type: packets[pieces[1]] + , endpoint: pieces[4] || '' + }; + + // whether we need to acknowledge the packet + if (id) { + packet.id = id; + if (pieces[3]) + packet.ack = 'data'; + else + packet.ack = true; + } + + // handle different packet types + switch (packet.type) { + case 'error': + var pieces = data.split('+'); + packet.reason = reasons[pieces[0]] || ''; + packet.advice = advice[pieces[1]] || ''; + break; + + case 'message': + packet.data = data || ''; + break; + + case 'event': + var pieces = data.match(/([^\ufffd]+)(\ufffd)?(.*)/); + packet.name = pieces[1] || ''; + packet.args = []; + + if (pieces[3]) { + try { + packet.args = JSON.parse(pieces[3]); + } catch (e) { } + } + break; + + case 'json': + try { + packet.data = JSON.parse(data); + } catch (e) { } + break; + + case 'connect': + packet.qs = data || ''; + break; + + case 'ack': + var pieces = data.match(/^([0-9]+)(\+)?(.*)/); + if (pieces) { + packet.ackId = pieces[1]; + packet.args = []; + + if (pieces[3]) { + try { + packet.args = pieces[3] ? JSON.parse(pieces[3]) : []; + } catch (e) { } + } + } + break; + + case 'disconnect': + case 'heartbeat': + break; + }; + + return packet; +}; + +/** + * Decodes data payload. Detects multiple messages + * + * @return {Array} messages + * @api public + */ + +exports.decodePayload = function (data) { + if (data[0] == '\ufffd') { + var ret = []; + + for (var i = 1, length = ''; i < data.length; i++) { + if (data[i] == '\ufffd') { + ret.push(exports.decodePacket(data.substr(i + 1).substr(0, length))); + i += Number(length); + } else { + length += data[i]; + } + } + + return ret; + } else { + return [exports.decodePacket(data)]; + } +}; diff --git a/lib/socket.io.js b/lib/socket.io.js new file mode 100644 index 0000000000..6b6de1ba1b --- /dev/null +++ b/lib/socket.io.js @@ -0,0 +1,106 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +const http = require('http') + , https = require('https') + , client = require('./client/lib/io'); + +/** + * Version. + */ + +exports.version = '0.7.0'; + +/** + * Supported protocol version. + */ + +exports.protocol = 1; + +/** + * Client that we serve. + */ + +exports.clientVersion = client.version; + +/** + * Attaches a manager + * + * @api public + */ + +exports.listen = function (server, options) { + if ('undefined' == typeof server) { + // create a server that listens on port 80 + server = 80; + } + + if ('number' == typeof server) { + // if a port number is passed + var port = server; + + if (options && options.key) + server = https.createServer(options, server); + else + server = http.createServer(); + + // default response + server.on('request', function (req, res) { + res.writeHead(200); + res.end('Welcome to socket.io.'); + }); + + server.listen(port); + } + + // otherwise assume a http/s server + return new exports.Manager(server); +}; + +/** + * Manager constructor. + * + * @api public + */ + +exports.Manager = require('./manager'); + +/** + * Transport constructor. + * + * @api public + */ + +exports.Transport = require('./transport'); + +/** + * Socket constructor. + * + * @api public + */ + +exports.Socket = require('./socket'); + +/** + * Store constructor. + * + * @api public + */ + +exports.Store = require('./store'); + +/** + * Parser. + * + * @api public + */ + +exports.parser = require('./parser'); diff --git a/lib/socket.js b/lib/socket.js new file mode 100644 index 0000000000..7d1d9108c0 --- /dev/null +++ b/lib/socket.js @@ -0,0 +1,225 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +const util = require('./util') + , EventEmitter = process.EventEmitter; + +/** + * Export the constructor. + */ + +exports = module.exports = Socket; + +/** + * Reserved event names. + */ + +const events = { + message: 1 + , connect: 1 + , disconnect: 1 + , open: 1 + , close: 1 + , error: 1 + , retry: 1 + , reconnect: 1 +}; + +/** + * ReadyStates -> event map. + */ + +const readyStates = { + 0: 'disconnect' + , 1: 'connect' + , 2: 'open' + , 3: 'close' +}; + +/** + * Socket constructor. + * + * @api public + */ + +function Socket (manager, id, nsp, readonly) { + this.id = id; + this.namespace = nsp; + this.manager = manager; + this.flags = {}; + this.rs = 1; + this.packets = 0; + + if (!readonly) + this.store.on('message:' + id, function () { + + }); +}; + +/** + * Inherits from EventEmitter. + */ + +Socket.prototype.__proto__ = EventEmitter.prototype; + +/** + * readyState getter. + * + * @api private + */ + +Socket.prototype.__defineGetter__('readyState', function (st) { + return this.rs; +}); + +/** + * readyState setter. + * + * @api private + */ + +Socket.prototype.__defineSetter__('readyState', function (state) { + this.rs = state; + this.emit('readyState', state); + this.emit(readyStates[state]); +}); + +/** + * Accessor shortcut for the store. + * + * @api private + */ + +Socket.prototype.__defineGetter__('store', function () { + return this.manager.store; +}); + +/** + * JSON message flag. + * + * @api public + */ + +Socket.prototype.__defineGetter__('json', function () { + this.flags.json = true; + return this; +}); + +/** + * Volatile message flag. + * + * @api public + */ + +Socket.prototype.__defineGetter__('volatile', function () { + this.flags.volatile = true; + return this; +}); + +/** + * Transmits a packet. + * + * @api private + */ + +Socket.prototype.packet = function (msg, volatile) { + if (volatile) { + this.store.publish('volatile:' + this.id, msg); + } else { + this.store.client(this.id).publish(msg); + } + + return this; +}; + +/** + * Stores data for the client. + * + * @api public + */ + +Socket.prototype.set = function (key, value, fn) { + this.store.client(this.id).set(key, value, fn); + return this; +}; + +/** + * Retrieves data for the client + * + * @api public + */ + +Socket.prototype.get = function (key, fn) { + this.store.client(this.id).get(key, fn); + return this; +}; + +/** + * Kicks client + * + * @api public + */ + +Socket.prototype.disconnect = function () { + this.packet({ type: 'disconnect' }); + return this; +}; + +/** + * Send a message. + * + * @api public + */ + +Socket.prototype.send = function (data, fn) { + var packet = { + type: this.flags.json ? 'json' : 'message' + , data: data + }; + + if (fn) { + packet.id = ++this.packets; + packet.ack = fn.length ? 'data' : true; + } + + this.packet(packet, this.flags.volatile); + this.flags = {}; + return this; +}; + +/** + * Emit override for custom events. + * + * @api public + */ + +Socket.prototype.emit = function (ev) { + if (events.ev) + return EventEmitter.protype.emit.apply(this, arguments); + + var args = util.toArray(arguments).slice(1) + , lastArg = args[args.length - 1]; + + // prepare packet to send + var packet = { + type: 'event' + , args: args + }; + + if ('function' == typeof lastArg) { + packet.id = ++this.packets; + packet.ack = lastArg.length ? 'data' : true; + } + + this.packet(packet, this.flags.volatile); + this.flags = {}; + return this; +}; diff --git a/lib/store.js b/lib/store.js new file mode 100644 index 0000000000..2887628e1d --- /dev/null +++ b/lib/store.js @@ -0,0 +1,55 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +const EventEmitter = process.EventEmitter; + +/** + * Expose the constructor. + */ + +exports = module.exports = Store; + +/** + * Store interface + * + * @api public + */ + +function Store () {}; + +/** + * Inherits from EventEmitter + */ + +Store.prototype.__proto__ = EventEmitter.prototype; + +/** + * Log accessor. + * + * @api public + */ + +Store.prototype.__defineGetter__('log', function () { + return this.manager.log; +}); + +/** + * Client. + * + * @api public + */ + +Store.Client = function (store, id) { + this.store = store; + this.id = id; + this.buffer = []; + this.dict = {}; +}; diff --git a/lib/stores/memory.js b/lib/stores/memory.js new file mode 100644 index 0000000000..777d2bc288 --- /dev/null +++ b/lib/stores/memory.js @@ -0,0 +1,252 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +const crypto = require('crypto') + , Store = require('../store') + +/** + * Exports the constructor. + */ + +exports = module.exports = Memory; +Memory.Client = Client; + +/** + * Memory store + * + * @api public + */ + +function Memory (opts) { + this.handshaken = []; + this.clients = {}; +}; + +/** + * Inherits from Store. + */ + +Memory.prototype.__proto__ = Store.prototype; + +/** + * Handshake a client. + * + * @param {Object} client request object + * @param {Function} callback + * @api public + */ + +Memory.prototype.handshake = function (data, fn) { + var id = this.generateId(); + this.handshaken.push(id); + fn(null, id); + return this; +}; + +/** + * Checks if a client is handshaken. + * + * @api public + */ + +Memory.prototype.isHandshaken = function (id, fn) { + fn(null, ~this.handshaken.indexOf(id)); + return this; +}; + +/** + * Generates a random id. + * + * @api private + */ + +Memory.prototype.generateId = function () { + var rand = String(Math.random() * Math.random() * Date.now()); + return crypto.createHash('md5').update(rand).digest('hex'); +}; + +/** + * Retrieves a client store instance. + * + * @api public + */ + +Memory.prototype.client = function (id) { + if (!this.clients[id]) { + this.clients[id] = new Memory.Client(this, id); + this.log.debug('initializing client store for', id); + } + + return this.clients[id]; +}; + +/** + * Destroys a client store. + * + * @api public + */ + +Memory.prototype.destroy = function (id) { + this.clients[id].destroy(); + this.clients[id] = null; +}; + +/** + * Simple publish + * + * @api public + */ + +Memory.prototype.publish = function (ev, data) { + this.emit(ev, data); + return this; +}; + +/** + * Simple subscribe + * + * @api public + */ + +Memory.prototype.subscribe = function (chn, fn) { + this.on(chn, fn); + return this; +}; + +/** + * Simple unsubscribe + * + * @api public + */ + +Memory.prototype.unsubscribe = function (chn) { + this.removeAllListeners(chn); +}; + +/** + * Client constructor + * + * @api private + */ + +function Client () { + Store.Client.apply(this, arguments); + this.reqs = 0; + this.paused = true; +}; + +/** + * Inherits from Store.Client + */ + +Client.prototype.__proto__ = Store.Client; + +/** + * Counts transport requests. + * + * @api public + */ + +Client.prototype.count = function (fn) { + fn(null, ++this.reqs); + return this; +}; + +/** + * Sets up queue consumption + * + * @api public + */ + +Client.prototype.consume = function (fn) { + this.paused = false; + + if (this.buffer.length) { + fn(this.buffer, null); + this.buffer = []; + } else { + this.consumer = fn; + } + + return this; +}; + +/** + * Publishes a message to be sent to the client. + * + * @String encoded message + * @api public + */ + +Client.prototype.publish = function (msg) { + if (this.paused) { + this.buffer.push(msg); + } else { + this.consumer(null, msg); + } + + return this; +}; + +/** + * Pauses the stream. + * + * @api public + */ + +Client.prototype.pause = function () { + this.paused = true; + return this; +}; + +/** + * Destroys the client. + * + * @api public + */ + +Client.prototype.destroy = function () { + this.buffer = null; +}; + +/** + * Gets a key + * + * @api public + */ + +Client.prototype.get = function (key, fn) { + fn(null, this.dict[key]); + return this; +}; + +/** + * Sets a key + * + * @api public + */ + +Client.prototype.set = function (key, value, fn) { + this.dict[key] = value; + fn(null); + return this; +}; + +/** + * Emits a message incoming from client. + * + * @api private + */ + +Client.prototype.onMessage = function (msg) { + this.store.emit('message:' + id, msg); +}; + diff --git a/lib/transport.js b/lib/transport.js new file mode 100644 index 0000000000..d5f9003c7f --- /dev/null +++ b/lib/transport.js @@ -0,0 +1,376 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +const parser = require('./parser'); + +/** + * Expose the constructor. + */ + +exports = module.exports = Transport; + +/** + * Transport constructor. + * + * @api public + */ + +function Transport (mng, data) { + this.manager = mng; + this.id = data.id; + this.paused = true; +}; + +/** + * Sets the corresponding request object. + */ + +Transport.prototype.__defineSetter__('request', function (req) { + this.log.debug('setting request'); + this.handleRequest(req); +}); + +/** + * Access the logger. + * + * @api public + */ + +Transport.prototype.__defineGetter__('log', function () { + return this.manager.log; +}); + +/** + * Access the store. + * + * @api public + */ + +Transport.prototype.__defineGetter__('store', function () { + return this.manager.store; +}); + +/** + * Handles a request when it's set. + * + * @api private + */ + +Transport.prototype.handleRequest = function (req) { + this.req = req; + + if (req.method == 'GET') { + this.socket = req.socket; + this.open = true; + + var self = this; + + this.log.debug('publishing that', this.id, 'connected'); + this.store.publish('open:' + this.id, function () { + self.store.once('open:' + this.id, function () { + self.log.debug('request for existing session connection change'); + self.end(); + }); + + if (!self.paused) + self.subscribe(); + }); + + if (!req.socket.__ioHandler) { + // add a handler only once per socket + this.socket.setNoDelay(true); + this.socket.on('end', this.onSocketEnd.bind(this)); + this.socket.on('error', this.onSocketError.bind(this)); + this.socket.__ioHandler = true; + } + } +}; + +/** + * Called when the connection dies + * + * @api private + */ + +Transport.prototype.onSocketEnd = function () { + if (this.open) { + this.setCloseTimeout(); + } +}; + +/** + * Called when the connection has an error. + * + * @api private + */ + +Transport.prototype.onSocketError = function (err) { + if (this.open) { + this.end(); + this.socket.destroy(); + } + + this.log.info('socket error, should set close timeout'); +}; + +/** + * Sets the close timeout. + */ + +Transport.prototype.setCloseTimeout = function () { + if (!this.closeTimeout) { + var self = this; + + this.closeTimeout = setTimeout(function () { + self.log.debug('fired close timeout for client', self.id); + self.closeTimeout = null; + self.socket.end(); + self.end(); + }, this.manager.get('close timeout') * 1000); + + this.log.debug('set close timeout for client', this.id); + } +}; + +/** + * Clears the close timeout. + */ + +Transport.prototype.clearCloseTimeout = function () { + if (this.closeTimeout) { + clearTimeout(this.closeTimeout); + this.closeTimeout = null; + + this.log.debug('cleared close timeout for client', this.id); + } +}; + +/** + * Sets the heartbeat timeout + */ + +Transport.prototype.setHeartbeatTimeout = function () { + if (!this.heartbeatTimeout) { + var self = this; + + this.heartbeatTimeout = setTimeout(function () { + self.log.debug('fired heartbeat timeout for client', self.id); + self.heartbeatTimeout = null; + self.end(); + }, this.manager.get('hearbeat timeout') * 1000); + + this.log.debug('set heartbeat timeout for client', this.id); + } +}; + +/** + * Clears the heartbeat timeout + * + * @param text + */ + +Transport.prototype.clearHeartbeatTimeout = function () { + if (this.heartbeatTimeout) { + clearTimeout(this.heartbeatTimeout); + this.heartbeatTimeout = null; + this.log.debug('cleared heartbeat timeout for client', this.id); + } +}; + +/** + * Sets the heartbeat interval. To be called when a connection opens and when + * a heartbeat is received. + * + * @api private + */ + +Transport.prototype.setHeartbeatInterval = function () { + if (!this.heartbeatTimeout) { + var self = this; + + this.heartbeatInterval = setTimeout(function () { + self.log.debug('emitting heartbeat for client', self.id); + self.packet({ type: 'heartbeat' }); + }, this.manager.get('heartbeat interval') * 1000); + + this.log.debug('set heartbeat interval for client', this.id); + } +}; + +/** + * Sends a heartbeat + * + * @api private + */ + +Transport.prototype.hearbeat = function () { +}; + + +/** + * Clears the heartbeat interval + * + * @api private + */ + +Transport.prototype.clearHeartbeatInterval = function () { + if (this.heartbeatInterval) { + clearTimeout(this.heartbeatInterval); + this.heartbeatInterval = null; + this.log.debug('cleared heartbeat interval for client', this.id); + } +}; + +/** + * Finishes the connection and makes sure client doesn't reopen + * + * @api private + */ + +Transport.prototype.disconnect = function () { + this.packet({ type: 'disconnect' }); + this.end(); + + return this; +}; + +/** + * Cleans up the connection, considers the client disconnected. + * + * @api private + */ + +Transport.prototype.end = function () { + this.log.info('ending socket'); + + this.clearCloseTimeout(); + this.clearHeartbeatTimeout(); + this.clearHeartbeatInterval(); + + if (this.subscribed) { + this.unsubscribe(); + this.store.destroy(this.id); + } + + this.open = false; +}; + +/** + * Signals that the transport can start flushing buffers. + * + * @api public + */ + +Transport.prototype.resume = function () { + this.paused = false; + this.subscribe(); + return this; +}; + +/** + * Signals that the transport should pause and buffer data. + * + * @api public + */ + +Transport.prototype.pause = function () { + this.paused = true; + return this; +}; + +/** + * Writes an error packet with the specified reason and advice. + * + * @param {Number} advice + * @param {Number} reason + * @api public + */ + +Transport.prototype.error = function (reason, advice) { + this.packet({ + type: 'error' + , reason: reason + , advice: advice + }); + + this.log.warn(reason, advice ? ('client should ' + advice) : ''); + this.end(); +}; + +/** + * Write a packet. + * + * @api public + */ + +Transport.prototype.packet = function (obj) { + return this.write(parser.encodePacket(obj)); +}; + +/** + * Subscribe client. + * + * @api private + */ + +Transport.prototype.subscribe = function () { + if (!this.subscribed) { + this.log.debug('subscribing', this.id); + + var self = this; + + // subscribe to buffered + normal messages + this.store.client(this.id).consume(function (payload, packet) { + if (payload) { + self.payload(payload.map(function (packet) { + return parser.encodePacket(packet); + })); + } else { + self.write(packet); + } + }); + + // subscribe to volatile messages + self.store.subscribe('volatile:' + this.id, function (packet) { + self.writeVolatile(packet); + }); + + this.subscribed = true; + } +}; + +/** + * Unsubscribe client. + * + * @api private + */ + +Transport.prototype.unsubscribe = function () { + if (this.subscribed) { + this.log.info('unsubscribing', this.id); + + this.store.unsubscribe('volatile:' + this.id); + this.store.client(this.id).pause(); + this.subscribed = false; + } +}; + +/** + * Handles a message + * + * @param {Object} message object + * @api private + */ + +Transport.prototype.onMessage = function (msg) { + this.store.publish('message:' + this.id, msg); +}; + diff --git a/lib/transports/htmlfile.js b/lib/transports/htmlfile.js new file mode 100644 index 0000000000..db69dba8df --- /dev/null +++ b/lib/transports/htmlfile.js @@ -0,0 +1,68 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Module requirements. + */ + +const HTTPTransport = require('./http'); + +/** + * Export the constructor. + */ + +exports = module.exports = HTMLFile; + +/** + * HTMLFile transport constructor. + * + * @api public + */ + +function HTMLFile (data, request) { + HTTPTransport.call(this, data, request); +}; + +/** + * Inherits from Transport. + */ + +HTMLFile.prototype.__proto__ = HTTPTransport.prototype; + +/** + * Handles the request. + * + * @api private + */ + +HTMLFile.prototype.handleRequest = function (req) { + HTTPTransport.prototype.handleRequest.call(this, req); + + if (req.method == 'GET') { + req.res.writeHead(200, { + 'Content-Type': 'text/html', + 'Connection': 'keep-alive', + 'Transfer-Encoding': 'chunked' + }); + + req.res.write('' + new Array(245).join(' ')); + } +}; + + +/** + * Performs the write. + * + * @api private + */ + +HTMLFile.prototype.doWrite = function (data) { + data = ''; + + this.response.write(data); + this.log.debug('writing', data); +}; diff --git a/lib/transports/http-polling.js b/lib/transports/http-polling.js new file mode 100644 index 0000000000..ce33094ecd --- /dev/null +++ b/lib/transports/http-polling.js @@ -0,0 +1,128 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Module requirements. + */ + +const HTTPTransport = require('./http'); + +/** + * Exports the constructor. + */ + +exports = module.exports = HTTPPolling; + +/** + * HTTP polling constructor. + * + * @api public. + */ + +function HTTPPolling (mng, data) { + HTTPTransport.call(this, mng, data); +}; + +/** + * Inherits from HTTPTransport. + * + * @api public. + */ + +HTTPPolling.prototype.__proto__ = HTTPTransport.prototype; + +/** + * Removes heartbeat timeouts for polling. + */ + +HTTPPolling.prototype.setHeartbeatTimeout = function () { + return; +}; + +/** + * Removes the heartbeat timeouts for polling. + */ + +HTTPPolling.prototype.clearHeartbeatTimeout = function () { + return; +}; + +/** + * Handles a request + * + * @api private + */ + +HTTPPolling.prototype.handleRequest = function (req) { + HTTPTransport.prototype.handleRequest.call(this, req); + + if (req.method == 'GET') { + var self = this; + + this.pollTimeout = setTimeout(function () { + self.write(''); + self.log.debug('polling closed due to exceeded duration'); + }, this.manager.get('polling duration') * 1000); + + this.log.debug('setting poll timeout'); + } +}; + +/** + * Performs a write. + * + * @api private. + */ + +HTTPPolling.prototype.write = function (data, close) { + if (this.pollTimeout) { + clearTimeout(this.pollTimeout); + this.pollTimeout = null; + } + + this.doWrite(data); + this.response.end(); + this.setCloseTimeout(); + this.unsubscribe(); + this.open = false; +}; + +/** + * Performs a volatile write + * + * @api private + */ + +HTTPPolling.prototype.writeVolatile = HTTPPolling.prototype.write; + +/** + * Handles data payload. + * + * @api private + */ + +HTTPPolling.prototype.onData = function (data) { + var messages = parser.decodeMany(data); + + for (var i = 0, l = messages.length; i < l; i++) { + this.onMessage(messages[i]); + } +}; + +/** + * Override end. + * + * @api private + */ + +HTTPPolling.prototype.end = function () { + if (this.open) + this.response.end(); + + return HTTPTransport.prototype.end.call(this); +}; + diff --git a/lib/transports/http.js b/lib/transports/http.js new file mode 100644 index 0000000000..82a02f5cd7 --- /dev/null +++ b/lib/transports/http.js @@ -0,0 +1,70 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Module requirements. + */ + +const Transport = require('../transport') + , parser = require('../parser'); + +/** + * Export the constructor. + */ + +exports = module.exports = HTTPTransport; + +/** + * HTTP interface constructor. For all non-websocket transports. + * + * @api public + */ + +function HTTPTransport (mng, data) { + Transport.call(this, mng, data); +}; + +/** + * Inherits from Transport. + */ + +HTTPTransport.prototype.__proto__ = Transport.prototype; + +/** + * Handles a request. + * + * @api private + */ + +HTTPTransport.prototype.handleRequest = function (req) { + if (req.method == 'POST') { + var buffer = '' + , res = req.res; + + res.on('data', function (data) { + buffer += data; + }); + + res.on('end', function () { + self.onData(buffer); + }); + } else { + this.response = req.res; + this.open = true; + Transport.prototype.handleRequest.call(this, req); + } +}; + +/** + * Writes a payload of messages + * + * @api private + */ + +HTTPTransport.prototype.payload = function (msgs) { + this.write(parser.encodePayload(msgs)); +}; diff --git a/lib/transports/index.js b/lib/transports/index.js new file mode 100644 index 0000000000..79c12a005d --- /dev/null +++ b/lib/transports/index.js @@ -0,0 +1,11 @@ + +/** + * Export transports. + */ + +module.exports = { + websocket: require('./websocket') + , htmlfile: require('./htmlfile') + , 'xhr-polling': require('./xhr-polling') + , 'jsonp-polling': require('./jsonp-polling') +}; diff --git a/lib/transports/jsonp-polling.js b/lib/transports/jsonp-polling.js new file mode 100644 index 0000000000..e114e37f0b --- /dev/null +++ b/lib/transports/jsonp-polling.js @@ -0,0 +1,52 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Module requirements. + */ + +const HTTPPolling = require('./http-polling'); + +/** + * Export the constructor. + */ + +exports = module.exports = JSONPPolling; + +/** + * HTTP interface constructor. Interface compatible with all transports that + * depend on request-response cycles. + * + * @api public + */ + +function JSONPPolling (data, request) { + HTTPPolling.call(this, data, request); +}; + +/** + * Inherits from Transport. + */ + +JSONPPolling.prototype.__proto__ = HTTPPolling.prototype; + +/** + * Performs the write. + * + * @api private + */ + +JSONPPolling.prototype.doWrite = function (data) { + this.response.writeHead(200, { + 'Content-Type': 'text/javascript; charset=UTF-8' + , 'Content-Length': Buffer.byteLength(data) + , 'Connection': 'Keep-Alive' + }); + + this.response.write(data); + this.log.debug('writing', data); +}; diff --git a/lib/transports/websocket.js b/lib/transports/websocket.js new file mode 100644 index 0000000000..3e20e066d4 --- /dev/null +++ b/lib/transports/websocket.js @@ -0,0 +1,64 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Module requirements. + */ + +const Transport = require('../transport'); + +/** + * Export the constructor. + */ + +exports = module.exports = WebSocket; + +/** + * HTTP interface constructor. Interface compatible with all transports that + * depend on request-response cycles. + * + * @api public + */ + +function WebSocket (data, request) { + Transport.call(this, data, request); +}; + +/** + * Inherits from Transport. + */ + +WebSocket.prototype.__proto__ = Transport.prototype; + +/** + * Writes a payload. + * + * @api private + */ + +WebSocket.prototype.payload = function (msgs) { + for (var i = 0, l = msgs.length; i < l; i++) { + this.write(msgs[i]); + } + + return this; +}; + + +/** + * Override end + * + * @api private + */ + +WebSocket.prototype.end = function () { + if (this.open) { + this.socket.end(); + } + + return Transport.prototype.end.call(this); +}; diff --git a/lib/transports/xhr-polling.js b/lib/transports/xhr-polling.js new file mode 100644 index 0000000000..c74929f090 --- /dev/null +++ b/lib/transports/xhr-polling.js @@ -0,0 +1,60 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Module requirements. + */ + +const HTTPPolling = require('./http-polling'); + +/** + * Export the constructor. + */ + +exports = module.exports = XHRPolling; + +/** + * Ajax polling transport. + * + * @api public + */ + +function XHRPolling (data, request) { + HTTPPolling.call(this, data, request); +}; + +/** + * Inherits from Transport. + */ + +XHRPolling.prototype.__proto__ = HTTPPolling.prototype; + +/** + * Frames data prior to write. + * + * @api private + */ + +XHRPolling.prototype.doWrite = function (data) { + var origin = this.req.headers.origin + , headers = { + 'Content-Type': 'text/plain; charset=UTF-8' + , 'Content-Length': Buffer.byteLength(data) + , 'Connection': 'Keep-Alive' + }; + + // https://developer.mozilla.org/En/HTTP_Access_Control + headers['Access-Control-Allow-Origin'] = '*'; + + if (this.req.headers.cookie) { + headers['Access-Control-Allow-Credentials'] = 'true'; + } + + this.response.writeHead(200, headers); + this.response.write(data); + this.log.debug('writing', data); +}; diff --git a/lib/util.js b/lib/util.js new file mode 100644 index 0000000000..25f31f2bd8 --- /dev/null +++ b/lib/util.js @@ -0,0 +1,25 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +/** + * Converts an enumerable to an array. + * + * @api public + */ + +exports.toArray = function (enu) { + var arr = []; + + for (var i = 0, l = enu.length; i < l; i++) + arr.push(enu[i]); + + return arr; +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000000..42cb8244cb --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "socket.io-node", + , "version": "0.7.0", + , "description": "WebSockets everywhere", + , "keywords": ["websocket", "socket", "realtime"], + , "author": "Guillermo Rauch ", + , "contributors": [ + { "name": "Guillermo Rauch", "email": "rauchg@gmail.com" } + , { "name": "Arnout Kazemier", "email": "info@3rd-eden.com" } + ] + , "dependencies": {} + , "main": "index" + , "engines": { "node": "0.4.x" } +} diff --git a/support/expresso b/support/expresso new file mode 160000 index 0000000000..1d2c78ab4b --- /dev/null +++ b/support/expresso @@ -0,0 +1 @@ +Subproject commit 1d2c78ab4b0b7dc5655b313f54167bcb3df53f76 diff --git a/support/should.js b/support/should.js new file mode 160000 index 0000000000..c05fac17d9 --- /dev/null +++ b/support/should.js @@ -0,0 +1 @@ +Subproject commit c05fac17d9894f1daeefded4db6609f962424831 diff --git a/support/socket.io-node-client b/support/socket.io-node-client new file mode 160000 index 0000000000..f6c5f54cc0 --- /dev/null +++ b/support/socket.io-node-client @@ -0,0 +1 @@ +Subproject commit f6c5f54cc055d29b843cef5e4095ed83b6f50ef0 diff --git a/test/common.js b/test/common.js new file mode 100644 index 0000000000..de5173b4f0 --- /dev/null +++ b/test/common.js @@ -0,0 +1,67 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Test dependencies. + */ + +const io = require('socket.io') + , should = module.exports = require('should') + , http = require('http') + , https = require('https'); + +/** + * Get utility. + */ + +get = function (opts, fn) { + opts = opts || {}; + opts.path = opts.path.replace(/{protocol}/g, io.protocol); + opts.headers = { + 'Host': 'localhost' + , 'Connection': 'Keep-Alive' + }; + + var req = (opts.secure ? https : http).request(opts, function (res) { + var buf = ''; + + res.on('data', function (chunk) { + buf += chunk; + }); + + res.on('end', function () { + fn(res, buf); + }); + }); + + req.end(); + return req; +}; + +/** + * Handshake utility + */ + +handshake = function (port, fn) { + get({ + port: port + , path: '/socket.io/{protocol}' + }, function (res, data) { + fn.apply(null, data.split(':')); + }); +}; + +/** + * Silence logging. + */ + +var old = io.listen; + +io.listen = function () { + console.log(''); + return old.apply(this, arguments); +}; diff --git a/test/fixtures/cert.crt b/test/fixtures/cert.crt new file mode 100644 index 0000000000..5883cd4496 --- /dev/null +++ b/test/fixtures/cert.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDXTCCAkWgAwIBAgIJAMUSOvlaeyQHMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTAxMTE2MDkzMjQ5WhcNMTMxMTE1MDkzMjQ5WjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAz+LXZOjcQCJq3+ZKUFabj71oo/ex/XsBcFqtBThjjTw9CVEVwfPQQp4X +wtPiB204vnYXwQ1/R2NdTQqCZu47l79LssL/u2a5Y9+0NEU3nQA5qdt+1FAE0c5o +exPimXOrR3GWfKz7PmZ2O0117IeCUUXPG5U8umhDe/4mDF4ZNJiKc404WthquTqg +S7rLQZHhZ6D0EnGnOkzlmxJMYPNHSOY1/6ivdNUUcC87awNEA3lgfhy25IyBK3QJ +c+aYKNTbt70Lery3bu2wWLFGtmNiGlQTS4JsxImRsECTI727ObS7/FWAQsqW+COL +0Sa5BuMFrFIpjPrEe0ih7vRRbdmXRwIDAQABo1AwTjAdBgNVHQ4EFgQUDnV4d6mD +tOnluLoCjkUHTX/n4agwHwYDVR0jBBgwFoAUDnV4d6mDtOnluLoCjkUHTX/n4agw +DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAFwV4MQfTo+qMv9JMiyno +IEiqfOz4RgtmBqRnXUffcjS2dhc7/z+FPZnM79Kej8eLHoVfxCyWRHFlzm93vEdv +wxOCrD13EDOi08OOZfxWyIlCa6Bg8cMAKqQzd2OvQOWqlRWBTThBJIhWflU33izX +Qn5GdmYqhfpc+9ZHHGhvXNydtRQkdxVK2dZNzLBvBlLlRmtoClU7xm3A+/5dddeP +AQHEPtyFlUw49VYtZ3ru6KqPms7MKvcRhYLsy9rwSfuuniMlx4d0bDR7TOkw0QQS +A0N8MGQRQpzl4mw4jLzyM5d5QtuGBh2P6hPGa0YQxtI3RPT/p6ENzzBiAKXiSfzo +xw== +-----END CERTIFICATE----- diff --git a/test/fixtures/key.key b/test/fixtures/key.key new file mode 100644 index 0000000000..f31ff3d944 --- /dev/null +++ b/test/fixtures/key.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAz+LXZOjcQCJq3+ZKUFabj71oo/ex/XsBcFqtBThjjTw9CVEV +wfPQQp4XwtPiB204vnYXwQ1/R2NdTQqCZu47l79LssL/u2a5Y9+0NEU3nQA5qdt+ +1FAE0c5oexPimXOrR3GWfKz7PmZ2O0117IeCUUXPG5U8umhDe/4mDF4ZNJiKc404 +WthquTqgS7rLQZHhZ6D0EnGnOkzlmxJMYPNHSOY1/6ivdNUUcC87awNEA3lgfhy2 +5IyBK3QJc+aYKNTbt70Lery3bu2wWLFGtmNiGlQTS4JsxImRsECTI727ObS7/FWA +QsqW+COL0Sa5BuMFrFIpjPrEe0ih7vRRbdmXRwIDAQABAoIBAGe4+9VqZfJN+dsq +8Osyuz01uQ8OmC0sAWTIqUlQgENIyf9rCJsUBlYmwR5BT6Z69XP6QhHdpSK+TiAR +XUz0EqG9HYzcxHIBaACP7j6iRoQ8R4kbbiWKo0z3WqQGIOqFjvD/mKEuQdE5mEYw +eOUCG6BnX1WY2Yr8WKd2AA/tp0/Y4d8z04u9eodMpSTbHTzYMJb5SbBN1vo6FY7q +8zSuO0BMzXlAxUsCwHsk1GQHFr8Oh3zIR7bQGtMBouI+6Lhh7sjFYsfxJboqMTBV +IKaA216M6ggHG7MU1/jeKcMGDmEfqQLQoyWp29rMK6TklUgipME2L3UD7vTyAVzz +xbVOpZkCgYEA8CXW4sZBBrSSrLR5SB+Ubu9qNTggLowOsC/kVKB2WJ4+xooc5HQo +mFhq1v/WxPQoWIxdYsfg2odlL+JclK5Qcy6vXmRSdAQ5lK9gBDKxZSYc3NwAw2HA +zyHCTK+I0n8PBYQ+yGcrxu0WqTGnlLW+Otk4CejO34WlgHwbH9bbY5UCgYEA3ZvT +C4+OoMHXlmICSt29zUrYiL33IWsR3/MaONxTEDuvgkOSXXQOl/8Ebd6Nu+3WbsSN +bjiPC/JyL1YCVmijdvFpl4gjtgvfJifs4G+QHvO6YfsYoVANk4u6g6rUuBIOwNK4 +RwYxwDc0oysp+g7tPxoSgDHReEVKJNzGBe9NGGsCgYEA4O4QP4gCEA3B9BF2J5+s +n9uPVxmiyvZUK6Iv8zP4pThTBBMIzNIf09G9AHPQ7djikU2nioY8jXKTzC3xGTHM +GJZ5m6fLsu7iH+nDvSreDSeNkTBfZqGAvoGYQ8uGE+L+ZuRfCcXYsxIOT5s6o4c3 +Dle2rVFpsuKzCY00urW796ECgYBn3go75+xEwrYGQSer6WR1nTgCV29GVYXKPooy +zmmMOT1Yw80NSkEw0pFD4cTyqVYREsTrPU0mn1sPfrOXxnGfZSVFpcR/Je9QVfQ7 +eW7GYxwfom335aqHVj10SxRqteP+UoWWnHujCPz94VRKZMakBddYCIGSan+G6YdS +7sdmwwKBgBc2qj0wvGXDF2kCLwSGfWoMf8CS1+5fIiUIdT1e/+7MfDdbmLMIFVjF +QKS3zVViXCbrG5SY6wS9hxoc57f6E2A8vcaX6zy2xkZlGHQCpWRtEM5R01OWJQaH +HsHMmQZGUQVoDm1oRkDhrTFK4K3ukc3rAxzeTZ96utOQN8/KJsTv +-----END RSA PRIVATE KEY----- diff --git a/test/io.test.js b/test/io.test.js new file mode 100644 index 0000000000..3678438f04 --- /dev/null +++ b/test/io.test.js @@ -0,0 +1,116 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Test dependencies. + */ + +var sio = require('socket.io') + , fs = require('fs') + , http = require('http') + , https = require('https') + , should = require('./common') + , ports = 15000; + +/** + * Test. + */ + +module.exports = { + + 'test that protocol version is present': function (done) { + sio.protocol.should.be.a('number'); + done(); + }, + + 'test that default transports are present': function (done) { + sio.Manager.defaultTransports.should.be.an.instanceof(Array); + done(); + }, + + 'test that version is present': function (done) { + sio.version.should.match(/([0-9]+)\.([0-9]+)\.([0-9]+)/); + done(); + }, + + 'test listening with a port': function (done) { + var port = ++ports + , io = sio.listen(port); + + io.server.should.be.an.instanceof(http.Server); + + get({ + port: port + , path: '/' + }, function (res, data) { + res.statusCode.should.eql(200); + data.should.eql('Welcome to socket.io.'); + io.server.close(); + done(); + }); + }, + + 'test listening with a server': function (done) { + var server = http.createServer() + , io = sio.listen(server) + , port = ++ports; + + server.listen(port); + + get({ + port: port + , path: '/socket.io' + }, function (res, data) { + res.statusCode.should.eql(200); + data.should.eql('Welcome to socket.io.'); + + server.close(); + done(); + }); + }, + + 'test listening with a https server': function (done) { + var server = https.createServer({ + key: fs.readFileSync(__dirname + '/fixtures/key.key') + , cert: fs.readFileSync(__dirname + '/fixtures/cert.crt') + }, function () { }) + , io = sio.listen(server) + , port = ++ports; + + server.listen(port); + + get({ + port: port + , path: '/socket.io' + , secure: true + }, function (res, data) { + res.statusCode.should.eql(200); + data.should.eql('Welcome to socket.io.'); + + server.close(); + done(); + }); + }, + + 'test listening with no arguments listens on 80': function (done) { + try { + var io = sio.listen(); + get({ + post: 80 + , path: '/socket.io' + }, function (res) { + res.statusCode.should.eql(200); + io.server.close(); + done(); + }); + } catch (e) { + e.should.match(/EACCES/); + done(); + } + } + +}; diff --git a/test/manager.test.js b/test/manager.test.js new file mode 100644 index 0000000000..a4ec682ea4 --- /dev/null +++ b/test/manager.test.js @@ -0,0 +1,292 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Test dependencies. + */ + +var sio = require('socket.io') + , http = require('http') + , should = require('./common') + , ports = 15100; + +/** + * Test. + */ + +module.exports = { + + 'test setting and getting a configuration flag': function (done) { + done(); + }, + + 'test enabling and disabling a configuration flag': function (done) { + var port = ++ports + , io = sio.listen(http.createServer()); + + io.enable('flag'); + io.enabled('flag').should.be.true; + io.disabled('flag').should.be.false; + + io.disable('flag'); + var port = ++ports + , io = sio.listen(http.createServer()); + + io.configure(function () { + io.set('a', 'b'); + io.enable('tobi'); + }); + + io.get('a').should.eql('b'); + io.enabled('tobi').should.be.true; + + done(); + }, + + 'test configuration callbacks with envs': function (done) { + var port = ++ports + , io = sio.listen(http.createServer()); + + process.env.NODE_ENV = 'development'; + + io.configure('production', function () { + io.set('ferret', 'tobi'); + }); + + io.configure('development', function () { + io.set('ferret', 'jane'); + }); + + io.get('ferret').should.eql('jane'); + done(); + }, + + 'test that normal requests are still served': function (done) { + var server = http.createServer(function (req, res) { + res.writeHead(200); + res.end('woot'); + }); + + var io = sio.listen(server) + , port = ++ports; + + server.listen(ports); + + get({ + port: port + , path: '/socket.io' + }, function (res, data) { + res.statusCode.should.eql(200); + data.should.eql('Welcome to socket.io.'); + + get({ + port: port + , path: '/woot' + }, function (res, data) { + server.close(); + done(); + }); + }); + }, + + 'test that the client is served': function (done) { + var port = ++ports + , io = sio.listen(port); + + get({ + port: port + , path: '/socket.io/socket.io.js' + }, function (res, data) { + res.headers['content-type'].should.eql('application/javascript'); + res.headers['content-length'].should.be.match(/([0-9]+)/); + res.headers.etag.should.match(/([0-9]+)\.([0-9]+)\.([0-9]+)/); + + data.should.match(/XMLHttpRequest/); + + io.server.close(); + done(); + }); + }, + + 'test that you can serve custom clients': function (done) { + var port = ++ports + , io = sio.listen(port); + + io.configure(function () { + io.set('browser client', 'custom_client'); + io.set('browser client etag', '1.0'); + }); + + get({ + port: port + , path: '/socket.io/socket.io.js' + }, function (res, data) { + res.headers['content-type'].should.eql('application/javascript'); + res.headers['content-length'].should.eql(13); + res.headers.etag.should.eql('1.0'); + + data.should.eql('custom_client'); + + io.server.close(); + done(); + }); + }, + + 'test handshake': function (done) { + var port = ++ports + , io = sio.listen(port); + + get({ + port: port + , path: '/socket.io/{protocol}/' + }, function (res, data) { + res.statusCode.should.eql(200); + data.should.match(/([^:]+):([0-9]+)?:([0-9]+)?:(.+)/); + io.server.close(); + done(); + }); + }, + + 'test handshake with unsupported protocol version': function (done) { + var port = ++ports + , io = sio.listen(port); + + get({ + port: port + , path: '/socket.io/-1/' + }, function (res, data) { + res.statusCode.should.eql(500); + data.should.match(/Protocol version not supported/); + io.server.close(); + done(); + }); + }, + + 'test authorization failure in handshake': function (done) { + var port = ++ports + , io = sio.listen(port); + + io.configure(function () { + function auth (data, fn) { + fn(null, false); + }; + + io.set('authorization', auth); + }); + + get({ + port: port + , path: '/socket.io/{protocol}/' + }, function (res, data) { + res.statusCode.should.eql(403); + data.should.match(/Handshake unauthorized/); + io.server.close(); + done(); + }); + }, + + 'test a handshake error': function (done) { + var port = ++ports + , io = sio.listen(port); + + io.configure(function () { + function auth (data, fn) { + fn(new Error); + }; + + io.set('authorization', auth); + }); + + get({ + port: port + , path: '/socket.io/{protocol}/' + }, function (res, data) { + res.statusCode.should.eql(500); + data.should.match(/Handshake error/); + io.server.close(); + done(); + }); + }, + + 'test limiting the supported transports for a manager': function (done) { + var port = ++ports + , io = sio.listen(port); + + io.configure(function () { + io.set('transports', ['tobi', 'jane']); + }); + + get({ + port: port + , path: '/socket.io/{protocol}/' + }, function (res, data) { + res.statusCode.should.eql(200); + data.should.match(/([^:]+):([0-9]+)?:([0-9]+)?:tobi,jane/); + io.server.close(); + done(); + }); + }, + + 'test setting a custom close timeout': function (done) { + var port = ++ports + , io = sio.listen(port); + + io.configure(function () { + io.set('close timeout', 66); + }); + + get({ + port: port + , path: '/socket.io/{protocol}/' + }, function (res, data) { + res.statusCode.should.eql(200); + data.should.match(/([^:]+):([0-9]+)?:66?:(.*)/); + io.server.close(); + done(); + }); + }, + + 'test setting a custom heartbeat timeout': function (done) { + var port = ++ports + , io = sio.listen(port); + + io.configure(function () { + io.set('heartbeat timeout', 33); + }); + + get({ + port: port + , path: '/socket.io/{protocol}/' + }, function (res, data) { + res.statusCode.should.eql(200); + data.should.match(/([^:]+):33:([0-9]+)?:(.*)/); + io.server.close(); + done(); + }); + }, + + 'test disabling timeouts': function (done) { + var port = ++ports + , io = sio.listen(port); + + io.configure(function () { + io.set('heartbeat timeout', null); + io.set('close timeout', ''); + }); + + get({ + port: port + , path: '/socket.io/{protocol}/' + }, function (res, data) { + res.statusCode.should.eql(200); + data.should.match(/([^:]+)::?:(.*)/); + io.server.close(); + done(); + }); + } + +}; diff --git a/test/parser.test.js b/test/parser.test.js new file mode 100644 index 0000000000..ffc23cef2f --- /dev/null +++ b/test/parser.test.js @@ -0,0 +1,335 @@ + +/** + * Test dependencies. + */ + +const parser = require('socket.io').parser + , decode = parser.decode + , should = require('./common'); + +/** + * Test. + */ + +module.exports = { + + 'decoding error packet': function () { + parser.decodePacket('7:::').should.eql({ + type: 'error' + , reason: '' + , advice: '' + , endpoint: '' + }); + }, + + 'decoding error packet with reason': function () { + parser.decodePacket('7:::0').should.eql({ + type: 'error' + , reason: 'transport not supported' + , advice: '' + , endpoint: '' + }); + }, + + 'decoding error packet with reason and advice': function () { + parser.decodePacket('7:::2+0').should.eql({ + type: 'error' + , reason: 'unauthorized' + , advice: 'reconnect' + , endpoint: '' + }); + }, + + 'decoding error packet with endpoint': function () { + parser.decodePacket('7::/woot').should.eql({ + type: 'error' + , reason: '' + , advice: '' + , endpoint: '/woot' + }); + }, + + 'decoding ack packet': function () { + parser.decodePacket('6:::140').should.eql({ + type: 'ack' + , ackId: '140' + , endpoint: '' + , args: [] + }); + }, + + 'decoding ack packet with args': function () { + parser.decodePacket('6:::12+' + JSON.stringify(['woot', 'wa'])).should.eql({ + type: 'ack' + , ackId: '12' + , endpoint: '' + , args: ['woot', 'wa'] + }); + }, + + 'decoding ack packet with bad json': function () { + var thrown = false; + + try { + parser.decodePacket('6:::1+{"++]').should.eql({ + type: 'ack' + , ackId: '1' + , endpoint: '' + , args: [] + }); + } catch (e) { + thrown = true; + } + + thrown.should.be.false; + }, + + 'decoding json packet': function () { + parser.decodePacket('4:::"2"').should.eql({ + type: 'json' + , endpoint: '' + , data: '2' + }); + }, + + 'decoding json packet with message id and ack data': function () { + parser.decodePacket('4:1+::{"a":"b"}').should.eql({ + type: 'json' + , id: 1 + , ack: 'data' + , endpoint: '' + , data: { a: 'b' } + }); + }, + + 'decoding an event packet': function () { + parser.decodePacket('5:::woot').should.eql({ + type: 'event' + , name: 'woot' + , endpoint: '' + , args: [] + }); + }, + + 'decoding an event packet with message id and ack': function () { + parser.decodePacket('5:1+::tobi').should.eql({ + type: 'event' + , id: 1 + , ack: 'data' + , endpoint: '' + , name: 'tobi' + , args: [] + }); + }, + + 'decoding an event packet with data': function () { + parser.decodePacket('5:::edwald\ufffd[{"a": "b"},2,"3"]').should.eql({ + type: 'event' + , name: 'edwald' + , endpoint: '' + , args: [{a: 'b'}, 2, '3'] + }); + }, + + 'decoding a message packet': function () { + parser.decodePacket('3:::woot').should.eql({ + type: 'message' + , endpoint: '' + , data: 'woot' + }); + }, + + 'decoding a message packet with id and endpoint': function () { + parser.decodePacket('3:5:/tobi').should.eql({ + type: 'message' + , id: 5 + , ack: true + , endpoint: '/tobi' + , data: '' + }); + }, + + 'decoding a heartbeat packet': function () { + parser.decodePacket('2:::').should.eql({ + type: 'heartbeat' + , endpoint: '' + }); + }, + + 'decoding a connection packet': function () { + parser.decodePacket('1::/tobi').should.eql({ + type: 'connect' + , endpoint: '/tobi' + , qs: '' + }); + }, + + 'decoding a connection packet with query string': function () { + parser.decodePacket('1::/test:?test=1').should.eql({ + type: 'connect' + , endpoint: '/test' + , qs: '?test=1' + }); + }, + + 'decoding a disconnection packet': function () { + parser.decodePacket('0::/woot').should.eql({ + type: 'disconnect' + , endpoint: '/woot' + }); + }, + + 'encoding error packet': function () { + parser.encodePacket({ + type: 'error' + , reason: '' + , advice: '' + , endpoint: '' + }).should.eql('7::'); + }, + + 'encoding error packet with reason': function () { + parser.encodePacket({ + type: 'error' + , reason: 'transport not supported' + , advice: '' + , endpoint: '' + }).should.eql('7:::0'); + }, + + 'encoding error packet with reason and advice': function () { + parser.encodePacket({ + type: 'error' + , reason: 'unauthorized' + , advice: 'reconnect' + , endpoint: '' + }).should.eql('7:::2+0'); + }, + + 'encoding error packet with endpoint': function () { + parser.encodePacket({ + type: 'error' + , reason: '' + , advice: '' + , endpoint: '/woot' + }).should.eql('7::/woot'); + }, + + 'encoding ack packet': function () { + parser.encodePacket({ + type: 'ack' + , ackId: '140' + , endpoint: '' + , args: [] + }).should.eql('6:::140'); + }, + + 'encoding ack packet with args': function () { + parser.encodePacket({ + type: 'ack' + , ackId: '12' + , endpoint: '' + , args: ['woot', 'wa'] + }).should.eql('6:::12+' + JSON.stringify(['woot', 'wa'])); + }, + + 'encoding json packet': function () { + parser.encodePacket({ + type: 'json' + , endpoint: '' + , data: '2' + }).should.eql('4:::"2"'); + }, + + 'encoding json packet with message id and ack data': function () { + parser.encodePacket({ + type: 'json' + , id: 1 + , ack: 'data' + , endpoint: '' + , data: { a: 'b' } + }).should.eql('4:1+::{"a":"b"}'); + }, + + 'encoding an event packet': function () { + parser.encodePacket({ + type: 'event' + , name: 'woot' + , endpoint: '' + , args: [] + }).should.eql('5:::woot'); + }, + + 'encoding an event packet with message id and ack': function () { + parser.encodePacket({ + type: 'event' + , id: 1 + , ack: 'data' + , endpoint: '' + , name: 'tobi' + , args: [] + }).should.eql('5:1+::tobi'); + }, + + 'encoding an event packet with data': function () { + parser.encodePacket({ + type: 'event' + , name: 'edwald' + , endpoint: '' + , args: [{a: 'b'}, 2, '3'] + }).should.eql('5:::edwald\ufffd[{"a":"b"},2,"3"]'); + }, + + 'encoding a message packet': function () { + parser.encodePacket({ + type: 'message' + , endpoint: '' + , data: 'woot' + }).should.eql('3:::woot'); + }, + + 'encoding a message packet with id and endpoint': function () { + parser.encodePacket({ + type: 'message' + , id: 5 + , ack: true + , endpoint: '/tobi' + , data: '' + }).should.eql('3:5:/tobi'); + }, + + 'encoding a heartbeat packet': function () { + parser.encodePacket({ + type: 'heartbeat' + , endpoint: '' + }).should.eql('2::'); + }, + + 'encoding a connection packet': function () { + parser.encodePacket({ + type: 'connect' + , endpoint: '/tobi' + , qs: '' + }).should.eql('1::/tobi'); + }, + + 'encoding a connection packet with query string': function () { + parser.encodePacket({ + type: 'connect' + , endpoint: '/test' + , qs: '?test=1' + }).should.eql('1::/test:?test=1'); + }, + + 'encoding a disconnection packet': function () { + parser.encodePacket({ + type: 'disconnect' + , endpoint: '/woot' + }).should.eql('0::/woot'); + }, + + 'test decoding a payload': function () { + parser.decodePayload('3:::5') + } + +}; diff --git a/test/socket.js b/test/socket.js new file mode 100644 index 0000000000..41e46751e3 --- /dev/null +++ b/test/socket.js @@ -0,0 +1,15 @@ + +/** + * Test dependencies. + */ + +const sio = require('socket.io') + , should = require('./common'); + +/** + * Test. + */ + +module.exports = { + +}; diff --git a/test/transports.xhr-polling.test.js b/test/transports.xhr-polling.test.js new file mode 100644 index 0000000000..cca6dd0c24 --- /dev/null +++ b/test/transports.xhr-polling.test.js @@ -0,0 +1,120 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Test dependencies. + */ + +var sio = require('socket.io') + , should = require('./common') + , parser = sio.parser + , ports = 15200; + +/** + * Test. + */ + +module.exports = { + + 'test handshake': function (done) { + var port = ++ports + , io = sio.listen(port); + + io.configure(function () { + io.set('polling duration', .2); + io.set('close timeout', .2); + }); + + function finish () { + req.res.socket.end(); + io.server.close(); + done(); + }; + + var req = get({ + port: port + , path: '/socket.io/{protocol}/' + }, function (res, data) { + var sessid = data.split(':')[0] + , total = 2; + + get({ + port: port + , path: '/socket.io/{protocol}/xhr-polling/jiasdasjid' + }, function (res, data) { + res.statusCode.should.eql(200); + + data.should.eql(parser.encodePacket({ + type: 'error' + , reason: 'client not handshaken' + })); + + --total || finish(); + }); + + get({ + port: port + , path: '/socket.io/{protocol}/xhr-polling/' + sessid + }, function (res, data) { + res.statusCode.should.eql(200); + data.should.eql(''); + --total || finish(); + }); + }); + }, + + 'test the connection event': function (done) { + var port = ++ports + , io = sio.listen(port) + , sessid, req; + + io.configure(function () { + io.set('polling duration', .2); + io.set('close timeout', .2); + }); + + io.sockets.on('connection', function (socket) { + socket.id.should.eql(sessid); + io.server.close(); + done(); + }); + + handshake(port, function (sid) { + sessid = sid; + req = get({ + port: port + , path: '/socket.io/{protocol}/xhr-polling/' + sid + }, function() {}); + }); + }, + + 'test sending back data': function (data) { + var port = ++ports + , io = sio.listen(port); + + io.configure(function () { + io.set('polling duration', .2); + io.set('close timeout', .2); + }); + + io.sockets.on('connection', function (socket) { + socket.send('woot'); + }); + + handshake(port, function (sid) { + get({ + port: port + , path: '/socket.io/{protocol}/xhr-polling/' + sid + }, function (res, data) { + var packet = parser.decodePacket(data); + packet.type.should.eql('message'); + packet.data.should.eql('woot'); + }); + }); + } + +};