server.js 21 KB

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