rc-web@69: /*! rc-web@69: * socket.io-node rc-web@69: * Copyright(c) 2011 LearnBoost rc-web@69: * MIT Licensed rc-web@69: */ rc-web@69: rc-web@69: /** rc-web@69: * Module dependencies. rc-web@69: */ rc-web@69: rc-web@69: var fs = require('fs') rc-web@69: , url = require('url') rc-web@69: , tty = require('tty') rc-web@69: , crypto = require('crypto') rc-web@69: , util = require('./util') rc-web@69: , store = require('./store') rc-web@69: , client = require('socket.io-client') rc-web@69: , transports = require('./transports') rc-web@69: , Logger = require('./logger') rc-web@69: , Socket = require('./socket') rc-web@69: , MemoryStore = require('./stores/memory') rc-web@69: , SocketNamespace = require('./namespace') rc-web@69: , Static = require('./static') rc-web@69: , EventEmitter = process.EventEmitter; rc-web@69: rc-web@69: /** rc-web@69: * Export the constructor. rc-web@69: */ rc-web@69: rc-web@69: exports = module.exports = Manager; rc-web@69: rc-web@69: /** rc-web@69: * Default transports. rc-web@69: */ rc-web@69: rc-web@69: var defaultTransports = exports.defaultTransports = [ rc-web@69: 'websocket' rc-web@69: , 'htmlfile' rc-web@69: , 'xhr-polling' rc-web@69: , 'jsonp-polling' rc-web@69: ]; rc-web@69: rc-web@69: /** rc-web@69: * Inherited defaults. rc-web@69: */ rc-web@69: rc-web@69: var parent = module.parent.exports rc-web@69: , protocol = parent.protocol rc-web@69: , jsonpolling_re = /^\d+$/; rc-web@69: rc-web@69: /** rc-web@69: * Manager constructor. rc-web@69: * rc-web@69: * @param {HTTPServer} server rc-web@69: * @param {Object} options, optional rc-web@69: * @api public rc-web@69: */ rc-web@69: rc-web@69: function Manager (server, options) { rc-web@69: this.server = server; rc-web@69: this.namespaces = {}; rc-web@69: this.sockets = this.of(''); rc-web@69: this.settings = { rc-web@69: origins: '*:*' rc-web@69: , log: true rc-web@69: , store: new MemoryStore rc-web@69: , logger: new Logger rc-web@69: , static: new Static(this) rc-web@69: , heartbeats: true rc-web@69: , resource: '/socket.io' rc-web@69: , transports: defaultTransports rc-web@69: , authorization: false rc-web@69: , blacklist: ['disconnect'] rc-web@69: , 'log level': 3 rc-web@69: , 'log colors': tty.isatty(process.stdout.fd) rc-web@69: , 'close timeout': 60 rc-web@69: , 'heartbeat interval': 25 rc-web@69: , 'heartbeat timeout': 60 rc-web@69: , 'polling duration': 20 rc-web@69: , 'flash policy server': true rc-web@69: , 'flash policy port': 10843 rc-web@69: , 'destroy upgrade': true rc-web@69: , 'destroy buffer size': 10E7 rc-web@69: , 'browser client': true rc-web@69: , 'browser client cache': true rc-web@69: , 'browser client minification': false rc-web@69: , 'browser client etag': false rc-web@69: , 'browser client expires': 315360000 rc-web@69: , 'browser client gzip': false rc-web@69: , 'browser client handler': false rc-web@69: , 'client store expiration': 15 rc-web@69: , 'match origin protocol': false rc-web@69: }; rc-web@69: rc-web@69: for (var i in options) { rc-web@69: if (options.hasOwnProperty(i)) { rc-web@69: this.settings[i] = options[i]; rc-web@69: } rc-web@69: } rc-web@69: rc-web@69: var self = this; rc-web@69: rc-web@69: // default error handler rc-web@69: server.on('error', function(err) { rc-web@69: self.log.warn('error raised: ' + err); rc-web@69: }); rc-web@69: rc-web@69: this.initStore(); rc-web@69: rc-web@69: this.on('set:store', function() { rc-web@69: self.initStore(); rc-web@69: }); rc-web@69: rc-web@69: // reset listeners rc-web@69: this.oldListeners = server.listeners('request').splice(0); rc-web@69: server.removeAllListeners('request'); rc-web@69: rc-web@69: server.on('request', function (req, res) { rc-web@69: self.handleRequest(req, res); rc-web@69: }); rc-web@69: rc-web@69: server.on('upgrade', function (req, socket, head) { rc-web@69: self.handleUpgrade(req, socket, head); rc-web@69: }); rc-web@69: rc-web@69: server.on('close', function () { rc-web@69: clearInterval(self.gc); rc-web@69: }); rc-web@69: rc-web@69: server.once('listening', function () { rc-web@69: self.gc = setInterval(self.garbageCollection.bind(self), 10000); rc-web@69: }); rc-web@69: rc-web@69: for (var i in transports) { rc-web@69: if (transports.hasOwnProperty(i)) { rc-web@69: if (transports[i].init) { rc-web@69: transports[i].init(this); rc-web@69: } rc-web@69: } rc-web@69: } rc-web@69: rc-web@69: // forward-compatibility with 1.0 rc-web@69: var self = this; rc-web@69: this.sockets.on('connection', function (conn) { rc-web@69: self.emit('connection', conn); rc-web@69: }); rc-web@69: rc-web@69: this.sequenceNumber = Date.now() | 0; rc-web@69: rc-web@69: this.log.info('socket.io started'); rc-web@69: }; rc-web@69: rc-web@69: Manager.prototype.__proto__ = EventEmitter.prototype rc-web@69: rc-web@69: /** rc-web@69: * Store accessor shortcut. rc-web@69: * rc-web@69: * @api public rc-web@69: */ rc-web@69: rc-web@69: Manager.prototype.__defineGetter__('store', function () { rc-web@69: var store = this.get('store'); rc-web@69: store.manager = this; rc-web@69: return store; rc-web@69: }); rc-web@69: rc-web@69: /** rc-web@69: * Logger accessor. rc-web@69: * rc-web@69: * @api public rc-web@69: */ rc-web@69: rc-web@69: Manager.prototype.__defineGetter__('log', function () { rc-web@69: var logger = this.get('logger'); rc-web@69: rc-web@69: logger.level = this.get('log level') || -1; rc-web@69: logger.colors = this.get('log colors'); rc-web@69: logger.enabled = this.enabled('log'); rc-web@69: rc-web@69: return logger; rc-web@69: }); rc-web@69: rc-web@69: /** rc-web@69: * Static accessor. rc-web@69: * rc-web@69: * @api public rc-web@69: */ rc-web@69: rc-web@69: Manager.prototype.__defineGetter__('static', function () { rc-web@69: return this.get('static'); rc-web@69: }); rc-web@69: rc-web@69: /** rc-web@69: * Get settings. rc-web@69: * rc-web@69: * @api public rc-web@69: */ rc-web@69: rc-web@69: Manager.prototype.get = function (key) { rc-web@69: return this.settings[key]; rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Set settings rc-web@69: * rc-web@69: * @api public rc-web@69: */ rc-web@69: rc-web@69: Manager.prototype.set = function (key, value) { rc-web@69: if (arguments.length == 1) return this.get(key); rc-web@69: this.settings[key] = value; rc-web@69: this.emit('set:' + key, this.settings[key], key); rc-web@69: return this; rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Enable a setting rc-web@69: * rc-web@69: * @api public rc-web@69: */ rc-web@69: rc-web@69: Manager.prototype.enable = function (key) { rc-web@69: this.settings[key] = true; rc-web@69: this.emit('set:' + key, this.settings[key], key); rc-web@69: return this; rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Disable a setting rc-web@69: * rc-web@69: * @api public rc-web@69: */ rc-web@69: rc-web@69: Manager.prototype.disable = function (key) { rc-web@69: this.settings[key] = false; rc-web@69: this.emit('set:' + key, this.settings[key], key); rc-web@69: return this; rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Checks if a setting is enabled rc-web@69: * rc-web@69: * @api public rc-web@69: */ rc-web@69: rc-web@69: Manager.prototype.enabled = function (key) { rc-web@69: return !!this.settings[key]; rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Checks if a setting is disabled rc-web@69: * rc-web@69: * @api public rc-web@69: */ rc-web@69: rc-web@69: Manager.prototype.disabled = function (key) { rc-web@69: return !this.settings[key]; rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Configure callbacks. rc-web@69: * rc-web@69: * @api public rc-web@69: */ rc-web@69: rc-web@69: Manager.prototype.configure = function (env, fn) { rc-web@69: if ('function' == typeof env) { rc-web@69: env.call(this); rc-web@69: } else if (env == (process.env.NODE_ENV || 'development')) { rc-web@69: fn.call(this); rc-web@69: } rc-web@69: rc-web@69: return this; rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Initializes everything related to the message dispatcher. rc-web@69: * rc-web@69: * @api private rc-web@69: */ rc-web@69: rc-web@69: Manager.prototype.initStore = function () { rc-web@69: this.handshaken = {}; rc-web@69: this.connected = {}; rc-web@69: this.open = {}; rc-web@69: this.closed = {}; rc-web@69: this.rooms = {}; rc-web@69: this.roomClients = {}; rc-web@69: rc-web@69: var self = this; rc-web@69: rc-web@69: this.store.subscribe('handshake', function (id, data) { rc-web@69: self.onHandshake(id, data); rc-web@69: }); rc-web@69: rc-web@69: this.store.subscribe('connect', function (id) { rc-web@69: self.onConnect(id); rc-web@69: }); rc-web@69: rc-web@69: this.store.subscribe('open', function (id) { rc-web@69: self.onOpen(id); rc-web@69: }); rc-web@69: rc-web@69: this.store.subscribe('join', function (id, room) { rc-web@69: self.onJoin(id, room); rc-web@69: }); rc-web@69: rc-web@69: this.store.subscribe('leave', function (id, room) { rc-web@69: self.onLeave(id, room); rc-web@69: }); rc-web@69: rc-web@69: this.store.subscribe('close', function (id) { rc-web@69: self.onClose(id); rc-web@69: }); rc-web@69: rc-web@69: this.store.subscribe('dispatch', function (room, packet, volatile, exceptions) { rc-web@69: self.onDispatch(room, packet, volatile, exceptions); rc-web@69: }); rc-web@69: rc-web@69: this.store.subscribe('disconnect', function (id) { rc-web@69: self.onDisconnect(id); rc-web@69: }); rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Called when a client handshakes. rc-web@69: * rc-web@69: * @param text rc-web@69: */ rc-web@69: rc-web@69: Manager.prototype.onHandshake = function (id, data) { rc-web@69: this.handshaken[id] = data; rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Called when a client connects (ie: transport first opens) rc-web@69: * rc-web@69: * @api private rc-web@69: */ rc-web@69: rc-web@69: Manager.prototype.onConnect = function (id) { rc-web@69: this.connected[id] = true; rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Called when a client opens a request in a different node. rc-web@69: * rc-web@69: * @api private rc-web@69: */ rc-web@69: rc-web@69: Manager.prototype.onOpen = function (id) { rc-web@69: this.open[id] = true; rc-web@69: rc-web@69: if (this.closed[id]) { rc-web@69: var self = this; rc-web@69: rc-web@69: this.store.unsubscribe('dispatch:' + id, function () { rc-web@69: var transport = self.transports[id]; rc-web@69: if (self.closed[id] && self.closed[id].length && transport) { rc-web@69: rc-web@69: // if we have buffered messages that accumulate between calling rc-web@69: // onOpen an this async callback, send them if the transport is rc-web@69: // still open, otherwise leave them buffered rc-web@69: if (transport.open) { rc-web@69: transport.payload(self.closed[id]); rc-web@69: self.closed[id] = []; rc-web@69: } rc-web@69: } rc-web@69: }); rc-web@69: } rc-web@69: rc-web@69: // clear the current transport rc-web@69: if (this.transports[id]) { rc-web@69: this.transports[id].discard(); rc-web@69: this.transports[id] = null; rc-web@69: } rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Called when a message is sent to a namespace and/or room. rc-web@69: * rc-web@69: * @api private rc-web@69: */ rc-web@69: rc-web@69: Manager.prototype.onDispatch = function (room, packet, volatile, exceptions) { rc-web@69: if (this.rooms[room]) { rc-web@69: for (var i = 0, l = this.rooms[room].length; i < l; i++) { rc-web@69: var id = this.rooms[room][i]; rc-web@69: rc-web@69: if (!~exceptions.indexOf(id)) { rc-web@69: if (this.transports[id] && this.transports[id].open) { rc-web@69: this.transports[id].onDispatch(packet, volatile); rc-web@69: } else if (!volatile) { rc-web@69: this.onClientDispatch(id, packet); rc-web@69: } rc-web@69: } rc-web@69: } rc-web@69: } rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Called when a client joins a nsp / room. rc-web@69: * rc-web@69: * @api private rc-web@69: */ rc-web@69: rc-web@69: Manager.prototype.onJoin = function (id, name) { rc-web@69: if (!this.roomClients[id]) { rc-web@69: this.roomClients[id] = {}; rc-web@69: } rc-web@69: rc-web@69: if (!this.rooms[name]) { rc-web@69: this.rooms[name] = []; rc-web@69: } rc-web@69: rc-web@69: if (!~this.rooms[name].indexOf(id)) { rc-web@69: this.rooms[name].push(id); rc-web@69: this.roomClients[id][name] = true; rc-web@69: } rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Called when a client leaves a nsp / room. rc-web@69: * rc-web@69: * @param private rc-web@69: */ rc-web@69: rc-web@69: Manager.prototype.onLeave = function (id, room) { rc-web@69: if (this.rooms[room]) { rc-web@69: var index = this.rooms[room].indexOf(id); rc-web@69: rc-web@69: if (index >= 0) { rc-web@69: this.rooms[room].splice(index, 1); rc-web@69: } rc-web@69: rc-web@69: if (!this.rooms[room].length) { rc-web@69: delete this.rooms[room]; rc-web@69: } rc-web@69: rc-web@69: if (this.roomClients[id]) { rc-web@69: delete this.roomClients[id][room]; rc-web@69: } rc-web@69: } rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Called when a client closes a request in different node. rc-web@69: * rc-web@69: * @api private rc-web@69: */ rc-web@69: rc-web@69: Manager.prototype.onClose = function (id) { rc-web@69: if (this.open[id]) { rc-web@69: delete this.open[id]; rc-web@69: } rc-web@69: rc-web@69: this.closed[id] = []; rc-web@69: rc-web@69: var self = this; rc-web@69: rc-web@69: this.store.subscribe('dispatch:' + id, function (packet, volatile) { rc-web@69: if (!volatile) { rc-web@69: self.onClientDispatch(id, packet); rc-web@69: } rc-web@69: }); rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Dispatches a message for a closed client. rc-web@69: * rc-web@69: * @api private rc-web@69: */ rc-web@69: rc-web@69: Manager.prototype.onClientDispatch = function (id, packet) { rc-web@69: if (this.closed[id]) { rc-web@69: this.closed[id].push(packet); rc-web@69: } rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Receives a message for a client. rc-web@69: * rc-web@69: * @api private rc-web@69: */ rc-web@69: rc-web@69: Manager.prototype.onClientMessage = function (id, packet) { rc-web@69: if (this.namespaces[packet.endpoint]) { rc-web@69: this.namespaces[packet.endpoint].handlePacket(id, packet); rc-web@69: } rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Fired when a client disconnects (not triggered). rc-web@69: * rc-web@69: * @api private rc-web@69: */ rc-web@69: rc-web@69: Manager.prototype.onClientDisconnect = function (id, reason) { rc-web@69: for (var name in this.namespaces) { rc-web@69: if (this.namespaces.hasOwnProperty(name)) { rc-web@69: this.namespaces[name].handleDisconnect(id, reason, typeof this.roomClients[id] !== 'undefined' && rc-web@69: typeof this.roomClients[id][name] !== 'undefined'); rc-web@69: } rc-web@69: } rc-web@69: rc-web@69: this.onDisconnect(id); rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Called when a client disconnects. rc-web@69: * rc-web@69: * @param text rc-web@69: */ rc-web@69: rc-web@69: Manager.prototype.onDisconnect = function (id, local) { rc-web@69: delete this.handshaken[id]; rc-web@69: rc-web@69: if (this.open[id]) { rc-web@69: delete this.open[id]; rc-web@69: } rc-web@69: rc-web@69: if (this.connected[id]) { rc-web@69: delete this.connected[id]; rc-web@69: } rc-web@69: rc-web@69: if (this.transports[id]) { rc-web@69: this.transports[id].discard(); rc-web@69: delete this.transports[id]; rc-web@69: } rc-web@69: rc-web@69: if (this.closed[id]) { rc-web@69: delete this.closed[id]; rc-web@69: } rc-web@69: rc-web@69: if (this.roomClients[id]) { rc-web@69: for (var room in this.roomClients[id]) { rc-web@69: if (this.roomClients[id].hasOwnProperty(room)) { rc-web@69: this.onLeave(id, room); rc-web@69: } rc-web@69: } rc-web@69: delete this.roomClients[id] rc-web@69: } rc-web@69: rc-web@69: this.store.destroyClient(id, this.get('client store expiration')); rc-web@69: rc-web@69: this.store.unsubscribe('dispatch:' + id); rc-web@69: rc-web@69: if (local) { rc-web@69: this.store.unsubscribe('message:' + id); rc-web@69: this.store.unsubscribe('disconnect:' + id); rc-web@69: } rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Handles an HTTP request. rc-web@69: * rc-web@69: * @api private rc-web@69: */ rc-web@69: rc-web@69: Manager.prototype.handleRequest = function (req, res) { rc-web@69: var data = this.checkRequest(req); rc-web@69: rc-web@69: if (!data) { rc-web@69: for (var i = 0, l = this.oldListeners.length; i < l; i++) { rc-web@69: this.oldListeners[i].call(this.server, req, res); rc-web@69: } rc-web@69: rc-web@69: return; rc-web@69: } rc-web@69: rc-web@69: if (data.static || !data.transport && !data.protocol) { rc-web@69: if (data.static && this.enabled('browser client')) { rc-web@69: this.static.write(data.path, req, res); rc-web@69: } else { rc-web@69: res.writeHead(200); rc-web@69: res.end('Welcome to socket.io.'); rc-web@69: rc-web@69: this.log.info('unhandled socket.io url'); rc-web@69: } rc-web@69: rc-web@69: return; rc-web@69: } rc-web@69: rc-web@69: if (data.protocol != protocol) { rc-web@69: res.writeHead(500); rc-web@69: res.end('Protocol version not supported.'); rc-web@69: rc-web@69: this.log.info('client protocol version unsupported'); rc-web@69: } else { rc-web@69: if (data.id) { rc-web@69: this.handleHTTPRequest(data, req, res); rc-web@69: } else { rc-web@69: this.handleHandshake(data, req, res); rc-web@69: } rc-web@69: } rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Handles an HTTP Upgrade. rc-web@69: * rc-web@69: * @api private rc-web@69: */ rc-web@69: rc-web@69: Manager.prototype.handleUpgrade = function (req, socket, head) { rc-web@69: var data = this.checkRequest(req) rc-web@69: , self = this; rc-web@69: rc-web@69: if (!data) { rc-web@69: if (this.enabled('destroy upgrade')) { rc-web@69: socket.end(); rc-web@69: this.log.debug('destroying non-socket.io upgrade'); rc-web@69: } rc-web@69: rc-web@69: return; rc-web@69: } rc-web@69: rc-web@69: req.head = head; rc-web@69: this.handleClient(data, req); rc-web@69: req.head = null; rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Handles a normal handshaken HTTP request (eg: long-polling) rc-web@69: * rc-web@69: * @api private rc-web@69: */ rc-web@69: rc-web@69: Manager.prototype.handleHTTPRequest = function (data, req, res) { rc-web@69: req.res = res; rc-web@69: this.handleClient(data, req); rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Intantiantes a new client. rc-web@69: * rc-web@69: * @api private rc-web@69: */ rc-web@69: rc-web@69: Manager.prototype.handleClient = function (data, req) { rc-web@69: var socket = req.socket rc-web@69: , store = this.store rc-web@69: , self = this; rc-web@69: rc-web@69: // handle sync disconnect xhrs rc-web@69: if (undefined != data.query.disconnect) { rc-web@69: if (this.transports[data.id] && this.transports[data.id].open) { rc-web@69: this.transports[data.id].onForcedDisconnect(); rc-web@69: } else { rc-web@69: this.store.publish('disconnect-force:' + data.id); rc-web@69: } rc-web@69: req.res.writeHead(200); rc-web@69: req.res.end(); rc-web@69: return; rc-web@69: } rc-web@69: rc-web@69: if (!~this.get('transports').indexOf(data.transport)) { rc-web@69: this.log.warn('unknown transport: "' + data.transport + '"'); rc-web@69: req.connection.end(); rc-web@69: return; rc-web@69: } rc-web@69: rc-web@69: var transport = new transports[data.transport](this, data, req) rc-web@69: , handshaken = this.handshaken[data.id]; rc-web@69: rc-web@69: if (transport.disconnected) { rc-web@69: // failed during transport setup rc-web@69: req.connection.end(); rc-web@69: return; rc-web@69: } rc-web@69: if (handshaken) { rc-web@69: if (transport.open) { rc-web@69: if (this.closed[data.id] && this.closed[data.id].length) { rc-web@69: transport.payload(this.closed[data.id]); rc-web@69: this.closed[data.id] = []; rc-web@69: } rc-web@69: rc-web@69: this.onOpen(data.id); rc-web@69: this.store.publish('open', data.id); rc-web@69: this.transports[data.id] = transport; rc-web@69: } rc-web@69: rc-web@69: if (!this.connected[data.id]) { rc-web@69: this.onConnect(data.id); rc-web@69: this.store.publish('connect', data.id); rc-web@69: rc-web@69: // flag as used rc-web@69: delete handshaken.issued; rc-web@69: this.onHandshake(data.id, handshaken); rc-web@69: this.store.publish('handshake', data.id, handshaken); rc-web@69: rc-web@69: // initialize the socket for all namespaces rc-web@69: for (var i in this.namespaces) { rc-web@69: if (this.namespaces.hasOwnProperty(i)) { rc-web@69: var socket = this.namespaces[i].socket(data.id, true); rc-web@69: rc-web@69: // echo back connect packet and fire connection event rc-web@69: if (i === '') { rc-web@69: this.namespaces[i].handlePacket(data.id, { type: 'connect' }); rc-web@69: } rc-web@69: } rc-web@69: } rc-web@69: rc-web@69: this.store.subscribe('message:' + data.id, function (packet) { rc-web@69: self.onClientMessage(data.id, packet); rc-web@69: }); rc-web@69: rc-web@69: this.store.subscribe('disconnect:' + data.id, function (reason) { rc-web@69: self.onClientDisconnect(data.id, reason); rc-web@69: }); rc-web@69: } rc-web@69: } else { rc-web@69: if (transport.open) { rc-web@69: transport.error('client not handshaken', 'reconnect'); rc-web@69: } rc-web@69: rc-web@69: transport.discard(); rc-web@69: } rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Generates a session id. rc-web@69: * rc-web@69: * @api private rc-web@69: */ rc-web@69: rc-web@69: Manager.prototype.generateId = function () { rc-web@69: var rand = new Buffer(15); // multiple of 3 for base64 rc-web@69: if (!rand.writeInt32BE) { rc-web@69: return Math.abs(Math.random() * Math.random() * Date.now() | 0).toString() rc-web@69: + Math.abs(Math.random() * Math.random() * Date.now() | 0).toString(); rc-web@69: } rc-web@69: this.sequenceNumber = (this.sequenceNumber + 1) | 0; rc-web@69: rand.writeInt32BE(this.sequenceNumber, 11); rc-web@69: if (crypto.randomBytes) { rc-web@69: crypto.randomBytes(12).copy(rand); rc-web@69: } else { rc-web@69: // not secure for node 0.4 rc-web@69: [0, 4, 8].forEach(function(i) { rc-web@69: rand.writeInt32BE(Math.random() * Math.pow(2, 32) | 0, i); rc-web@69: }); rc-web@69: } rc-web@69: return rand.toString('base64').replace(/\//g, '_').replace(/\+/g, '-'); rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Handles a handshake request. rc-web@69: * rc-web@69: * @api private rc-web@69: */ rc-web@69: rc-web@69: Manager.prototype.handleHandshake = function (data, req, res) { rc-web@69: var self = this rc-web@69: , origin = req.headers.origin rc-web@69: , headers = { rc-web@69: 'Content-Type': 'text/plain' rc-web@69: }; rc-web@69: rc-web@69: function writeErr (status, message) { rc-web@69: if (data.query.jsonp && jsonpolling_re.test(data.query.jsonp)) { rc-web@69: res.writeHead(200, { 'Content-Type': 'application/javascript' }); rc-web@69: res.end('io.j[' + data.query.jsonp + '](new Error("' + message + '"));'); rc-web@69: } else { rc-web@69: res.writeHead(status, headers); rc-web@69: res.end(message); rc-web@69: } rc-web@69: }; rc-web@69: rc-web@69: function error (err) { rc-web@69: writeErr(500, 'handshake error'); rc-web@69: self.log.warn('handshake error ' + err); rc-web@69: }; rc-web@69: rc-web@69: if (!this.verifyOrigin(req)) { rc-web@69: writeErr(403, 'handshake bad origin'); rc-web@69: return; rc-web@69: } rc-web@69: rc-web@69: var handshakeData = this.handshakeData(data); rc-web@69: rc-web@69: if (origin) { rc-web@69: // https://developer.mozilla.org/En/HTTP_Access_Control rc-web@69: headers['Access-Control-Allow-Origin'] = origin; rc-web@69: headers['Access-Control-Allow-Credentials'] = 'true'; rc-web@69: } rc-web@69: rc-web@69: this.authorize(handshakeData, function (err, authorized, newData) { rc-web@69: if (err) return error(err); rc-web@69: rc-web@69: if (authorized) { rc-web@69: var id = self.generateId() rc-web@69: , hs = [ rc-web@69: id rc-web@69: , self.enabled('heartbeats') ? self.get('heartbeat timeout') || '' : '' rc-web@69: , self.get('close timeout') || '' rc-web@69: , self.transports(data).join(',') rc-web@69: ].join(':'); rc-web@69: rc-web@69: if (data.query.jsonp && jsonpolling_re.test(data.query.jsonp)) { rc-web@69: hs = 'io.j[' + data.query.jsonp + '](' + JSON.stringify(hs) + ');'; rc-web@69: res.writeHead(200, { 'Content-Type': 'application/javascript' }); rc-web@69: } else { rc-web@69: res.writeHead(200, headers); rc-web@69: } rc-web@69: rc-web@69: res.end(hs); rc-web@69: rc-web@69: self.onHandshake(id, newData || handshakeData); rc-web@69: self.store.publish('handshake', id, newData || handshakeData); rc-web@69: rc-web@69: self.log.info('handshake authorized', id); rc-web@69: } else { rc-web@69: writeErr(403, 'handshake unauthorized'); rc-web@69: self.log.info('handshake unauthorized'); rc-web@69: } rc-web@69: }) rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Gets normalized handshake data rc-web@69: * rc-web@69: * @api private rc-web@69: */ rc-web@69: rc-web@69: Manager.prototype.handshakeData = function (data) { rc-web@69: var connection = data.request.connection rc-web@69: , connectionAddress rc-web@69: , date = new Date; rc-web@69: rc-web@69: if (connection.remoteAddress) { rc-web@69: connectionAddress = { rc-web@69: address: connection.remoteAddress rc-web@69: , port: connection.remotePort rc-web@69: }; rc-web@69: } else if (connection.socket && connection.socket.remoteAddress) { rc-web@69: connectionAddress = { rc-web@69: address: connection.socket.remoteAddress rc-web@69: , port: connection.socket.remotePort rc-web@69: }; rc-web@69: } rc-web@69: rc-web@69: return { rc-web@69: headers: data.headers rc-web@69: , address: connectionAddress rc-web@69: , time: date.toString() rc-web@69: , query: data.query rc-web@69: , url: data.request.url rc-web@69: , xdomain: !!data.request.headers.origin rc-web@69: , secure: data.request.connection.secure rc-web@69: , issued: +date rc-web@69: }; rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Verifies the origin of a request. rc-web@69: * rc-web@69: * @api private rc-web@69: */ rc-web@69: rc-web@69: Manager.prototype.verifyOrigin = function (request) { rc-web@69: var origin = request.headers.origin || request.headers.referer rc-web@69: , origins = this.get('origins'); rc-web@69: rc-web@69: if (origin === 'null') origin = '*'; rc-web@69: rc-web@69: if (origins.indexOf('*:*') !== -1) { rc-web@69: return true; rc-web@69: } rc-web@69: rc-web@69: if (origin) { rc-web@69: try { rc-web@69: var parts = url.parse(origin); rc-web@69: parts.port = parts.port || 80; rc-web@69: var ok = rc-web@69: ~origins.indexOf(parts.hostname + ':' + parts.port) || rc-web@69: ~origins.indexOf(parts.hostname + ':*') || rc-web@69: ~origins.indexOf('*:' + parts.port); rc-web@69: if (!ok) this.log.warn('illegal origin: ' + origin); rc-web@69: return ok; rc-web@69: } catch (ex) { rc-web@69: this.log.warn('error parsing origin'); rc-web@69: } rc-web@69: } rc-web@69: else { rc-web@69: this.log.warn('origin missing from handshake, yet required by config'); rc-web@69: } rc-web@69: return false; rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Handles an incoming packet. rc-web@69: * rc-web@69: * @api private rc-web@69: */ rc-web@69: rc-web@69: Manager.prototype.handlePacket = function (sessid, packet) { rc-web@69: this.of(packet.endpoint || '').handlePacket(sessid, packet); rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Performs authentication. rc-web@69: * rc-web@69: * @param Object client request data rc-web@69: * @api private rc-web@69: */ rc-web@69: rc-web@69: Manager.prototype.authorize = function (data, fn) { rc-web@69: if (this.get('authorization')) { rc-web@69: var self = this; rc-web@69: rc-web@69: this.get('authorization').call(this, data, function (err, authorized) { rc-web@69: self.log.debug('client ' + authorized ? 'authorized' : 'unauthorized'); rc-web@69: fn(err, authorized); rc-web@69: }); rc-web@69: } else { rc-web@69: this.log.debug('client authorized'); rc-web@69: fn(null, true); rc-web@69: } rc-web@69: rc-web@69: return this; rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Retrieves the transports adviced to the user. rc-web@69: * rc-web@69: * @api private rc-web@69: */ rc-web@69: rc-web@69: Manager.prototype.transports = function (data) { rc-web@69: var transp = this.get('transports') rc-web@69: , ret = []; rc-web@69: rc-web@69: for (var i = 0, l = transp.length; i < l; i++) { rc-web@69: var transport = transp[i]; rc-web@69: rc-web@69: if (transport) { rc-web@69: if (!transport.checkClient || transport.checkClient(data)) { rc-web@69: ret.push(transport); rc-web@69: } rc-web@69: } rc-web@69: } rc-web@69: rc-web@69: return ret; rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Checks whether a request is a socket.io one. rc-web@69: * rc-web@69: * @return {Object} a client request data object or `false` rc-web@69: * @api private rc-web@69: */ rc-web@69: rc-web@69: var regexp = /^\/([^\/]+)\/?([^\/]+)?\/?([^\/]+)?\/?$/ rc-web@69: rc-web@69: Manager.prototype.checkRequest = function (req) { rc-web@69: var resource = this.get('resource'); rc-web@69: rc-web@69: var match; rc-web@69: if (typeof resource === 'string') { rc-web@69: match = req.url.substr(0, resource.length); rc-web@69: if (match !== resource) match = null; rc-web@69: } else { rc-web@69: match = resource.exec(req.url); rc-web@69: if (match) match = match[0]; rc-web@69: } rc-web@69: rc-web@69: if (match) { rc-web@69: var uri = url.parse(req.url.substr(match.length), true) rc-web@69: , path = uri.pathname || '' rc-web@69: , pieces = path.match(regexp); rc-web@69: rc-web@69: // client request data rc-web@69: var data = { rc-web@69: query: uri.query || {} rc-web@69: , headers: req.headers rc-web@69: , request: req rc-web@69: , path: path rc-web@69: }; rc-web@69: rc-web@69: if (pieces) { rc-web@69: data.protocol = Number(pieces[1]); rc-web@69: data.transport = pieces[2]; rc-web@69: data.id = pieces[3]; rc-web@69: data.static = !!this.static.has(path); rc-web@69: }; rc-web@69: rc-web@69: return data; rc-web@69: } rc-web@69: rc-web@69: return false; rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Declares a socket namespace rc-web@69: * rc-web@69: * @api public rc-web@69: */ rc-web@69: rc-web@69: Manager.prototype.of = function (nsp) { rc-web@69: if (this.namespaces[nsp]) { rc-web@69: return this.namespaces[nsp]; rc-web@69: } rc-web@69: rc-web@69: return this.namespaces[nsp] = new SocketNamespace(this, nsp); rc-web@69: }; rc-web@69: rc-web@69: /** rc-web@69: * Perform garbage collection on long living objects and properties that cannot rc-web@69: * be removed automatically. rc-web@69: * rc-web@69: * @api private rc-web@69: */ rc-web@69: rc-web@69: Manager.prototype.garbageCollection = function () { rc-web@69: // clean up unused handshakes rc-web@69: var ids = Object.keys(this.handshaken) rc-web@69: , i = ids.length rc-web@69: , now = Date.now() rc-web@69: , handshake; rc-web@69: rc-web@69: while (i--) { rc-web@69: handshake = this.handshaken[ids[i]]; rc-web@69: rc-web@69: if ('issued' in handshake && (now - handshake.issued) >= 3E4) { rc-web@69: this.onDisconnect(ids[i]); rc-web@69: } rc-web@69: } rc-web@69: };