diff --git a/lib/server.ts b/lib/server.ts index 8a1b8aa3..ac493bbc 100644 --- a/lib/server.ts +++ b/lib/server.ts @@ -7,12 +7,19 @@ import { Socket } from "./socket"; import debugModule from "debug"; import { serialize } from "cookie"; import { Server as DEFAULT_WS_ENGINE } from "ws"; -import { IncomingMessage, Server as HttpServer } from "http"; -import { CookieSerializeOptions } from "cookie"; -import { CorsOptions, CorsOptionsDelegate } from "cors"; +import type { + IncomingMessage, + Server as HttpServer, + ServerResponse, +} from "http"; +import type { CookieSerializeOptions } from "cookie"; +import type { CorsOptions, CorsOptionsDelegate } from "cors"; +import type { Duplex } from "stream"; const debug = debugModule("engine"); +const kResponseHeaders = Symbol("responseHeaders"); + type Transport = "polling" | "websocket"; export interface AttachOptions { @@ -119,12 +126,26 @@ export interface ServerOptions { allowEIO3?: boolean; } +/** + * An Express-compatible middleware. + * + * Middleware functions are functions that have access to the request object (req), the response object (res), and the + * next middleware function in the application’s request-response cycle. + * + * @see https://expressjs.com/en/guide/using-middleware.html + */ +type Middleware = ( + req: IncomingMessage, + res: ServerResponse, + next: () => void +) => void; + export abstract class BaseServer extends EventEmitter { public opts: ServerOptions; protected clients: any; private clientsCount: number; - protected corsMiddleware: Function; + protected middlewares: Middleware[] = []; /** * Server constructor. @@ -170,7 +191,7 @@ export abstract class BaseServer extends EventEmitter { } if (this.opts.cors) { - this.corsMiddleware = require("cors")(this.opts.cors); + this.use(require("cors")(this.opts.cors)); } if (opts.perMessageDeflate) { @@ -289,6 +310,52 @@ export abstract class BaseServer extends EventEmitter { fn(); } + /** + * Adds a new middleware. + * + * @example + * import helmet from "helmet"; + * + * engine.use(helmet()); + * + * @param fn + */ + public use(fn: Middleware) { + this.middlewares.push(fn); + } + + /** + * Apply the middlewares to the request. + * + * @param req + * @param res + * @param callback + * @protected + */ + protected _applyMiddlewares( + req: IncomingMessage, + res: ServerResponse, + callback: () => void + ) { + if (this.middlewares.length === 0) { + debug("no middleware to apply, skipping"); + return callback(); + } + + const apply = (i) => { + debug("applying middleware n°%d", i + 1); + this.middlewares[i](req, res, () => { + if (i + 1 < this.middlewares.length) { + apply(i + 1); + } else { + callback(); + } + }); + }; + + apply(0); + } + /** * Closes all clients. * @@ -449,6 +516,40 @@ export abstract class BaseServer extends EventEmitter { }; } +/** + * Exposes a subset of the http.ServerResponse interface, in order to be able to apply the middlewares to an upgrade + * request. + * + * @see https://nodejs.org/api/http.html#class-httpserverresponse + */ +class WebSocketResponse { + constructor(readonly req, readonly socket: Duplex) { + // temporarily store the response headers on the req object (see the "headers" event) + req[kResponseHeaders] = {}; + } + + public setHeader(name: string, value: any) { + this.req[kResponseHeaders][name] = value; + } + + public getHeader(name: string) { + return this.req[kResponseHeaders][name]; + } + + public removeHeader(name: string) { + delete this.req[kResponseHeaders][name]; + } + + public write() {} + + public writeHead() {} + + public end() { + // we could return a proper error code, but the WebSocket client will emit an "error" event anyway. + this.socket.destroy(); + } +} + export class Server extends BaseServer { public httpServer?: HttpServer; private ws: any; @@ -474,7 +575,8 @@ export class Server extends BaseServer { this.ws.on("headers", (headersArray, req) => { // note: 'ws' uses an array of headers, while Engine.IO uses an object (response.writeHead() accepts both formats) // we could also try to parse the array and then sync the values, but that will be error-prone - const additionalHeaders = {}; + const additionalHeaders = req[kResponseHeaders] || {}; + delete req[kResponseHeaders]; const isInitialRequest = !req._query.sid; if (isInitialRequest) { @@ -483,6 +585,7 @@ export class Server extends BaseServer { this.emit("headers", additionalHeaders, req); + debug("writing headers: %j", additionalHeaders); Object.keys(additionalHeaders).forEach((key) => { headersArray.push(`${key}: ${additionalHeaders[key]}`); }); @@ -517,13 +620,14 @@ export class Server extends BaseServer { /** * Handles an Engine.IO HTTP request. * - * @param {http.IncomingMessage} request - * @param {http.ServerResponse|http.OutgoingMessage} response + * @param {IncomingMessage} req + * @param {ServerResponse} res * @api public */ - public handleRequest(req, res) { + public handleRequest(req: IncomingMessage, res: ServerResponse) { debug('handling "%s" http request "%s"', req.method, req.url); this.prepare(req); + // @ts-ignore req.res = res; const callback = (errorCode, errorContext) => { @@ -538,23 +642,22 @@ export class Server extends BaseServer { return; } + // @ts-ignore if (req._query.sid) { debug("setting new request for existing client"); + // @ts-ignore this.clients[req._query.sid].transport.onRequest(req); } else { const closeConnection = (errorCode, errorContext) => abortRequest(res, errorCode, errorContext); + // @ts-ignore this.handshake(req._query.transport, req, closeConnection); } }; - if (this.corsMiddleware) { - this.corsMiddleware.call(null, req, res, () => { - this.verify(req, false, callback); - }); - } else { + this._applyMiddlewares(req, res, () => { this.verify(req, false, callback); - } + }); } /** @@ -562,27 +665,39 @@ export class Server extends BaseServer { * * @api public */ - public handleUpgrade(req, socket, upgradeHead) { + public handleUpgrade( + req: IncomingMessage, + socket: Duplex, + upgradeHead: Buffer + ) { this.prepare(req); - this.verify(req, true, (errorCode, errorContext) => { - if (errorCode) { - this.emit("connection_error", { - req, - code: errorCode, - message: Server.errorMessages[errorCode], - context: errorContext, - }); - abortUpgrade(socket, errorCode, errorContext); - return; - } + const res = new WebSocketResponse(req, socket); - const head = Buffer.from(upgradeHead); - upgradeHead = null; + this._applyMiddlewares(req, res as unknown as ServerResponse, () => { + this.verify(req, true, (errorCode, errorContext) => { + if (errorCode) { + this.emit("connection_error", { + req, + code: errorCode, + message: Server.errorMessages[errorCode], + context: errorContext, + }); + abortUpgrade(socket, errorCode, errorContext); + return; + } - // delegate to ws - this.ws.handleUpgrade(req, socket, head, (websocket) => { - this.onWebSocket(req, socket, websocket); + const head = Buffer.from(upgradeHead); + upgradeHead = null; + + // some middlewares (like express-session) wait for the writeHead() call to flush their headers + // see https://github.com/expressjs/session/blob/1010fadc2f071ddf2add94235d72224cf65159c6/index.js#L220-L244 + res.writeHead(); + + // delegate to ws + this.ws.handleUpgrade(req, socket, head, (websocket) => { + this.onWebSocket(req, socket, websocket); + }); }); }); } diff --git a/lib/userver.ts b/lib/userver.ts index 5ad66ac2..29729bd1 100644 --- a/lib/userver.ts +++ b/lib/userver.ts @@ -34,6 +34,7 @@ export class uServer extends BaseServer { */ private prepare(req, res: HttpResponse) { req.method = req.getMethod().toUpperCase(); + req.url = req.getUrl(); const params = new URLSearchParams(req.getQuery()); req._query = Object.fromEntries(params.entries()); @@ -91,6 +92,23 @@ export class uServer extends BaseServer { }); } + override _applyMiddlewares(req: any, res: any, callback: () => void): void { + if (this.middlewares.length === 0) { + return callback(); + } + + // needed to buffer headers until the status is computed + req.res = new ResponseWrapper(res); + + super._applyMiddlewares(req, req.res, () => { + // some middlewares (like express-session) wait for the writeHead() call to flush their headers + // see https://github.com/expressjs/session/blob/1010fadc2f071ddf2add94235d72224cf65159c6/index.js#L220-L244 + req.res.writeHead(); + + callback(); + }); + } + private handleRequest( res: HttpResponse, req: HttpRequest & { res: any; _query: any } @@ -100,104 +118,99 @@ export class uServer extends BaseServer { req.res = res; - const callback = (errorCode, errorContext) => { - if (errorCode !== undefined) { - this.emit("connection_error", { - req, - code: errorCode, - message: Server.errorMessages[errorCode], - context: errorContext, - }); - this.abortRequest(req.res, errorCode, errorContext); - return; - } - - if (req._query.sid) { - debug("setting new request for existing client"); - this.clients[req._query.sid].transport.onRequest(req); - } else { - const closeConnection = (errorCode, errorContext) => - this.abortRequest(res, errorCode, errorContext); - this.handshake(req._query.transport, req, closeConnection); - } - }; - - if (this.corsMiddleware) { - // needed to buffer headers until the status is computed - req.res = new ResponseWrapper(res); + this._applyMiddlewares(req, res, () => { + this.verify(req, false, (errorCode, errorContext) => { + if (errorCode !== undefined) { + this.emit("connection_error", { + req, + code: errorCode, + message: Server.errorMessages[errorCode], + context: errorContext, + }); + this.abortRequest(req.res, errorCode, errorContext); + return; + } - this.corsMiddleware.call(null, req, req.res, () => { - this.verify(req, false, callback); + if (req._query.sid) { + debug("setting new request for existing client"); + this.clients[req._query.sid].transport.onRequest(req); + } else { + const closeConnection = (errorCode, errorContext) => + this.abortRequest(res, errorCode, errorContext); + this.handshake(req._query.transport, req, closeConnection); + } }); - } else { - this.verify(req, false, callback); - } + }); } private handleUpgrade( res: HttpResponse, - req: HttpRequest & { _query: any }, + req: HttpRequest & { res: any; _query: any }, context ) { debug("on upgrade"); this.prepare(req, res); - // @ts-ignore req.res = res; - this.verify(req, true, async (errorCode, errorContext) => { - if (errorCode) { - this.emit("connection_error", { - req, - code: errorCode, - message: Server.errorMessages[errorCode], - context: errorContext, - }); - this.abortRequest(res, errorCode, errorContext); - return; - } - - const id = req._query.sid; - let transport; - - if (id) { - const client = this.clients[id]; - if (!client) { - debug("upgrade attempt for closed client"); - res.close(); - } else if (client.upgrading) { - debug("transport has already been trying to upgrade"); - res.close(); - } else if (client.upgraded) { - debug("transport had already been upgraded"); - res.close(); - } else { - debug("upgrading existing transport"); - transport = this.createTransport(req._query.transport, req); - client.maybeUpgrade(transport); - } - } else { - transport = await this.handshake( - req._query.transport, - req, - (errorCode, errorContext) => - this.abortRequest(res, errorCode, errorContext) - ); - if (!transport) { + this._applyMiddlewares(req, res, () => { + this.verify(req, true, async (errorCode, errorContext) => { + if (errorCode) { + this.emit("connection_error", { + req, + code: errorCode, + message: Server.errorMessages[errorCode], + context: errorContext, + }); + this.abortRequest(res, errorCode, errorContext); return; } - } - res.upgrade( - { - transport, - }, - req.getHeader("sec-websocket-key"), - req.getHeader("sec-websocket-protocol"), - req.getHeader("sec-websocket-extensions"), - context - ); + const id = req._query.sid; + let transport; + + if (id) { + const client = this.clients[id]; + if (!client) { + debug("upgrade attempt for closed client"); + res.close(); + } else if (client.upgrading) { + debug("transport has already been trying to upgrade"); + res.close(); + } else if (client.upgraded) { + debug("transport had already been upgraded"); + res.close(); + } else { + debug("upgrading existing transport"); + transport = this.createTransport(req._query.transport, req); + client.maybeUpgrade(transport); + } + } else { + transport = await this.handshake( + req._query.transport, + req, + (errorCode, errorContext) => + this.abortRequest(res, errorCode, errorContext) + ); + if (!transport) { + return; + } + } + + // calling writeStatus() triggers the flushing of any header added in a middleware + req.res.writeStatus("101 Switching Protocols"); + + res.upgrade( + { + transport, + }, + req.getHeader("sec-websocket-key"), + req.getHeader("sec-websocket-protocol"), + req.getHeader("sec-websocket-extensions"), + context + ); + }); }); } @@ -233,11 +246,29 @@ class ResponseWrapper { constructor(readonly res: HttpResponse) {} public set statusCode(status: number) { + if (!status) { + return; + } + // FIXME: handle all status codes? this.writeStatus(status === 200 ? "200 OK" : "204 No Content"); } + public writeHead(status: number) { + this.statusCode = status; + } + public setHeader(key, value) { - this.writeHeader(key, value); + if (Array.isArray(value)) { + value.forEach((val) => { + this.writeHeader(key, val); + }); + } else { + this.writeHeader(key, value); + } + } + + public removeHeader() { + // FIXME: not implemented } // needed by vary: https://github.com/jshttp/vary/blob/5d725d059b3871025cf753e9dfa08924d0bcfa8f/index.js#L134 diff --git a/package-lock.json b/package-lock.json index c070b7c8..e71bbefd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "engine.io", - "version": "6.2.1", + "version": "6.3.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "engine.io", - "version": "6.2.1", + "version": "6.3.1", "license": "MIT", "dependencies": { "@types/cookie": "^0.4.1", @@ -26,6 +26,8 @@ "engine.io-client": "6.3.0", "engine.io-client-v3": "npm:engine.io-client@3.5.2", "expect.js": "^0.3.1", + "express-session": "^1.17.3", + "helmet": "^6.0.1", "mocha": "^9.1.3", "prettier": "^2.8.2", "rimraf": "^3.0.2", @@ -483,13 +485,19 @@ "dev": true }, "node_modules/cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", "engines": { "node": ">= 0.6" } }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true + }, "node_modules/cookiejar": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", @@ -551,6 +559,15 @@ "node": ">=0.4.0" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/diff": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", @@ -763,6 +780,60 @@ "integrity": "sha1-sKWaDS7/VDdUTr8M6qYBWEHQm1s=", "dev": true }, + "node_modules/express-session": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz", + "integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==", + "dev": true, + "dependencies": { + "cookie": "0.4.2", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express-session/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/express-session/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -992,6 +1063,15 @@ "he": "bin/he" } }, + "node_modules/helmet": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-6.0.1.tgz", + "integrity": "sha512-8wo+VdQhTMVBMCITYZaGTbE4lvlthelPYSvoyNvk4RECTmrVjMerp9RfUOQXZWLvCcAn1pKj7ZRxK4lI9Alrcw==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/indexof": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", @@ -1456,6 +1536,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1507,6 +1596,15 @@ "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==", "dev": true }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -1573,6 +1671,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -1805,6 +1912,18 @@ "node": ">=4.2.0" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dev": true, + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -2382,9 +2501,15 @@ "dev": true }, "cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true }, "cookiejar": { "version": "2.1.2", @@ -2427,6 +2552,12 @@ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", "dev": true }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true + }, "diff": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", @@ -2591,6 +2722,45 @@ "integrity": "sha1-sKWaDS7/VDdUTr8M6qYBWEHQm1s=", "dev": true }, + "express-session": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz", + "integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==", + "dev": true, + "requires": { + "cookie": "0.4.2", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -2760,6 +2930,12 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, + "helmet": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-6.0.1.tgz", + "integrity": "sha512-8wo+VdQhTMVBMCITYZaGTbE4lvlthelPYSvoyNvk4RECTmrVjMerp9RfUOQXZWLvCcAn1pKj7ZRxK4lI9Alrcw==", + "dev": true + }, "indexof": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", @@ -3094,6 +3270,12 @@ "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", "dev": true }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -3133,6 +3315,12 @@ "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==", "dev": true }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3172,6 +3360,12 @@ "side-channel": "^1.0.4" } }, + "random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "dev": true + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -3352,6 +3546,15 @@ "integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==", "dev": true }, + "uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dev": true, + "requires": { + "random-bytes": "~1.0.0" + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index d606462f..97d891bc 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,8 @@ "engine.io-client": "6.3.0", "engine.io-client-v3": "npm:engine.io-client@3.5.2", "expect.js": "^0.3.1", + "express-session": "^1.17.3", + "helmet": "^6.0.1", "mocha": "^9.1.3", "prettier": "^2.8.2", "rimraf": "^3.0.2", diff --git a/test/middlewares.js b/test/middlewares.js new file mode 100644 index 00000000..a81f1354 --- /dev/null +++ b/test/middlewares.js @@ -0,0 +1,250 @@ +const listen = require("./common").listen; +const expect = require("expect.js"); +const request = require("superagent"); +const { WebSocket } = require("ws"); +const helmet = require("helmet"); +const session = require("express-session"); + +describe("middlewares", () => { + it("should apply middleware (polling)", (done) => { + const engine = listen((port) => { + engine.use((req, res, next) => { + res.setHeader("foo", "bar"); + next(); + }); + + request + .get(`http://localhost:${port}/engine.io/`) + .query({ EIO: 4, transport: "polling" }) + .end((err, res) => { + expect(err).to.be(null); + expect(res.status).to.eql(200); + expect(res.headers["foo"]).to.eql("bar"); + + if (engine.httpServer) { + engine.httpServer.close(); + } + done(); + }); + }); + }); + + it("should apply middleware (websocket)", (done) => { + const engine = listen((port) => { + engine.use((req, res, next) => { + res.setHeader("foo", "bar"); + next(); + }); + + const socket = new WebSocket( + `ws://localhost:${port}/engine.io/?EIO=4&transport=websocket` + ); + + socket.on("upgrade", (res) => { + expect(res.headers["foo"]).to.eql("bar"); + + if (engine.httpServer) { + engine.httpServer.close(); + } + done(); + }); + + socket.on("open", () => { + socket.close(); + }); + }); + }); + + it("should apply all middlewares in order", (done) => { + const engine = listen((port) => { + let count = 0; + + engine.use((req, res, next) => { + expect(++count).to.eql(1); + next(); + }); + + engine.use((req, res, next) => { + expect(++count).to.eql(2); + next(); + }); + + engine.use((req, res, next) => { + expect(++count).to.eql(3); + next(); + }); + + request + .get(`http://localhost:${port}/engine.io/`) + .query({ EIO: 4, transport: "polling" }) + .end((err, res) => { + expect(err).to.be(null); + expect(res.status).to.eql(200); + + if (engine.httpServer) { + engine.httpServer.close(); + } + done(); + }); + }); + }); + + it("should end the request (polling)", function (done) { + if (process.env.EIO_WS_ENGINE === "uws") { + return this.skip(); + } + const engine = listen((port) => { + engine.use((req, res, _next) => { + res.writeHead(503); + res.end(); + }); + + engine.on("connection", () => { + done(new Error("should not happen")); + }); + + request + .get(`http://localhost:${port}/engine.io/`) + .query({ EIO: 4, transport: "polling" }) + .end((err, res) => { + expect(err).to.be.an(Error); + expect(res.status).to.eql(503); + + if (engine.httpServer) { + engine.httpServer.close(); + } + done(); + }); + }); + }); + + it("should end the request (websocket)", (done) => { + const engine = listen((port) => { + engine.use((req, res, _next) => { + res.writeHead(503); + res.end(); + }); + + engine.on("connection", () => { + done(new Error("should not happen")); + }); + + const socket = new WebSocket( + `ws://localhost:${port}/engine.io/?EIO=4&transport=websocket` + ); + + socket.addEventListener("error", () => { + if (engine.httpServer) { + engine.httpServer.close(); + } + done(); + }); + }); + }); + + it("should work with helmet (polling)", (done) => { + const engine = listen((port) => { + engine.use(helmet()); + + request + .get(`http://localhost:${port}/engine.io/`) + .query({ EIO: 4, transport: "polling" }) + .end((err, res) => { + expect(err).to.be(null); + expect(res.status).to.eql(200); + expect(res.headers["x-download-options"]).to.eql("noopen"); + expect(res.headers["x-content-type-options"]).to.eql("nosniff"); + + if (engine.httpServer) { + engine.httpServer.close(); + } + done(); + }); + }); + }); + + it("should work with helmet (websocket)", (done) => { + const engine = listen((port) => { + engine.use(helmet()); + + const socket = new WebSocket( + `ws://localhost:${port}/engine.io/?EIO=4&transport=websocket` + ); + + socket.on("upgrade", (res) => { + expect(res.headers["x-download-options"]).to.eql("noopen"); + expect(res.headers["x-content-type-options"]).to.eql("nosniff"); + + if (engine.httpServer) { + engine.httpServer.close(); + } + done(); + }); + + socket.on("open", () => { + socket.close(); + }); + }); + }); + + it("should work with express-session (polling)", (done) => { + const engine = listen((port) => { + engine.use( + session({ + secret: "keyboard cat", + resave: false, + saveUninitialized: true, + cookie: {}, + }) + ); + + request + .get(`http://localhost:${port}/engine.io/`) + .query({ EIO: 4, transport: "polling" }) + .end((err, res) => { + expect(err).to.be(null); + // expect(res.status).to.eql(200); + expect(res.headers["set-cookie"][0].startsWith("connect.sid=")).to.be( + true + ); + + if (engine.httpServer) { + engine.httpServer.close(); + } + done(); + }); + }); + }); + + it("should work with express-session (websocket)", (done) => { + const engine = listen((port) => { + engine.use( + session({ + secret: "keyboard cat", + resave: false, + saveUninitialized: true, + cookie: {}, + }) + ); + + const socket = new WebSocket( + `ws://localhost:${port}/engine.io/?EIO=4&transport=websocket` + ); + + socket.on("upgrade", (res) => { + expect(res.headers["set-cookie"][0].startsWith("connect.sid=")).to.be( + true + ); + + if (engine.httpServer) { + engine.httpServer.close(); + } + done(); + }); + + socket.on("open", () => { + socket.close(); + }); + }); + }); +});