feat: add support for Express middlewares

This commit implements middlewares at the Engine.IO level, because
Socket.IO middlewares are meant for namespace authorization and are not
executed during a classic HTTP request/response cycle.

A workaround was possible by using the allowRequest option and the
"headers" event, but this feels way cleaner and works with upgrade
requests too.

Syntax:

```js
engine.use((req, res, next) => {
  // do something

  next();
});

// with express-session
import session from "express-session";

engine.use(session({
  secret: "keyboard cat",
  resave: false,
  saveUninitialized: true,
  cookie: { secure: true }
});

// with helmet
import helmet from "helmet";

engine.use(helmet());
```

Related:

- https://github.com/socketio/engine.io/issues/668
- https://github.com/socketio/engine.io/issues/651
- https://github.com/socketio/socket.io/issues/4609
- https://github.com/socketio/socket.io/issues/3933
- a lot of other issues asking for compatibility with express-session
This commit is contained in:
Damien Arrachequesne
2023-02-06 16:42:15 +01:00
parent 4d6f4541c3
commit 24786e77c5
5 changed files with 724 additions and 123 deletions

View File

@@ -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 applications 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,
const res = new WebSocketResponse(req, socket);
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;
}
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);
});
abortUpgrade(socket, errorCode, errorContext);
return;
}
const head = Buffer.from(upgradeHead);
upgradeHead = null;
// delegate to ws
this.ws.handleUpgrade(req, socket, head, (websocket) => {
this.onWebSocket(req, socket, websocket);
});
});
}

View File

@@ -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;
}
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;
}
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.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

219
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

250
test/middlewares.js Normal file
View File

@@ -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();
});
});
});
});