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');
20 module.exports = Server;
25 * @param {Object} options
29 function Server (opts) {
30 if (!(this instanceof Server)) {
31 return new Server(opts);
35 this.clientsCount = 0;
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;
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;
69 * Protocol errors mappings.
75 BAD_HANDSHAKE_METHOD: 2,
80 Server.errorMessages = {
81 0: 'Transport unknown',
82 1: 'Session ID unknown',
83 2: 'Bad handshake method',
89 * Inherits from EventEmitter.
92 util.inherits(Server, EventEmitter);
95 * Initialize websocket server
100 Server.prototype.init = function () {
101 if (!~this.transports.indexOf('websocket')) return;
103 if (this.ws) this.ws.close();
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({
109 clientTracking: false,
110 perMessageDeflate: this.perMessageDeflate,
111 maxPayload: this.maxHttpBufferSize
116 * Returns a list of available transports for upgrade given a certain transport.
122 Server.prototype.upgrades = function (transport) {
123 if (!this.allowUpgrades) return [];
124 return transports[transport].upgradesTo || [];
128 * Verifies a request.
130 * @param {http.IncomingMessage}
131 * @return {Boolean} whether the request is valid
135 Server.prototype.verify = function (req, upgrade, fn) {
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);
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);
152 var sid = req._query.sid;
154 if (!this.clients.hasOwnProperty(sid)) {
155 debug('unknown sid "%s"', sid);
156 return fn(Server.errors.UNKNOWN_SID, false);
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);
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);
173 * Prepares a request by processing the query string.
178 Server.prototype.prepare = function (req) {
179 // try to leverage pre-existing `req._query` (e.g: from connect)
181 req._query = ~req.url.indexOf('?') ? qs.parse(parse(req.url).query) : {};
186 * Closes all clients.
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);
199 debug('closing webSocketServer');
201 // don't delete this.ws because it can be used again if the http server starts listening again
207 * Handles an Engine.IO HTTP request.
209 * @param {http.IncomingMessage} request
210 * @param {http.ServerResponse|http.OutgoingMessage} response
214 Server.prototype.handleRequest = function (req, res) {
215 debug('handling "%s" http request "%s"', req.method, req.url);
220 this.verify(req, false, function (err, success) {
222 sendErrorMessage(req, res, err);
226 if (req._query.sid) {
227 debug('setting new request for existing client');
228 self.clients[req._query.sid].transport.onRequest(req);
230 self.handshake(req._query.transport, req);
236 * Sends an Engine.IO Error Message
238 * @param {http.ServerResponse} response
239 * @param {code} error code
243 function sendErrorMessage (req, res, code) {
244 var headers = { 'Content-Type': 'application/json' };
246 var isForbidden = !Server.errorMessages.hasOwnProperty(code);
248 res.writeHead(403, headers);
249 res.end(JSON.stringify({
250 code: Server.errors.FORBIDDEN,
251 message: code || Server.errorMessages[Server.errors.FORBIDDEN]
255 if (req.headers.origin) {
256 headers['Access-Control-Allow-Credentials'] = 'true';
257 headers['Access-Control-Allow-Origin'] = req.headers.origin;
259 headers['Access-Control-Allow-Origin'] = '*';
261 if (res !== undefined) {
262 res.writeHead(400, headers);
263 res.end(JSON.stringify({
265 message: Server.errorMessages[code]
271 * generate a socket id.
272 * Overwrite this method to generate your custom socket id
274 * @param {Object} request object
278 Server.prototype.generateId = function (req) {
279 return base64id.generateId();
283 * Handshakes a new client.
285 * @param {String} transport name
286 * @param {Object} request object
290 Server.prototype.handshake = function (transportName, req) {
291 var id = this.generateId(req);
293 debug('handshaking client "%s"', id);
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;
304 if (req._query && req._query.b64) {
305 transport.supportsBinary = false;
307 transport.supportsBinary = true;
310 debug('error handshaking to transport "%s"', transportName);
311 sendErrorMessage(req, req.res, Server.errors.BAD_REQUEST);
314 var socket = new Socket(id, this, transport, req);
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);
322 headers['Set-Cookie'] = cookieMod.serialize(self.cookie, id,
324 path: self.cookiePath,
325 httpOnly: self.cookiePath ? self.cookieHttpOnly : false,
332 transport.onRequest(req);
334 this.clients[id] = socket;
337 socket.once('close', function () {
338 delete self.clients[id];
342 this.emit('connection', socket);
346 * Handles an Engine.IO HTTP Upgrade.
351 Server.prototype.handleUpgrade = function (req, socket, upgradeHead) {
355 this.verify(req, true, function (err, success) {
357 abortConnection(socket, err);
361 var head = Buffer.from(upgradeHead); // eslint-disable-line node/no-deprecated-api
365 self.ws.handleUpgrade(req, socket, head, function (conn) {
366 self.onWebSocket(req, conn);
372 * Called upon a ws.io connection.
374 * @param {ws.Socket} websocket
378 Server.prototype.onWebSocket = function (req, socket) {
379 socket.on('error', onUpgradeError);
381 if (transports[req._query.transport] !== undefined && !transports[req._query.transport].prototype.handlesUpgrades) {
382 debug('transport doesnt handle upgraded requests');
388 var id = req._query.sid;
390 // keep a reference to the ws.Socket
391 req.websocket = socket;
394 var client = this.clients[id];
396 debug('upgrade attempt for closed client');
398 } else if (client.upgrading) {
399 debug('transport has already been trying to upgrade');
401 } else if (client.upgraded) {
402 debug('transport had already been upgraded');
405 debug('upgrading existing transport');
407 // transport error handling takes over
408 socket.removeListener('error', onUpgradeError);
410 var transport = new transports[req._query.transport](req);
411 if (req._query && req._query.b64) {
412 transport.supportsBinary = false;
414 transport.supportsBinary = true;
416 transport.perMessageDeflate = this.perMessageDeflate;
417 client.maybeUpgrade(transport);
420 // transport error handling takes over
421 socket.removeListener('error', onUpgradeError);
423 this.handshake(req._query.transport, req);
426 function onUpgradeError () {
427 debug('websocket error before upgrade');
428 // socket.close() not needed
433 * Captures upgrade requests for a http.Server.
435 * @param {http.Server} server
436 * @param {Object} options
440 Server.prototype.attach = function (server, options) {
442 options = options || {};
443 var path = (options.path || '/engine.io').replace(/\/$/, '');
445 var destroyUpgradeTimeout = options.destroyUpgradeTimeout || 1000;
450 function check (req) {
451 if ('OPTIONS' === req.method && false === options.handlePreflightRequest) {
454 return path === req.url.substr(0, path.length);
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));
463 // add request handler
464 server.on('request', function (req, res) {
466 debug('intercepting request for path "%s"', path);
467 if ('OPTIONS' === req.method && 'function' === typeof options.handlePreflightRequest) {
468 options.handlePreflightRequest.call(server, req, res);
470 self.handleRequest(req, res);
473 for (var i = 0, l = listeners.length; i < l; i++) {
474 listeners[i].call(server, req, res);
479 if (~self.transports.indexOf('websocket')) {
480 server.on('upgrade', function (req, socket, head) {
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) {
492 }, destroyUpgradeTimeout);
499 * Closes the connection
501 * @param {net.Socket} socket
502 * @param {code} error code
506 function abortConnection (socket, code) {
507 socket.on('error', () => {
508 debug('ignoring error from closed connection');
510 if (socket.writable) {
511 var message = Server.errorMessages.hasOwnProperty(code) ? Server.errorMessages[code] : String(code || '');
512 var length = Buffer.byteLength(message);
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' +
528 * From https://github.com/nodejs/node/blob/v8.4.0/lib/_http_common.js#L303-L354
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
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.
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
558 function checkInvalidHeaderChar(val) {
562 if (!validHdrChars[val.charCodeAt(0)]) {
563 debug('invalid header, index 0, char "%s"', val.charCodeAt(0));
568 if (!validHdrChars[val.charCodeAt(1)]) {
569 debug('invalid header, index 1, char "%s"', val.charCodeAt(1));
574 if (!validHdrChars[val.charCodeAt(2)]) {
575 debug('invalid header, index 2, char "%s"', val.charCodeAt(2));
580 if (!validHdrChars[val.charCodeAt(3)]) {
581 debug('invalid header, index 3, char "%s"', val.charCodeAt(3));
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));