server.js (15670B)
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 = false !== opts.perMessageDeflate ? (opts.perMessageDeflate || true) : 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 var wsModule; 106 switch (this.wsEngine) { 107 case 'uws': wsModule = require('uws'); break; 108 case 'ws': wsModule = require('ws'); break; 109 default: throw new Error('unknown wsEngine'); 110 } 111 this.ws = new wsModule.Server({ 112 noServer: true, 113 clientTracking: false, 114 perMessageDeflate: this.perMessageDeflate, 115 maxPayload: this.maxHttpBufferSize 116 }); 117 }; 118 119 /** 120 * Returns a list of available transports for upgrade given a certain transport. 121 * 122 * @return {Array} 123 * @api public 124 */ 125 126 Server.prototype.upgrades = function (transport) { 127 if (!this.allowUpgrades) return []; 128 return transports[transport].upgradesTo || []; 129 }; 130 131 /** 132 * Verifies a request. 133 * 134 * @param {http.IncomingMessage} 135 * @return {Boolean} whether the request is valid 136 * @api private 137 */ 138 139 Server.prototype.verify = function (req, upgrade, fn) { 140 // transport check 141 var transport = req._query.transport; 142 if (!~this.transports.indexOf(transport)) { 143 debug('unknown transport "%s"', transport); 144 return fn(Server.errors.UNKNOWN_TRANSPORT, false); 145 } 146 147 // 'Origin' header check 148 var isOriginInvalid = checkInvalidHeaderChar(req.headers.origin); 149 if (isOriginInvalid) { 150 req.headers.origin = null; 151 debug('origin header invalid'); 152 return fn(Server.errors.BAD_REQUEST, false); 153 } 154 155 // sid check 156 var sid = req._query.sid; 157 if (sid) { 158 if (!this.clients.hasOwnProperty(sid)) { 159 debug('unknown sid "%s"', sid); 160 return fn(Server.errors.UNKNOWN_SID, false); 161 } 162 if (!upgrade && this.clients[sid].transport.name !== transport) { 163 debug('bad request: unexpected transport without upgrade'); 164 return fn(Server.errors.BAD_REQUEST, false); 165 } 166 } else { 167 // handshake is GET only 168 if ('GET' !== req.method) return fn(Server.errors.BAD_HANDSHAKE_METHOD, false); 169 if (!this.allowRequest) return fn(null, true); 170 return this.allowRequest(req, fn); 171 } 172 173 fn(null, true); 174 }; 175 176 /** 177 * Prepares a request by processing the query string. 178 * 179 * @api private 180 */ 181 182 Server.prototype.prepare = function (req) { 183 // try to leverage pre-existing `req._query` (e.g: from connect) 184 if (!req._query) { 185 req._query = ~req.url.indexOf('?') ? qs.parse(parse(req.url).query) : {}; 186 } 187 }; 188 189 /** 190 * Closes all clients. 191 * 192 * @api public 193 */ 194 195 Server.prototype.close = function () { 196 debug('closing all open clients'); 197 for (var i in this.clients) { 198 if (this.clients.hasOwnProperty(i)) { 199 this.clients[i].close(true); 200 } 201 } 202 if (this.ws) { 203 debug('closing webSocketServer'); 204 this.ws.close(); 205 // don't delete this.ws because it can be used again if the http server starts listening again 206 } 207 return this; 208 }; 209 210 /** 211 * Handles an Engine.IO HTTP request. 212 * 213 * @param {http.IncomingMessage} request 214 * @param {http.ServerResponse|http.OutgoingMessage} response 215 * @api public 216 */ 217 218 Server.prototype.handleRequest = function (req, res) { 219 debug('handling "%s" http request "%s"', req.method, req.url); 220 this.prepare(req); 221 req.res = res; 222 223 var self = this; 224 this.verify(req, false, function (err, success) { 225 if (!success) { 226 sendErrorMessage(req, res, err); 227 return; 228 } 229 230 if (req._query.sid) { 231 debug('setting new request for existing client'); 232 self.clients[req._query.sid].transport.onRequest(req); 233 } else { 234 self.handshake(req._query.transport, req); 235 } 236 }); 237 }; 238 239 /** 240 * Sends an Engine.IO Error Message 241 * 242 * @param {http.ServerResponse} response 243 * @param {code} error code 244 * @api private 245 */ 246 247 function sendErrorMessage (req, res, code) { 248 var headers = { 'Content-Type': 'application/json' }; 249 250 var isForbidden = !Server.errorMessages.hasOwnProperty(code); 251 if (isForbidden) { 252 res.writeHead(403, headers); 253 res.end(JSON.stringify({ 254 code: Server.errors.FORBIDDEN, 255 message: code || Server.errorMessages[Server.errors.FORBIDDEN] 256 })); 257 return; 258 } 259 if (req.headers.origin) { 260 headers['Access-Control-Allow-Credentials'] = 'true'; 261 headers['Access-Control-Allow-Origin'] = req.headers.origin; 262 } else { 263 headers['Access-Control-Allow-Origin'] = '*'; 264 } 265 if (res !== undefined) { 266 res.writeHead(400, headers); 267 res.end(JSON.stringify({ 268 code: code, 269 message: Server.errorMessages[code] 270 })); 271 } 272 } 273 274 /** 275 * generate a socket id. 276 * Overwrite this method to generate your custom socket id 277 * 278 * @param {Object} request object 279 * @api public 280 */ 281 282 Server.prototype.generateId = function (req) { 283 return base64id.generateId(); 284 }; 285 286 /** 287 * Handshakes a new client. 288 * 289 * @param {String} transport name 290 * @param {Object} request object 291 * @api private 292 */ 293 294 Server.prototype.handshake = function (transportName, req) { 295 var id = this.generateId(req); 296 297 debug('handshaking client "%s"', id); 298 299 try { 300 var transport = new transports[transportName](req); 301 if ('polling' === transportName) { 302 transport.maxHttpBufferSize = this.maxHttpBufferSize; 303 transport.httpCompression = this.httpCompression; 304 } else if ('websocket' === transportName) { 305 transport.perMessageDeflate = this.perMessageDeflate; 306 } 307 308 if (req._query && req._query.b64) { 309 transport.supportsBinary = false; 310 } else { 311 transport.supportsBinary = true; 312 } 313 } catch (e) { 314 debug('error handshaking to transport "%s"', transportName); 315 sendErrorMessage(req, req.res, Server.errors.BAD_REQUEST); 316 return; 317 } 318 var socket = new Socket(id, this, transport, req); 319 var self = this; 320 321 if (false !== this.cookie) { 322 transport.on('headers', function (headers) { 323 headers['Set-Cookie'] = cookieMod.serialize(self.cookie, id, 324 { 325 path: self.cookiePath, 326 httpOnly: self.cookiePath ? self.cookieHttpOnly : false, 327 sameSite: true 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 }