source: trip-planner-front/node_modules/engine.io/lib/server.js@ 6a3a178

Last change on this file since 6a3a178 was 6a3a178, checked in by Ema <ema_spirova@…>, 3 years ago

initial commit

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