index.js (12947B)
1 'use strict'; 2 3 /** 4 * Module dependencies. 5 */ 6 7 var http = require('http'); 8 var read = require('fs').readFileSync; 9 var path = require('path'); 10 var exists = require('fs').existsSync; 11 var engine = require('engine.io'); 12 var clientVersion = require('socket.io-client/package.json').version; 13 var Client = require('./client'); 14 var Emitter = require('events').EventEmitter; 15 var Namespace = require('./namespace'); 16 var ParentNamespace = require('./parent-namespace'); 17 var Adapter = require('socket.io-adapter'); 18 var parser = require('socket.io-parser'); 19 var debug = require('debug')('socket.io:server'); 20 var url = require('url'); 21 22 /** 23 * Module exports. 24 */ 25 26 module.exports = Server; 27 28 /** 29 * Socket.IO client source. 30 */ 31 32 var clientSource = undefined; 33 var clientSourceMap = undefined; 34 35 /** 36 * Server constructor. 37 * 38 * @param {http.Server|Number|Object} srv http server, port or options 39 * @param {Object} [opts] 40 * @api public 41 */ 42 43 function Server(srv, opts){ 44 if (!(this instanceof Server)) return new Server(srv, opts); 45 if ('object' == typeof srv && srv instanceof Object && !srv.listen) { 46 opts = srv; 47 srv = null; 48 } 49 opts = opts || {}; 50 this.nsps = {}; 51 this.parentNsps = new Map(); 52 this.path(opts.path || '/socket.io'); 53 this.serveClient(false !== opts.serveClient); 54 this.parser = opts.parser || parser; 55 this.encoder = new this.parser.Encoder(); 56 this.adapter(opts.adapter || Adapter); 57 this.origins(opts.origins || '*:*'); 58 this.sockets = this.of('/'); 59 if (srv) this.attach(srv, opts); 60 } 61 62 /** 63 * Server request verification function, that checks for allowed origins 64 * 65 * @param {http.IncomingMessage} req request 66 * @param {Function} fn callback to be called with the result: `fn(err, success)` 67 */ 68 69 Server.prototype.checkRequest = function(req, fn) { 70 var origin = req.headers.origin || req.headers.referer; 71 72 // file:// URLs produce a null Origin which can't be authorized via echo-back 73 if ('null' == origin || null == origin) origin = '*'; 74 75 if (!!origin && typeof(this._origins) == 'function') return this._origins(origin, fn); 76 if (this._origins.indexOf('*:*') !== -1) return fn(null, true); 77 if (origin) { 78 try { 79 var parts = url.parse(origin); 80 var defaultPort = 'https:' == parts.protocol ? 443 : 80; 81 parts.port = parts.port != null 82 ? parts.port 83 : defaultPort; 84 var ok = 85 ~this._origins.indexOf(parts.protocol + '//' + parts.hostname + ':' + parts.port) || 86 ~this._origins.indexOf(parts.hostname + ':' + parts.port) || 87 ~this._origins.indexOf(parts.hostname + ':*') || 88 ~this._origins.indexOf('*:' + parts.port); 89 debug('origin %s is %svalid', origin, !!ok ? '' : 'not '); 90 return fn(null, !!ok); 91 } catch (ex) { 92 } 93 } 94 fn(null, false); 95 }; 96 97 /** 98 * Sets/gets whether client code is being served. 99 * 100 * @param {Boolean} v whether to serve client code 101 * @return {Server|Boolean} self when setting or value when getting 102 * @api public 103 */ 104 105 Server.prototype.serveClient = function(v){ 106 if (!arguments.length) return this._serveClient; 107 this._serveClient = v; 108 var resolvePath = function(file){ 109 var filepath = path.resolve(__dirname, './../../', file); 110 if (exists(filepath)) { 111 return filepath; 112 } 113 return require.resolve(file); 114 }; 115 if (v && !clientSource) { 116 clientSource = read(resolvePath( 'socket.io-client/dist/socket.io.js'), 'utf-8'); 117 try { 118 clientSourceMap = read(resolvePath( 'socket.io-client/dist/socket.io.js.map'), 'utf-8'); 119 } catch(err) { 120 debug('could not load sourcemap file'); 121 } 122 } 123 return this; 124 }; 125 126 /** 127 * Old settings for backwards compatibility 128 */ 129 130 var oldSettings = { 131 "transports": "transports", 132 "heartbeat timeout": "pingTimeout", 133 "heartbeat interval": "pingInterval", 134 "destroy buffer size": "maxHttpBufferSize" 135 }; 136 137 /** 138 * Backwards compatibility. 139 * 140 * @api public 141 */ 142 143 Server.prototype.set = function(key, val){ 144 if ('authorization' == key && val) { 145 this.use(function(socket, next) { 146 val(socket.request, function(err, authorized) { 147 if (err) return next(new Error(err)); 148 if (!authorized) return next(new Error('Not authorized')); 149 next(); 150 }); 151 }); 152 } else if ('origins' == key && val) { 153 this.origins(val); 154 } else if ('resource' == key) { 155 this.path(val); 156 } else if (oldSettings[key] && this.eio[oldSettings[key]]) { 157 this.eio[oldSettings[key]] = val; 158 } else { 159 console.error('Option %s is not valid. Please refer to the README.', key); 160 } 161 162 return this; 163 }; 164 165 /** 166 * Executes the middleware for an incoming namespace not already created on the server. 167 * 168 * @param {String} name name of incoming namespace 169 * @param {Object} query the query parameters 170 * @param {Function} fn callback 171 * @api private 172 */ 173 174 Server.prototype.checkNamespace = function(name, query, fn){ 175 if (this.parentNsps.size === 0) return fn(false); 176 177 const keysIterator = this.parentNsps.keys(); 178 179 const run = () => { 180 let nextFn = keysIterator.next(); 181 if (nextFn.done) { 182 return fn(false); 183 } 184 nextFn.value(name, query, (err, allow) => { 185 if (err || !allow) { 186 run(); 187 } else { 188 fn(this.parentNsps.get(nextFn.value).createChild(name)); 189 } 190 }); 191 }; 192 193 run(); 194 }; 195 196 /** 197 * Sets the client serving path. 198 * 199 * @param {String} v pathname 200 * @return {Server|String} self when setting or value when getting 201 * @api public 202 */ 203 204 Server.prototype.path = function(v){ 205 if (!arguments.length) return this._path; 206 this._path = v.replace(/\/$/, ''); 207 return this; 208 }; 209 210 /** 211 * Sets the adapter for rooms. 212 * 213 * @param {Adapter} v pathname 214 * @return {Server|Adapter} self when setting or value when getting 215 * @api public 216 */ 217 218 Server.prototype.adapter = function(v){ 219 if (!arguments.length) return this._adapter; 220 this._adapter = v; 221 for (var i in this.nsps) { 222 if (this.nsps.hasOwnProperty(i)) { 223 this.nsps[i].initAdapter(); 224 } 225 } 226 return this; 227 }; 228 229 /** 230 * Sets the allowed origins for requests. 231 * 232 * @param {String|String[]} v origins 233 * @return {Server|Adapter} self when setting or value when getting 234 * @api public 235 */ 236 237 Server.prototype.origins = function(v){ 238 if (!arguments.length) return this._origins; 239 240 this._origins = v; 241 return this; 242 }; 243 244 /** 245 * Attaches socket.io to a server or port. 246 * 247 * @param {http.Server|Number} server or port 248 * @param {Object} options passed to engine.io 249 * @return {Server} self 250 * @api public 251 */ 252 253 Server.prototype.listen = 254 Server.prototype.attach = function(srv, opts){ 255 if ('function' == typeof srv) { 256 var msg = 'You are trying to attach socket.io to an express ' + 257 'request handler function. Please pass a http.Server instance.'; 258 throw new Error(msg); 259 } 260 261 // handle a port as a string 262 if (Number(srv) == srv) { 263 srv = Number(srv); 264 } 265 266 if ('number' == typeof srv) { 267 debug('creating http server and binding to %d', srv); 268 var port = srv; 269 srv = http.Server(function(req, res){ 270 res.writeHead(404); 271 res.end(); 272 }); 273 srv.listen(port); 274 275 } 276 277 // set engine.io path to `/socket.io` 278 opts = opts || {}; 279 opts.path = opts.path || this.path(); 280 // set origins verification 281 opts.allowRequest = opts.allowRequest || this.checkRequest.bind(this); 282 283 if (this.sockets.fns.length > 0) { 284 this.initEngine(srv, opts); 285 return this; 286 } 287 288 var self = this; 289 var connectPacket = { type: parser.CONNECT, nsp: '/' }; 290 this.encoder.encode(connectPacket, function (encodedPacket){ 291 // the CONNECT packet will be merged with Engine.IO handshake, 292 // to reduce the number of round trips 293 opts.initialPacket = encodedPacket; 294 295 self.initEngine(srv, opts); 296 }); 297 return this; 298 }; 299 300 /** 301 * Initialize engine 302 * 303 * @param {Object} options passed to engine.io 304 * @api private 305 */ 306 307 Server.prototype.initEngine = function(srv, opts){ 308 // initialize engine 309 debug('creating engine.io instance with opts %j', opts); 310 this.eio = engine.attach(srv, opts); 311 312 // attach static file serving 313 if (this._serveClient) this.attachServe(srv); 314 315 // Export http server 316 this.httpServer = srv; 317 318 // bind to engine events 319 this.bind(this.eio); 320 }; 321 322 /** 323 * Attaches the static file serving. 324 * 325 * @param {Function|http.Server} srv http server 326 * @api private 327 */ 328 329 Server.prototype.attachServe = function(srv){ 330 debug('attaching client serving req handler'); 331 var url = this._path + '/socket.io.js'; 332 var urlMap = this._path + '/socket.io.js.map'; 333 var evs = srv.listeners('request').slice(0); 334 var self = this; 335 srv.removeAllListeners('request'); 336 srv.on('request', function(req, res) { 337 if (0 === req.url.indexOf(urlMap)) { 338 self.serveMap(req, res); 339 } else if (0 === req.url.indexOf(url)) { 340 self.serve(req, res); 341 } else { 342 for (var i = 0; i < evs.length; i++) { 343 evs[i].call(srv, req, res); 344 } 345 } 346 }); 347 }; 348 349 /** 350 * Handles a request serving `/socket.io.js` 351 * 352 * @param {http.Request} req 353 * @param {http.Response} res 354 * @api private 355 */ 356 357 Server.prototype.serve = function(req, res){ 358 // Per the standard, ETags must be quoted: 359 // https://tools.ietf.org/html/rfc7232#section-2.3 360 var expectedEtag = '"' + clientVersion + '"'; 361 362 var etag = req.headers['if-none-match']; 363 if (etag) { 364 if (expectedEtag == etag) { 365 debug('serve client 304'); 366 res.writeHead(304); 367 res.end(); 368 return; 369 } 370 } 371 372 debug('serve client source'); 373 res.setHeader("Cache-Control", "public, max-age=0"); 374 res.setHeader('Content-Type', 'application/javascript'); 375 res.setHeader('ETag', expectedEtag); 376 res.writeHead(200); 377 res.end(clientSource); 378 }; 379 380 /** 381 * Handles a request serving `/socket.io.js.map` 382 * 383 * @param {http.Request} req 384 * @param {http.Response} res 385 * @api private 386 */ 387 388 Server.prototype.serveMap = function(req, res){ 389 // Per the standard, ETags must be quoted: 390 // https://tools.ietf.org/html/rfc7232#section-2.3 391 var expectedEtag = '"' + clientVersion + '"'; 392 393 var etag = req.headers['if-none-match']; 394 if (etag) { 395 if (expectedEtag == etag) { 396 debug('serve client 304'); 397 res.writeHead(304); 398 res.end(); 399 return; 400 } 401 } 402 403 debug('serve client sourcemap'); 404 res.setHeader('Content-Type', 'application/json'); 405 res.setHeader('ETag', expectedEtag); 406 res.writeHead(200); 407 res.end(clientSourceMap); 408 }; 409 410 /** 411 * Binds socket.io to an engine.io instance. 412 * 413 * @param {engine.Server} engine engine.io (or compatible) server 414 * @return {Server} self 415 * @api public 416 */ 417 418 Server.prototype.bind = function(engine){ 419 this.engine = engine; 420 this.engine.on('connection', this.onconnection.bind(this)); 421 return this; 422 }; 423 424 /** 425 * Called with each incoming transport connection. 426 * 427 * @param {engine.Socket} conn 428 * @return {Server} self 429 * @api public 430 */ 431 432 Server.prototype.onconnection = function(conn){ 433 debug('incoming connection with id %s', conn.id); 434 var client = new Client(this, conn); 435 client.connect('/'); 436 return this; 437 }; 438 439 /** 440 * Looks up a namespace. 441 * 442 * @param {String|RegExp|Function} name nsp name 443 * @param {Function} [fn] optional, nsp `connection` ev handler 444 * @api public 445 */ 446 447 Server.prototype.of = function(name, fn){ 448 if (typeof name === 'function' || name instanceof RegExp) { 449 const parentNsp = new ParentNamespace(this); 450 debug('initializing parent namespace %s', parentNsp.name); 451 if (typeof name === 'function') { 452 this.parentNsps.set(name, parentNsp); 453 } else { 454 this.parentNsps.set((nsp, conn, next) => next(null, name.test(nsp)), parentNsp); 455 } 456 if (fn) parentNsp.on('connect', fn); 457 return parentNsp; 458 } 459 460 if (String(name)[0] !== '/') name = '/' + name; 461 462 var nsp = this.nsps[name]; 463 if (!nsp) { 464 debug('initializing namespace %s', name); 465 nsp = new Namespace(this, name); 466 this.nsps[name] = nsp; 467 } 468 if (fn) nsp.on('connect', fn); 469 return nsp; 470 }; 471 472 /** 473 * Closes server connection 474 * 475 * @param {Function} [fn] optional, called as `fn([err])` on error OR all conns closed 476 * @api public 477 */ 478 479 Server.prototype.close = function(fn){ 480 for (var id in this.nsps['/'].sockets) { 481 if (this.nsps['/'].sockets.hasOwnProperty(id)) { 482 this.nsps['/'].sockets[id].onclose(); 483 } 484 } 485 486 this.engine.close(); 487 488 if (this.httpServer) { 489 this.httpServer.close(fn); 490 } else { 491 fn && fn(); 492 } 493 }; 494 495 /** 496 * Expose main namespace (/). 497 */ 498 499 var emitterMethods = Object.keys(Emitter.prototype).filter(function(key){ 500 return typeof Emitter.prototype[key] === 'function'; 501 }); 502 503 emitterMethods.concat(['to', 'in', 'use', 'send', 'write', 'clients', 'compress', 'binary']).forEach(function(fn){ 504 Server.prototype[fn] = function(){ 505 return this.sockets[fn].apply(this.sockets, arguments); 506 }; 507 }); 508 509 Namespace.flags.forEach(function(flag){ 510 Object.defineProperty(Server.prototype, flag, { 511 get: function() { 512 this.sockets.flags = this.sockets.flags || {}; 513 this.sockets.flags[flag] = true; 514 return this; 515 } 516 }); 517 }); 518 519 /** 520 * BC with `io.listen` 521 */ 522 523 Server.listen = Server;