[Service] Integrate DeviceHome and SignalingServer
[platform/framework/web/wrtjs.git] / device_home / node_modules / engine.io / lib / server.js
1
2 /**
3  * Module dependencies.
4  */
5
6 var qs = require('querystring');
7 var parse = require('url').parse;
8 var base64id = require('base64id');
9 var transports = require('./transports');
10 var EventEmitter = require('events').EventEmitter;
11 var Socket = require('./socket');
12 var util = require('util');
13 var debug = require('debug')('engine');
14 var cookieMod = require('cookie');
15
16 /**
17  * Module exports.
18  */
19
20 module.exports = Server;
21
22 /**
23  * Server constructor.
24  *
25  * @param {Object} options
26  * @api public
27  */
28
29 function Server (opts) {
30   if (!(this instanceof Server)) {
31     return new Server(opts);
32   }
33
34   this.clients = {};
35   this.clientsCount = 0;
36
37   opts = opts || {};
38
39   this.wsEngine = opts.wsEngine || process.env.EIO_WS_ENGINE || 'ws';
40   this.pingTimeout = opts.pingTimeout || 5000;
41   this.pingInterval = opts.pingInterval || 25000;
42   this.upgradeTimeout = opts.upgradeTimeout || 10000;
43   this.maxHttpBufferSize = opts.maxHttpBufferSize || 10E7;
44   this.transports = opts.transports || Object.keys(transports);
45   this.allowUpgrades = false !== opts.allowUpgrades;
46   this.allowRequest = opts.allowRequest;
47   this.cookie = false !== opts.cookie ? (opts.cookie || 'io') : false;
48   this.cookiePath = false !== opts.cookiePath ? (opts.cookiePath || '/') : false;
49   this.cookieHttpOnly = false !== opts.cookieHttpOnly;
50   this.perMessageDeflate = opts.perMessageDeflate || false;
51   this.httpCompression = false !== opts.httpCompression ? (opts.httpCompression || {}) : false;
52   this.initialPacket = opts.initialPacket;
53
54   var self = this;
55
56   // initialize compression options
57   ['perMessageDeflate', 'httpCompression'].forEach(function (type) {
58     var compression = self[type];
59     if (true === compression) self[type] = compression = {};
60     if (compression && null == compression.threshold) {
61       compression.threshold = 1024;
62     }
63   });
64
65   this.init();
66 }
67
68 /**
69  * Protocol errors mappings.
70  */
71
72 Server.errors = {
73   UNKNOWN_TRANSPORT: 0,
74   UNKNOWN_SID: 1,
75   BAD_HANDSHAKE_METHOD: 2,
76   BAD_REQUEST: 3,
77   FORBIDDEN: 4
78 };
79
80 Server.errorMessages = {
81   0: 'Transport unknown',
82   1: 'Session ID unknown',
83   2: 'Bad handshake method',
84   3: 'Bad request',
85   4: 'Forbidden'
86 };
87
88 /**
89  * Inherits from EventEmitter.
90  */
91
92 util.inherits(Server, EventEmitter);
93
94 /**
95  * Initialize websocket server
96  *
97  * @api private
98  */
99
100 Server.prototype.init = function () {
101   if (!~this.transports.indexOf('websocket')) return;
102
103   if (this.ws) this.ws.close();
104
105   // add explicit require for bundlers like webpack
106   var wsModule = this.wsEngine === 'ws' ? require('ws') : require(this.wsEngine);
107   this.ws = new wsModule.Server({
108     noServer: true,
109     clientTracking: false,
110     perMessageDeflate: this.perMessageDeflate,
111     maxPayload: this.maxHttpBufferSize
112   });
113 };
114
115 /**
116  * Returns a list of available transports for upgrade given a certain transport.
117  *
118  * @return {Array}
119  * @api public
120  */
121
122 Server.prototype.upgrades = function (transport) {
123   if (!this.allowUpgrades) return [];
124   return transports[transport].upgradesTo || [];
125 };
126
127 /**
128  * Verifies a request.
129  *
130  * @param {http.IncomingMessage}
131  * @return {Boolean} whether the request is valid
132  * @api private
133  */
134
135 Server.prototype.verify = function (req, upgrade, fn) {
136   // transport check
137   var transport = req._query.transport;
138   if (!~this.transports.indexOf(transport)) {
139     debug('unknown transport "%s"', transport);
140     return fn(Server.errors.UNKNOWN_TRANSPORT, false);
141   }
142
143   // 'Origin' header check
144   var isOriginInvalid = checkInvalidHeaderChar(req.headers.origin);
145   if (isOriginInvalid) {
146     req.headers.origin = null;
147     debug('origin header invalid');
148     return fn(Server.errors.BAD_REQUEST, false);
149   }
150
151   // sid check
152   var sid = req._query.sid;
153   if (sid) {
154     if (!this.clients.hasOwnProperty(sid)) {
155       debug('unknown sid "%s"', sid);
156       return fn(Server.errors.UNKNOWN_SID, false);
157     }
158     if (!upgrade && this.clients[sid].transport.name !== transport) {
159       debug('bad request: unexpected transport without upgrade');
160       return fn(Server.errors.BAD_REQUEST, false);
161     }
162   } else {
163     // handshake is GET only
164     if ('GET' !== req.method) return fn(Server.errors.BAD_HANDSHAKE_METHOD, false);
165     if (!this.allowRequest) return fn(null, true);
166     return this.allowRequest(req, fn);
167   }
168
169   fn(null, true);
170 };
171
172 /**
173  * Prepares a request by processing the query string.
174  *
175  * @api private
176  */
177
178 Server.prototype.prepare = function (req) {
179   // try to leverage pre-existing `req._query` (e.g: from connect)
180   if (!req._query) {
181     req._query = ~req.url.indexOf('?') ? qs.parse(parse(req.url).query) : {};
182   }
183 };
184
185 /**
186  * Closes all clients.
187  *
188  * @api public
189  */
190
191 Server.prototype.close = function () {
192   debug('closing all open clients');
193   for (var i in this.clients) {
194     if (this.clients.hasOwnProperty(i)) {
195       this.clients[i].close(true);
196     }
197   }
198   if (this.ws) {
199     debug('closing webSocketServer');
200     this.ws.close();
201     // don't delete this.ws because it can be used again if the http server starts listening again
202   }
203   return this;
204 };
205
206 /**
207  * Handles an Engine.IO HTTP request.
208  *
209  * @param {http.IncomingMessage} request
210  * @param {http.ServerResponse|http.OutgoingMessage} response
211  * @api public
212  */
213
214 Server.prototype.handleRequest = function (req, res) {
215   debug('handling "%s" http request "%s"', req.method, req.url);
216   this.prepare(req);
217   req.res = res;
218
219   var self = this;
220   this.verify(req, false, function (err, success) {
221     if (!success) {
222       sendErrorMessage(req, res, err);
223       return;
224     }
225
226     if (req._query.sid) {
227       debug('setting new request for existing client');
228       self.clients[req._query.sid].transport.onRequest(req);
229     } else {
230       self.handshake(req._query.transport, req);
231     }
232   });
233 };
234
235 /**
236  * Sends an Engine.IO Error Message
237  *
238  * @param {http.ServerResponse} response
239  * @param {code} error code
240  * @api private
241  */
242
243 function sendErrorMessage (req, res, code) {
244   var headers = { 'Content-Type': 'application/json' };
245
246   var isForbidden = !Server.errorMessages.hasOwnProperty(code);
247   if (isForbidden) {
248     res.writeHead(403, headers);
249     res.end(JSON.stringify({
250       code: Server.errors.FORBIDDEN,
251       message: code || Server.errorMessages[Server.errors.FORBIDDEN]
252     }));
253     return;
254   }
255   if (req.headers.origin) {
256     headers['Access-Control-Allow-Credentials'] = 'true';
257     headers['Access-Control-Allow-Origin'] = req.headers.origin;
258   } else {
259     headers['Access-Control-Allow-Origin'] = '*';
260   }
261   if (res !== undefined) {
262     res.writeHead(400, headers);
263     res.end(JSON.stringify({
264       code: code,
265       message: Server.errorMessages[code]
266     }));
267   }
268 }
269
270 /**
271  * generate a socket id.
272  * Overwrite this method to generate your custom socket id
273  *
274  * @param {Object} request object
275  * @api public
276  */
277
278 Server.prototype.generateId = function (req) {
279   return base64id.generateId();
280 };
281
282 /**
283  * Handshakes a new client.
284  *
285  * @param {String} transport name
286  * @param {Object} request object
287  * @api private
288  */
289
290 Server.prototype.handshake = function (transportName, req) {
291   var id = this.generateId(req);
292
293   debug('handshaking client "%s"', id);
294
295   try {
296     var transport = new transports[transportName](req);
297     if ('polling' === transportName) {
298       transport.maxHttpBufferSize = this.maxHttpBufferSize;
299       transport.httpCompression = this.httpCompression;
300     } else if ('websocket' === transportName) {
301       transport.perMessageDeflate = this.perMessageDeflate;
302     }
303
304     if (req._query && req._query.b64) {
305       transport.supportsBinary = false;
306     } else {
307       transport.supportsBinary = true;
308     }
309   } catch (e) {
310     debug('error handshaking to transport "%s"', transportName);
311     sendErrorMessage(req, req.res, Server.errors.BAD_REQUEST);
312     return;
313   }
314   var socket = new Socket(id, this, transport, req);
315   var self = this;
316
317   if (false !== this.cookie) {
318     transport.on('headers', function (headers) {
319       if (typeof self.cookie === 'object') {
320         headers['Set-Cookie'] = cookieMod.serialize(self.cookie.name, id, self.cookie);
321       } else {
322         headers['Set-Cookie'] = cookieMod.serialize(self.cookie, id,
323           {
324             path: self.cookiePath,
325             httpOnly: self.cookiePath ? self.cookieHttpOnly : false,
326             sameSite: true
327           });
328       }
329     });
330   }
331
332   transport.onRequest(req);
333
334   this.clients[id] = socket;
335   this.clientsCount++;
336
337   socket.once('close', function () {
338     delete self.clients[id];
339     self.clientsCount--;
340   });
341
342   this.emit('connection', socket);
343 };
344
345 /**
346  * Handles an Engine.IO HTTP Upgrade.
347  *
348  * @api public
349  */
350
351 Server.prototype.handleUpgrade = function (req, socket, upgradeHead) {
352   this.prepare(req);
353
354   var self = this;
355   this.verify(req, true, function (err, success) {
356     if (!success) {
357       abortConnection(socket, err);
358       return;
359     }
360
361     var head = Buffer.from(upgradeHead); // eslint-disable-line node/no-deprecated-api
362     upgradeHead = null;
363
364     // delegate to ws
365     self.ws.handleUpgrade(req, socket, head, function (conn) {
366       self.onWebSocket(req, conn);
367     });
368   });
369 };
370
371 /**
372  * Called upon a ws.io connection.
373  *
374  * @param {ws.Socket} websocket
375  * @api private
376  */
377
378 Server.prototype.onWebSocket = function (req, socket) {
379   socket.on('error', onUpgradeError);
380
381   if (transports[req._query.transport] !== undefined && !transports[req._query.transport].prototype.handlesUpgrades) {
382     debug('transport doesnt handle upgraded requests');
383     socket.close();
384     return;
385   }
386
387   // get client id
388   var id = req._query.sid;
389
390   // keep a reference to the ws.Socket
391   req.websocket = socket;
392
393   if (id) {
394     var client = this.clients[id];
395     if (!client) {
396       debug('upgrade attempt for closed client');
397       socket.close();
398     } else if (client.upgrading) {
399       debug('transport has already been trying to upgrade');
400       socket.close();
401     } else if (client.upgraded) {
402       debug('transport had already been upgraded');
403       socket.close();
404     } else {
405       debug('upgrading existing transport');
406
407       // transport error handling takes over
408       socket.removeListener('error', onUpgradeError);
409
410       var transport = new transports[req._query.transport](req);
411       if (req._query && req._query.b64) {
412         transport.supportsBinary = false;
413       } else {
414         transport.supportsBinary = true;
415       }
416       transport.perMessageDeflate = this.perMessageDeflate;
417       client.maybeUpgrade(transport);
418     }
419   } else {
420     // transport error handling takes over
421     socket.removeListener('error', onUpgradeError);
422
423     this.handshake(req._query.transport, req);
424   }
425
426   function onUpgradeError () {
427     debug('websocket error before upgrade');
428     // socket.close() not needed
429   }
430 };
431
432 /**
433  * Captures upgrade requests for a http.Server.
434  *
435  * @param {http.Server} server
436  * @param {Object} options
437  * @api public
438  */
439
440 Server.prototype.attach = function (server, options) {
441   var self = this;
442   options = options || {};
443   var path = (options.path || '/engine.io').replace(/\/$/, '');
444
445   var destroyUpgradeTimeout = options.destroyUpgradeTimeout || 1000;
446
447   // normalize path
448   path += '/';
449
450   function check (req) {
451     if ('OPTIONS' === req.method && false === options.handlePreflightRequest) {
452       return false;
453     }
454     return path === req.url.substr(0, path.length);
455   }
456
457   // cache and clean up listeners
458   var listeners = server.listeners('request').slice(0);
459   server.removeAllListeners('request');
460   server.on('close', self.close.bind(self));
461   server.on('listening', self.init.bind(self));
462
463   // add request handler
464   server.on('request', function (req, res) {
465     if (check(req)) {
466       debug('intercepting request for path "%s"', path);
467       if ('OPTIONS' === req.method && 'function' === typeof options.handlePreflightRequest) {
468         options.handlePreflightRequest.call(server, req, res);
469       } else {
470         self.handleRequest(req, res);
471       }
472     } else {
473       for (var i = 0, l = listeners.length; i < l; i++) {
474         listeners[i].call(server, req, res);
475       }
476     }
477   });
478
479   if (~self.transports.indexOf('websocket')) {
480     server.on('upgrade', function (req, socket, head) {
481       if (check(req)) {
482         self.handleUpgrade(req, socket, head);
483       } else if (false !== options.destroyUpgrade) {
484         // default node behavior is to disconnect when no handlers
485         // but by adding a handler, we prevent that
486         // and if no eio thing handles the upgrade
487         // then the socket needs to die!
488         setTimeout(function () {
489           if (socket.writable && socket.bytesWritten <= 0) {
490             return socket.end();
491           }
492         }, destroyUpgradeTimeout);
493       }
494     });
495   }
496 };
497
498 /**
499  * Closes the connection
500  *
501  * @param {net.Socket} socket
502  * @param {code} error code
503  * @api private
504  */
505
506 function abortConnection (socket, code) {
507   socket.on('error', () => {
508     debug('ignoring error from closed connection');
509   });
510   if (socket.writable) {
511     var message = Server.errorMessages.hasOwnProperty(code) ? Server.errorMessages[code] : String(code || '');
512     var length = Buffer.byteLength(message);
513     socket.write(
514       'HTTP/1.1 400 Bad Request\r\n' +
515       'Connection: close\r\n' +
516       'Content-type: text/html\r\n' +
517       'Content-Length: ' + length + '\r\n' +
518       '\r\n' +
519       message
520     );
521   }
522   socket.destroy();
523 }
524
525 /* eslint-disable */
526
527 /**
528  * From https://github.com/nodejs/node/blob/v8.4.0/lib/_http_common.js#L303-L354
529  *
530  * True if val contains an invalid field-vchar
531  *  field-value    = *( field-content / obs-fold )
532  *  field-content  = field-vchar [ 1*( SP / HTAB ) field-vchar ]
533  *  field-vchar    = VCHAR / obs-text
534  *
535  * checkInvalidHeaderChar() is currently designed to be inlinable by v8,
536  * so take care when making changes to the implementation so that the source
537  * code size does not exceed v8's default max_inlined_source_size setting.
538  **/
539 var validHdrChars = [
540   0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, // 0 - 15
541   0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31
542   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 32 - 47
543   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 48 - 63
544   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79
545   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 80 - 95
546   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111
547   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, // 112 - 127
548   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 128 ...
549   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
550   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
551   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
552   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
553   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
554   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
555   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1  // ... 255
556 ];
557
558 function checkInvalidHeaderChar(val) {
559   val += '';
560   if (val.length < 1)
561     return false;
562   if (!validHdrChars[val.charCodeAt(0)]) {
563     debug('invalid header, index 0, char "%s"', val.charCodeAt(0));
564     return true;
565   }
566   if (val.length < 2)
567     return false;
568   if (!validHdrChars[val.charCodeAt(1)]) {
569     debug('invalid header, index 1, char "%s"', val.charCodeAt(1));
570     return true;
571   }
572   if (val.length < 3)
573     return false;
574   if (!validHdrChars[val.charCodeAt(2)]) {
575     debug('invalid header, index 2, char "%s"', val.charCodeAt(2));
576     return true;
577   }
578   if (val.length < 4)
579     return false;
580   if (!validHdrChars[val.charCodeAt(3)]) {
581     debug('invalid header, index 3, char "%s"', val.charCodeAt(3));
582     return true;
583   }
584   for (var i = 4; i < val.length; ++i) {
585     if (!validHdrChars[val.charCodeAt(i)]) {
586       debug('invalid header, index "%i", char "%s"', i, val.charCodeAt(i));
587       return true;
588     }
589   }
590   return false;
591 }