UNPKG

12.9 kBJavaScriptView Raw
1'use strict';
2
3/**
4 * Module dependencies.
5 */
6
7var http = require('http');
8var read = require('fs').readFileSync;
9var path = require('path');
10var exists = require('fs').existsSync;
11var engine = require('engine.io');
12var clientVersion = require('socket.io-client/package.json').version;
13var Client = require('./client');
14var Emitter = require('events').EventEmitter;
15var Namespace = require('./namespace');
16var ParentNamespace = require('./parent-namespace');
17var Adapter = require('socket.io-adapter');
18var parser = require('socket.io-parser');
19var debug = require('debug')('socket.io:server');
20var url = require('url');
21
22/**
23 * Module exports.
24 */
25
26module.exports = Server;
27
28/**
29 * Socket.IO client source.
30 */
31
32var clientSource = undefined;
33var 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
43function 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
69Server.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
105Server.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
130var 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
143Server.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
174Server.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
204Server.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
218Server.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
237Server.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
253Server.prototype.listen =
254Server.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
307Server.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
329Server.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
357Server.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
388Server.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
418Server.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
432Server.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
447Server.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
479Server.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
499var emitterMethods = Object.keys(Emitter.prototype).filter(function(key){
500 return typeof Emitter.prototype[key] === 'function';
501});
502
503emitterMethods.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
509Namespace.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
523Server.listen = Server;