123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523 |
- '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("Cache-Control", "public, max-age=0");
- 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;
|