@@ -7,12 +7,19 @@ import { Socket } from "./socket";
|
7 | 7 | import debugModule from "debug";
|
8 | 8 | import { serialize } from "cookie";
|
9 | 9 | import { Server as DEFAULT_WS_ENGINE } from "ws";
|
10 |
| -import { IncomingMessage, Server as HttpServer } from "http"; |
11 |
| -import { CookieSerializeOptions } from "cookie"; |
12 |
| -import { CorsOptions, CorsOptionsDelegate } from "cors"; |
| 10 | +import type { |
| 11 | +IncomingMessage, |
| 12 | +Server as HttpServer, |
| 13 | +ServerResponse, |
| 14 | +} from "http"; |
| 15 | +import type { CookieSerializeOptions } from "cookie"; |
| 16 | +import type { CorsOptions, CorsOptionsDelegate } from "cors"; |
| 17 | +import type { Duplex } from "stream"; |
13 | 18 |
|
14 | 19 | const debug = debugModule("engine");
|
15 | 20 |
|
| 21 | +const kResponseHeaders = Symbol("responseHeaders"); |
| 22 | + |
16 | 23 | type Transport = "polling" | "websocket";
|
17 | 24 |
|
18 | 25 | export interface AttachOptions {
|
@@ -119,12 +126,26 @@ export interface ServerOptions {
|
119 | 126 | allowEIO3?: boolean;
|
120 | 127 | }
|
121 | 128 |
|
| 129 | +/** |
| 130 | +* An Express-compatible middleware. |
| 131 | +* |
| 132 | +* Middleware functions are functions that have access to the request object (req), the response object (res), and the |
| 133 | +* next middleware function in the application’s request-response cycle. |
| 134 | +* |
| 135 | +* @see https://expressjs.com/en/guide/using-middleware.html |
| 136 | +*/ |
| 137 | +type Middleware = ( |
| 138 | +req: IncomingMessage, |
| 139 | +res: ServerResponse, |
| 140 | +next: () => void |
| 141 | +) => void; |
| 142 | + |
122 | 143 | export abstract class BaseServer extends EventEmitter {
|
123 | 144 | public opts: ServerOptions;
|
124 | 145 |
|
125 | 146 | protected clients: any;
|
126 | 147 | private clientsCount: number;
|
127 |
| -protected corsMiddleware: Function; |
| 148 | +protected middlewares: Middleware[] = []; |
128 | 149 |
|
129 | 150 | /**
|
130 | 151 | * Server constructor.
|
@@ -170,7 +191,7 @@ export abstract class BaseServer extends EventEmitter {
|
170 | 191 | }
|
171 | 192 |
|
172 | 193 | if (this.opts.cors) {
|
173 |
| -this.corsMiddleware = require("cors")(this.opts.cors); |
| 194 | +this.use(require("cors")(this.opts.cors)); |
174 | 195 | }
|
175 | 196 |
|
176 | 197 | if (opts.perMessageDeflate) {
|
@@ -289,6 +310,52 @@ export abstract class BaseServer extends EventEmitter {
|
289 | 310 | fn();
|
290 | 311 | }
|
291 | 312 |
|
| 313 | +/** |
| 314 | +* Adds a new middleware. |
| 315 | +* |
| 316 | +* @example |
| 317 | +* import helmet from "helmet"; |
| 318 | +* |
| 319 | +* engine.use(helmet()); |
| 320 | +* |
| 321 | +* @param fn |
| 322 | +*/ |
| 323 | +public use(fn: Middleware) { |
| 324 | +this.middlewares.push(fn); |
| 325 | +} |
| 326 | + |
| 327 | +/** |
| 328 | +* Apply the middlewares to the request. |
| 329 | +* |
| 330 | +* @param req |
| 331 | +* @param res |
| 332 | +* @param callback |
| 333 | +* @protected |
| 334 | +*/ |
| 335 | +protected _applyMiddlewares( |
| 336 | +req: IncomingMessage, |
| 337 | +res: ServerResponse, |
| 338 | +callback: () => void |
| 339 | +) { |
| 340 | +if (this.middlewares.length === 0) { |
| 341 | +debug("no middleware to apply, skipping"); |
| 342 | +return callback(); |
| 343 | +} |
| 344 | + |
| 345 | +const apply = (i) => { |
| 346 | +debug("applying middleware n°%d", i + 1); |
| 347 | +this.middlewares[i](req, res, () => { |
| 348 | +if (i + 1 < this.middlewares.length) { |
| 349 | +apply(i + 1); |
| 350 | +} else { |
| 351 | +callback(); |
| 352 | +} |
| 353 | +}); |
| 354 | +}; |
| 355 | + |
| 356 | +apply(0); |
| 357 | +} |
| 358 | + |
292 | 359 | /**
|
293 | 360 | * Closes all clients.
|
294 | 361 | *
|
@@ -449,6 +516,40 @@ export abstract class BaseServer extends EventEmitter {
|
449 | 516 | };
|
450 | 517 | }
|
451 | 518 |
|
| 519 | +/** |
| 520 | +* Exposes a subset of the http.ServerResponse interface, in order to be able to apply the middlewares to an upgrade |
| 521 | +* request. |
| 522 | +* |
| 523 | +* @see https://nodejs.org/api/http.html#class-httpserverresponse |
| 524 | +*/ |
| 525 | +class WebSocketResponse { |
| 526 | +constructor(readonly req, readonly socket: Duplex) { |
| 527 | +// temporarily store the response headers on the req object (see the "headers" event) |
| 528 | +req[kResponseHeaders] = {}; |
| 529 | +} |
| 530 | + |
| 531 | +public setHeader(name: string, value: any) { |
| 532 | +this.req[kResponseHeaders][name] = value; |
| 533 | +} |
| 534 | + |
| 535 | +public getHeader(name: string) { |
| 536 | +return this.req[kResponseHeaders][name]; |
| 537 | +} |
| 538 | + |
| 539 | +public removeHeader(name: string) { |
| 540 | +delete this.req[kResponseHeaders][name]; |
| 541 | +} |
| 542 | + |
| 543 | +public write() {} |
| 544 | + |
| 545 | +public writeHead() {} |
| 546 | + |
| 547 | +public end() { |
| 548 | +// we could return a proper error code, but the WebSocket client will emit an "error" event anyway. |
| 549 | +this.socket.destroy(); |
| 550 | +} |
| 551 | +} |
| 552 | + |
452 | 553 | export class Server extends BaseServer {
|
453 | 554 | public httpServer?: HttpServer;
|
454 | 555 | private ws: any;
|
@@ -474,7 +575,8 @@ export class Server extends BaseServer {
|
474 | 575 | this.ws.on("headers", (headersArray, req) => {
|
475 | 576 | // note: 'ws' uses an array of headers, while Engine.IO uses an object (response.writeHead() accepts both formats)
|
476 | 577 | // we could also try to parse the array and then sync the values, but that will be error-prone
|
477 |
| -const additionalHeaders = {}; |
| 578 | +const additionalHeaders = req[kResponseHeaders] || {}; |
| 579 | +delete req[kResponseHeaders]; |
478 | 580 |
|
479 | 581 | const isInitialRequest = !req._query.sid;
|
480 | 582 | if (isInitialRequest) {
|
@@ -483,6 +585,7 @@ export class Server extends BaseServer {
|
483 | 585 |
|
484 | 586 | this.emit("headers", additionalHeaders, req);
|
485 | 587 |
|
| 588 | +debug("writing headers: %j", additionalHeaders); |
486 | 589 | Object.keys(additionalHeaders).forEach((key) => {
|
487 | 590 | headersArray.push(`${key}: ${additionalHeaders[key]}`);
|
488 | 591 | });
|
@@ -517,13 +620,14 @@ export class Server extends BaseServer {
|
517 | 620 | /**
|
518 | 621 | * Handles an Engine.IO HTTP request.
|
519 | 622 | *
|
520 |
| -* @param {http.IncomingMessage} request |
521 |
| -* @param {http.ServerResponse|http.OutgoingMessage} response |
| 623 | +* @param {IncomingMessage} req |
| 624 | +* @param {ServerResponse} res |
522 | 625 | * @api public
|
523 | 626 | */
|
524 |
| -public handleRequest(req, res) { |
| 627 | +public handleRequest(req: IncomingMessage, res: ServerResponse) { |
525 | 628 | debug('handling "%s" http request "%s"', req.method, req.url);
|
526 | 629 | this.prepare(req);
|
| 630 | +// @ts-ignore |
527 | 631 | req.res = res;
|
528 | 632 |
|
529 | 633 | const callback = (errorCode, errorContext) => {
|
@@ -538,51 +642,62 @@ export class Server extends BaseServer {
|
538 | 642 | return;
|
539 | 643 | }
|
540 | 644 |
|
| 645 | +// @ts-ignore |
541 | 646 | if (req._query.sid) {
|
542 | 647 | debug("setting new request for existing client");
|
| 648 | +// @ts-ignore |
543 | 649 | this.clients[req._query.sid].transport.onRequest(req);
|
544 | 650 | } else {
|
545 | 651 | const closeConnection = (errorCode, errorContext) =>
|
546 | 652 | abortRequest(res, errorCode, errorContext);
|
| 653 | +// @ts-ignore |
547 | 654 | this.handshake(req._query.transport, req, closeConnection);
|
548 | 655 | }
|
549 | 656 | };
|
550 | 657 |
|
551 |
| -if (this.corsMiddleware) { |
552 |
| -this.corsMiddleware.call(null, req, res, () => { |
553 |
| -this.verify(req, false, callback); |
554 |
| -}); |
555 |
| -} else { |
| 658 | +this._applyMiddlewares(req, res, () => { |
556 | 659 | this.verify(req, false, callback);
|
557 |
| -} |
| 660 | +}); |
558 | 661 | }
|
559 | 662 |
|
560 | 663 | /**
|
561 | 664 | * Handles an Engine.IO HTTP Upgrade.
|
562 | 665 | *
|
563 | 666 | * @api public
|
564 | 667 | */
|
565 |
| -public handleUpgrade(req, socket, upgradeHead) { |
| 668 | +public handleUpgrade( |
| 669 | +req: IncomingMessage, |
| 670 | +socket: Duplex, |
| 671 | +upgradeHead: Buffer |
| 672 | +) { |
566 | 673 | this.prepare(req);
|
567 | 674 |
|
568 |
| -this.verify(req, true, (errorCode, errorContext) => { |
569 |
| -if (errorCode) { |
570 |
| -this.emit("connection_error", { |
571 |
| -req, |
572 |
| -code: errorCode, |
573 |
| -message: Server.errorMessages[errorCode], |
574 |
| -context: errorContext, |
575 |
| -}); |
576 |
| -abortUpgrade(socket, errorCode, errorContext); |
577 |
| -return; |
578 |
| -} |
| 675 | +const res = new WebSocketResponse(req, socket); |
579 | 676 |
|
580 |
| -const head = Buffer.from(upgradeHead); |
581 |
| -upgradeHead = null; |
| 677 | +this._applyMiddlewares(req, res as unknown as ServerResponse, () => { |
| 678 | +this.verify(req, true, (errorCode, errorContext) => { |
| 679 | +if (errorCode) { |
| 680 | +this.emit("connection_error", { |
| 681 | +req, |
| 682 | +code: errorCode, |
| 683 | +message: Server.errorMessages[errorCode], |
| 684 | +context: errorContext, |
| 685 | +}); |
| 686 | +abortUpgrade(socket, errorCode, errorContext); |
| 687 | +return; |
| 688 | +} |
582 | 689 |
|
583 |
| -// delegate to ws |
584 |
| -this.ws.handleUpgrade(req, socket, head, (websocket) => { |
585 |
| -this.onWebSocket(req, socket, websocket); |
| 690 | +const head = Buffer.from(upgradeHead); |
| 691 | +upgradeHead = null; |
| 692 | + |
| 693 | +// some middlewares (like express-session) wait for the writeHead() call to flush their headers |
| 694 | +// see https://.com/expressjs/session/blob/1010fadc2f071ddf2add94235d72224cf65159c6/index.js#L220-L244 |
| 695 | +res.writeHead(); |
| 696 | + |
| 697 | +// delegate to ws |
| 698 | +this.ws.handleUpgrade(req, socket, head, (websocket) => { |
| 699 | +this.onWebSocket(req, socket, websocket); |
| 700 | +}); |
586 | 701 | });
|
587 | 702 | });
|
588 | 703 | }
|
|
0 commit comments