'use strict'; /** * Module dependencies. */ var http = require('http'); var read = require('fs').readFileSync; var path = require('path'); var exists = require('fs').existsSync; var engine = require('engine.io'); var clientVersion = require('socket.io-client/package.json').version; var Client = require('./client'); var Emitter = require('events').EventEmitter; var Namespace = require('./namespace'); var ParentNamespace = require('./parent-namespace'); var Adapter = require('socket.io-adapter'); var parser = require('socket.io-parser'); var debug = require('debug')('socket.io:server'); var url = require('url'); /** * Module exports. */ module.exports = Server; /** * Socket.IO client source. */ var clientSource = undefined; var clientSourceMap = undefined; /** * Server constructor. * * @param {http.Server|Number|Object} srv http server, port or options * @param {Object} [opts] * @api public */ function Server(srv, opts){ if (!(this instanceof Server)) return new Server(srv, opts); if ('object' == typeof srv && srv instanceof Object && !srv.listen) { opts = srv; srv = null; } opts = opts || {}; this.nsps = {}; this.parentNsps = new Map(); this.path(opts.path || '/socket.io'); this.serveClient(false !== opts.serveClient); this.parser = opts.parser || parser; this.encoder = new this.parser.Encoder(); this.adapter(opts.adapter || Adapter); this.origins(opts.origins || '*:*'); this.sockets = this.of('/'); if (srv) this.attach(srv, opts); } /** * Server request verification function, that checks for allowed origins * * @param {http.IncomingMessage} req request * @param {Function} fn callback to be called with the result: `fn(err, success)` */ Server.prototype.checkRequest = function(req, fn) { var origin = req.headers.origin || req.headers.referer; // file:// URLs produce a null Origin which can't be authorized via echo-back if ('null' == origin || null == origin) origin = '*'; if (!!origin && typeof(this._origins) == 'function') return this._origins(origin, fn); if (this._origins.indexOf('*:*') !== -1) return fn(null, true); if (origin) { try { var parts = url.parse(origin); var defaultPort = 'https:' == parts.protocol ? 443 : 80; parts.port = parts.port != null ? parts.port : defaultPort; var ok = ~this._origins.indexOf(parts.protocol + '//' + parts.hostname + ':' + parts.port) || ~this._origins.indexOf(parts.hostname + ':' + parts.port) || ~this._origins.indexOf(parts.hostname + ':*') || ~this._origins.indexOf('*:' + parts.port); debug('origin %s is %svalid', origin, !!ok ? '' : 'not '); return fn(null, !!ok); } catch (ex) { } } fn(null, false); }; /** * Sets/gets whether client code is being served. * * @param {Boolean} v whether to serve client code * @return {Server|Boolean} self when setting or value when getting * @api public */ Server.prototype.serveClient = function(v){ if (!arguments.length) return this._serveClient; this._serveClient = v; var resolvePath = function(file){ var filepath = path.resolve(__dirname, './../../', file); if (exists(filepath)) { return filepath; } return require.resolve(file); }; if (v && !clientSource) { clientSource = read(resolvePath( 'socket.io-client/dist/socket.io.js'), 'utf-8'); try { clientSourceMap = read(resolvePath( 'socket.io-client/dist/socket.io.js.map'), 'utf-8'); } catch(err) { debug('could not load sourcemap file'); } } return this; }; /** * Old settings for backwards compatibility */ var oldSettings = { "transports": "transports", "heartbeat timeout": "pingTimeout", "heartbeat interval": "pingInterval", "destroy buffer size": "maxHttpBufferSize" }; /** * Backwards compatibility. * * @api public */ Server.prototype.set = function(key, val){ if ('authorization' == key && val) { this.use(function(socket, next) { val(socket.request, function(err, authorized) { if (err) return next(new Error(err)); if (!authorized) return next(new Error('Not authorized')); next(); }); }); } else if ('origins' == key && val) { this.origins(val); } else if ('resource' == key) { this.path(val); } else if (oldSettings[key] && this.eio[oldSettings[key]]) { this.eio[oldSettings[key]] = val; } else { console.error('Option %s is not valid. Please refer to the README.', key); } return this; }; /** * Executes the middleware for an incoming namespace not already created on the server. * * @param {String} name name of incoming namespace * @param {Object} query the query parameters * @param {Function} fn callback * @api private */ Server.prototype.checkNamespace = function(name, query, fn){ if (this.parentNsps.size === 0) return fn(false); const keysIterator = this.parentNsps.keys(); const run = () => { let nextFn = keysIterator.next(); if (nextFn.done) { return fn(false); } nextFn.value(name, query, (err, allow) => { if (err || !allow) { run(); } else { fn(this.parentNsps.get(nextFn.value).createChild(name)); } }); }; run(); }; /** * Sets the client serving path. * * @param {String} v pathname * @return {Server|String} self when setting or value when getting * @api public */ Server.prototype.path = function(v){ if (!arguments.length) return this._path; this._path = v.replace(/\/$/, ''); return this; }; /** * Sets the adapter for rooms. * * @param {Adapter} v pathname * @return {Server|Adapter} self when setting or value when getting * @api public */ Server.prototype.adapter = function(v){ if (!arguments.length) return this._adapter; this._adapter = v; for (var i in this.nsps) { if (this.nsps.hasOwnProperty(i)) { this.nsps[i].initAdapter(); } } return this; }; /** * Sets the allowed origins for requests. * * @param {String|String[]} v origins * @return {Server|Adapter} self when setting or value when getting * @api public */ Server.prototype.origins = function(v){ if (!arguments.length) return this._origins; this._origins = v; return this; }; /** * Attaches socket.io to a server or port. * * @param {http.Server|Number} server or port * @param {Object} options passed to engine.io * @return {Server} self * @api public */ Server.prototype.listen = Server.prototype.attach = function(srv, opts){ if ('function' == typeof srv) { var msg = 'You are trying to attach socket.io to an express ' + 'request handler function. Please pass a http.Server instance.'; throw new Error(msg); } // handle a port as a string if (Number(srv) == srv) { srv = Number(srv); } if ('number' == typeof srv) { debug('creating http server and binding to %d', srv); var port = srv; srv = http.Server(function(req, res){ res.writeHead(404); res.end(); }); srv.listen(port); } // set engine.io path to `/socket.io` opts = opts || {}; opts.path = opts.path || this.path(); // set origins verification opts.allowRequest = opts.allowRequest || this.checkRequest.bind(this); if (this.sockets.fns.length > 0) { this.initEngine(srv, opts); return this; } var self = this; var connectPacket = { type: parser.CONNECT, nsp: '/' }; this.encoder.encode(connectPacket, function (encodedPacket){ // the CONNECT packet will be merged with Engine.IO handshake, // to reduce the number of round trips opts.initialPacket = encodedPacket; self.initEngine(srv, opts); }); return this; }; /** * Initialize engine * * @param {Object} options passed to engine.io * @api private */ Server.prototype.initEngine = function(srv, opts){ // initialize engine debug('creating engine.io instance with opts %j', opts); this.eio = engine.attach(srv, opts); // attach static file serving if (this._serveClient) this.attachServe(srv); // Export http server this.httpServer = srv; // bind to engine events this.bind(this.eio); }; /** * Attaches the static file serving. * * @param {Function|http.Server} srv http server * @api private */ Server.prototype.attachServe = function(srv){ debug('attaching client serving req handler'); var url = this._path + '/socket.io.js'; var urlMap = this._path + '/socket.io.js.map'; var evs = srv.listeners('request').slice(0); var self = this; srv.removeAllListeners('request'); srv.on('request', function(req, res) { if (0 === req.url.indexOf(urlMap)) { self.serveMap(req, res); } else if (0 === req.url.indexOf(url)) { self.serve(req, res); } else { for (var i = 0; i < evs.length; i++) { evs[i].call(srv, req, res); } } }); }; /** * Handles a request serving `/socket.io.js` * * @param {http.Request} req * @param {http.Response} res * @api private */ Server.prototype.serve = function(req, res){ // Per the standard, ETags must be quoted: // https://tools.ietf.org/html/rfc7232#section-2.3 var expectedEtag = '"' + clientVersion + '"'; var etag = req.headers['if-none-match']; if (etag) { if (expectedEtag == etag) { debug('serve client 304'); res.writeHead(304); res.end(); return; } } debug('serve client source'); res.setHeader('Content-Type', 'application/javascript'); res.setHeader('ETag', expectedEtag); res.writeHead(200); res.end(clientSource); }; /** * Handles a request serving `/socket.io.js.map` * * @param {http.Request} req * @param {http.Response} res * @api private */ Server.prototype.serveMap = function(req, res){ // Per the standard, ETags must be quoted: // https://tools.ietf.org/html/rfc7232#section-2.3 var expectedEtag = '"' + clientVersion + '"'; var etag = req.headers['if-none-match']; if (etag) { if (expectedEtag == etag) { debug('serve client 304'); res.writeHead(304); res.end(); return; } } debug('serve client sourcemap'); res.setHeader('Content-Type', 'application/json'); res.setHeader('ETag', expectedEtag); res.writeHead(200); res.end(clientSourceMap); }; /** * Binds socket.io to an engine.io instance. * * @param {engine.Server} engine engine.io (or compatible) server * @return {Server} self * @api public */ Server.prototype.bind = function(engine){ this.engine = engine; this.engine.on('connection', this.onconnection.bind(this)); return this; }; /** * Called with each incoming transport connection. * * @param {engine.Socket} conn * @return {Server} self * @api public */ Server.prototype.onconnection = function(conn){ debug('incoming connection with id %s', conn.id); var client = new Client(this, conn); client.connect('/'); return this; }; /** * Looks up a namespace. * * @param {String|RegExp|Function} name nsp name * @param {Function} [fn] optional, nsp `connection` ev handler * @api public */ Server.prototype.of = function(name, fn){ if (typeof name === 'function' || name instanceof RegExp) { const parentNsp = new ParentNamespace(this); debug('initializing parent namespace %s', parentNsp.name); if (typeof name === 'function') { this.parentNsps.set(name, parentNsp); } else { this.parentNsps.set((nsp, conn, next) => next(null, name.test(nsp)), parentNsp); } if (fn) parentNsp.on('connect', fn); return parentNsp; } if (String(name)[0] !== '/') name = '/' + name; var nsp = this.nsps[name]; if (!nsp) { debug('initializing namespace %s', name); nsp = new Namespace(this, name); this.nsps[name] = nsp; } if (fn) nsp.on('connect', fn); return nsp; }; /** * Closes server connection * * @param {Function} [fn] optional, called as `fn([err])` on error OR all conns closed * @api public */ Server.prototype.close = function(fn){ for (var id in this.nsps['/'].sockets) { if (this.nsps['/'].sockets.hasOwnProperty(id)) { this.nsps['/'].sockets[id].onclose(); } } this.engine.close(); if (this.httpServer) { this.httpServer.close(fn); } else { fn && fn(); } }; /** * Expose main namespace (/). */ var emitterMethods = Object.keys(Emitter.prototype).filter(function(key){ return typeof Emitter.prototype[key] === 'function'; }); emitterMethods.concat(['to', 'in', 'use', 'send', 'write', 'clients', 'compress', 'binary']).forEach(function(fn){ Server.prototype[fn] = function(){ return this.sockets[fn].apply(this.sockets, arguments); }; }); Namespace.flags.forEach(function(flag){ Object.defineProperty(Server.prototype, flag, { get: function() { this.sockets.flags = this.sockets.flags || {}; this.sockets.flags[flag] = true; return this; } }); }); /** * BC with `io.listen` */ Server.listen = Server;