diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f0ef66e1..cc3f4ece 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,7 @@ jobs: matrix: node-version: - 18 + - 20 steps: - name: Checkout repository diff --git a/CHANGELOG.md b/CHANGELOG.md index 1df1a2ad..27869dea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,13 @@ # History +## 2024 + +- [6.5.5](#655-2024-06-18) (Jun 2024) (from the [6.5.x](https://github.com/socketio/engine.io-client/tree/6.5.x) branch) + ## 2023 +- [6.5.4](#654-2023-11-09) (Nov 2023) +- [6.5.3](#653-2023-10-06) (Oct 2023) - [6.5.2](#652-2023-08-01) (Aug 2023) - [6.5.1](#651-2023-06-27) (Jun 2023) - [6.5.0](#650-2023-06-16) (Jun 2023) @@ -50,6 +56,48 @@ # Release notes +## [6.5.5](https://github.com/socketio/engine.io/compare/6.5.4...6.5.5) (2024-06-18) + +This release contains a bump of the `ws` dependency, which includes an important [security fix](https://github.com/websockets/ws/commit/e55e5106f10fcbaac37cfa89759e4cc0d073a52c). + +Advisory: https://github.com/advisories/GHSA-3h5v-q93c-6h6q + +### Bug Fixes + +* **types:** make socket.request writable ([#697](https://github.com/socketio/engine.io/issues/697)) ([0efa04b](https://github.com/socketio/engine.io/commit/0efa04b5841816d18b0c6ebf7c5f592f8382978a)) + +### Dependencies + +- [`ws@~8.17.1`](https://github.com/websockets/ws/releases/tag/8.17.1) ([diff](https://github.com/websockets/ws/compare/8.11.0...8.17.1)) + + + +## [6.5.4](https://github.com/socketio/engine.io/compare/6.5.3...6.5.4) (2023-11-09) + +This release contains some minor changes which should improve the memory usage of the server, notably [this](https://github.com/socketio/engine.io/commit/f27a6c35017e4eb37546949f754e09933102837a). + + +### Dependencies + +- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change) + + + +## [6.5.3](https://github.com/socketio/engine.io/compare/6.5.2...6.5.3) (2023-10-06) + + +### Bug Fixes + +* improve compatibility with node16 module resolution ([#689](https://github.com/socketio/engine.io/issues/689)) ([c6bf8c0](https://github.com/socketio/engine.io/commit/c6bf8c0f571aad7a5917f43860c8c3d74a9b429b)) +* **webtransport:** properly handle abruptly closed connections ([ff1c861](https://github.com/socketio/engine.io/commit/ff1c8615483bab25acc9cf04fb40339b0bd78812)) + + +### Dependencies + +- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change) + + + ## [6.5.2](https://github.com/socketio/engine.io/compare/6.5.1...6.5.2) (2023-08-01) diff --git a/lib/server.ts b/lib/server.ts index 5cec3d61..99b3f471 100644 --- a/lib/server.ts +++ b/lib/server.ts @@ -464,12 +464,6 @@ export abstract class BaseServer extends EventEmitter { } else if ("websocket" === transportName) { transport.perMessageDeflate = this.opts.perMessageDeflate; } - - if (req._query && req._query.b64) { - transport.supportsBinary = false; - } else { - transport.supportsBinary = true; - } } catch (e) { debug('error handshaking to transport "%s"', transportName); this.emit("connection_error", { @@ -862,11 +856,6 @@ export class Server extends BaseServer { websocket.removeListener("error", onUpgradeError); const transport = this.createTransport(req._query.transport, req); - if (req._query && req._query.b64) { - transport.supportsBinary = false; - } else { - transport.supportsBinary = true; - } transport.perMessageDeflate = this.opts.perMessageDeflate; client.maybeUpgrade(transport); } diff --git a/lib/socket.ts b/lib/socket.ts index 7afb9622..59814dee 100644 --- a/lib/socket.ts +++ b/lib/socket.ts @@ -12,24 +12,24 @@ export interface SendOptions { compress?: boolean; } +type ReadyState = "opening" | "open" | "closing" | "closed"; + export class Socket extends EventEmitter { public readonly protocol: number; // TODO for the next major release: do not keep the reference to the first HTTP request, as it stays in memory - public readonly request: IncomingMessage; + public request: IncomingMessage; public readonly remoteAddress: string; - public _readyState: string; + public _readyState: ReadyState = "opening"; public transport: Transport; private server: Server; - private upgrading: boolean; - private upgraded: boolean; - private writeBuffer: Packet[]; - private packetsFn: Array<() => void>; - private sentCallbackFn: any[]; - private cleanupFn: any[]; - private checkIntervalTimer; - private upgradeTimeoutTimer; + private upgrading = false; + private upgraded = false; + private writeBuffer: Packet[] = []; + private packetsFn: Array<() => void> = []; + private sentCallbackFn: any[] = []; + private cleanupFn: any[] = []; private pingTimeoutTimer; private pingIntervalTimer; @@ -45,7 +45,7 @@ export class Socket extends EventEmitter { return this._readyState; } - set readyState(state) { + set readyState(state: ReadyState) { debug("readyState updated from %s to %s", this._readyState, state); this._readyState = state; } @@ -59,13 +59,6 @@ export class Socket extends EventEmitter { super(); this.id = id; this.server = server; - this.upgrading = false; - this.upgraded = false; - this.readyState = "opening"; - this.writeBuffer = []; - this.packetsFn = []; - this.sentCallbackFn = []; - this.cleanupFn = []; this.request = req; this.protocol = protocol; @@ -81,8 +74,6 @@ export class Socket extends EventEmitter { // see https://github.com/fails-components/webtransport/issues/114 } - this.checkIntervalTimer = null; - this.upgradeTimeoutTimer = null; this.pingTimeoutTimer = null; this.pingIntervalTimer = null; @@ -183,7 +174,7 @@ export class Socket extends EventEmitter { /** * Called upon transport error. * - * @param {Error} error object + * @param {Error} err - error object * @api private */ private onError(err) { @@ -265,7 +256,7 @@ export class Socket extends EventEmitter { this.upgrading = true; // set transport upgrade timer - this.upgradeTimeoutTimer = setTimeout(() => { + const upgradeTimeoutTimer = setTimeout(() => { debug("client did not complete upgrade - closing transport"); cleanup(); if ("open" === transport.readyState) { @@ -273,13 +264,15 @@ export class Socket extends EventEmitter { } }, this.server.opts.upgradeTimeout); + let checkIntervalTimer; + const onPacket = (packet) => { if ("ping" === packet.type && "probe" === packet.data) { debug("got probe ping packet, sending pong"); transport.send([{ type: "pong", data: "probe" }]); this.emit("upgrading", transport); - clearInterval(this.checkIntervalTimer); - this.checkIntervalTimer = setInterval(check, 100); + clearInterval(checkIntervalTimer); + checkIntervalTimer = setInterval(check, 100); } else if ("upgrade" === packet.type && this.readyState !== "closed") { debug("got upgrade packet - upgrading"); cleanup(); @@ -311,11 +304,8 @@ export class Socket extends EventEmitter { const cleanup = () => { this.upgrading = false; - clearInterval(this.checkIntervalTimer); - this.checkIntervalTimer = null; - - clearTimeout(this.upgradeTimeoutTimer); - this.upgradeTimeoutTimer = null; + clearInterval(checkIntervalTimer); + clearTimeout(upgradeTimeoutTimer); transport.removeListener("packet", onPacket); transport.removeListener("close", onTransportClose); @@ -384,9 +374,6 @@ export class Socket extends EventEmitter { clearTimeout(this.pingIntervalTimer); clearTimeout(this.pingTimeoutTimer); - clearInterval(this.checkIntervalTimer); - this.checkIntervalTimer = null; - clearTimeout(this.upgradeTimeoutTimer); // clean writeBuffer in next tick, so developers can still // grab the writeBuffer on 'close' event process.nextTick(() => { diff --git a/lib/transport.ts b/lib/transport.ts index 4068a31e..b0777bca 100644 --- a/lib/transport.ts +++ b/lib/transport.ts @@ -15,13 +15,15 @@ const debug = debugModule("engine:transport"); function noop() {} +type ReadyState = "open" | "closing" | "closed"; + export abstract class Transport extends EventEmitter { public sid: string; - public writable: boolean; + public writable = false; public protocol: number; - protected _readyState: string; - protected discarded: boolean; + protected _readyState: ReadyState = "open"; + protected discarded = false; protected parser: any; protected req: IncomingMessage & { cleanup: Function }; protected supportsBinary: boolean; @@ -30,7 +32,7 @@ export abstract class Transport extends EventEmitter { return this._readyState; } - set readyState(state) { + set readyState(state: ReadyState) { debug( "readyState updated from %s to %s (%s)", this._readyState, @@ -43,15 +45,14 @@ export abstract class Transport extends EventEmitter { /** * Transport constructor. * - * @param {http.IncomingMessage} request + * @param {http.IncomingMessage} req * @api public */ constructor(req) { super(); - this.readyState = "open"; - this.discarded = false; this.protocol = req._query.EIO === "4" ? 4 : 3; // 3rd revision by default this.parser = this.protocol === 4 ? parser_v4 : parser_v3; + this.supportsBinary = !(req._query && req._query.b64); } /** @@ -66,7 +67,7 @@ export abstract class Transport extends EventEmitter { /** * Called with an incoming HTTP request. * - * @param {http.IncomingMessage} request + * @param {http.IncomingMessage} req * @api protected */ protected onRequest(req) { @@ -89,8 +90,8 @@ export abstract class Transport extends EventEmitter { /** * Called with a transport error. * - * @param {String} message error - * @param {Object} error description + * @param {String} msg - message error + * @param {Object} desc - error description * @api protected */ protected onError(msg: string, desc?) { diff --git a/lib/transports-uws/polling.ts b/lib/transports-uws/polling.ts index ce503a8a..16e72c00 100644 --- a/lib/transports-uws/polling.ts +++ b/lib/transports-uws/polling.ts @@ -55,6 +55,8 @@ export class Polling extends Transport { */ onRequest(req) { const res = req.res; + // remove the reference to the ServerResponse object (as the first request of the session is kept in memory by default) + req.res = null; if (req.getMethod() === "get") { this.onPollRequest(req, res); @@ -423,6 +425,8 @@ export class Polling extends Transport { headers["X-XSS-Protection"] = "0"; } + headers["cache-control"] = "no-store"; + this.emit("headers", headers, req); return headers; } diff --git a/lib/transports/polling.ts b/lib/transports/polling.ts index 70be3411..e5ea24cf 100644 --- a/lib/transports/polling.ts +++ b/lib/transports/polling.ts @@ -54,6 +54,8 @@ export class Polling extends Transport { */ onRequest(req: IncomingMessage & { res: ServerResponse }) { const res = req.res; + // remove the reference to the ServerResponse object (as the first request of the session is kept in memory by default) + req.res = null; if ("GET" === req.method) { this.onPollRequest(req, res); @@ -392,6 +394,8 @@ export class Polling extends Transport { headers["X-XSS-Protection"] = "0"; } + headers["cache-control"] = "no-store"; + this.emit("headers", headers, req); return headers; } diff --git a/lib/transports/webtransport.ts b/lib/transports/webtransport.ts index 4f6f6877..5922fab0 100644 --- a/lib/transports/webtransport.ts +++ b/lib/transports/webtransport.ts @@ -14,7 +14,9 @@ export class WebTransport extends Transport { super({ _query: { EIO: "4" } }); const transformStream = createPacketEncoderStream(); - transformStream.readable.pipeTo(stream.writable); + transformStream.readable.pipeTo(stream.writable).catch(() => { + debug("the stream was closed"); + }); this.writer = transformStream.writable.getWriter(); (async () => { diff --git a/package-lock.json b/package-lock.json index 7508dec7..0b5d814a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "engine.io", - "version": "6.5.1", + "version": "6.5.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "engine.io", - "version": "6.5.1", + "version": "6.5.4", "license": "MIT", "dependencies": { "@types/cookie": "^0.4.1", @@ -18,7 +18,7 @@ "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", - "ws": "~8.11.0" + "ws": "~8.17.1" }, "devDependencies": { "@fails-components/webtransport": "^0.1.7", @@ -828,6 +828,27 @@ "node": ">=10.0.0" } }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/engine.io-parser": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", @@ -2461,15 +2482,15 @@ "dev": true }, "node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "engines": { "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { @@ -3131,6 +3152,13 @@ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.1.0.tgz", "integrity": "sha512-enySgNiK5tyZFynt3z7iqBR+Bto9EVVVvDFuTT0ioHCGbzirZVGDGiQjZzEp8hWl6hd5FSVytJGuScX1C1C35w==", "dev": true + }, + "ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "dev": true, + "requires": {} } } }, @@ -4385,9 +4413,9 @@ "dev": true }, "ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "requires": {} }, "xmlhttprequest-ssl": { diff --git a/package.json b/package.json index 5753cdc1..294090e1 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,12 @@ { "name": "engine.io", - "version": "6.5.2", + "version": "6.5.5", "description": "The realtime engine behind Socket.IO. Provides the foundation of a bidirectional connection between client and server", "type": "commonjs", "main": "./build/engine.io.js", "types": "./build/engine.io.d.ts", "exports": { + "types": "./build/engine.io.d.ts", "import": "./wrapper.mjs", "require": "./build/engine.io.js" }, @@ -40,7 +41,7 @@ "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", - "ws": "~8.11.0" + "ws": "~8.17.1" }, "devDependencies": { "@fails-components/webtransport": "^0.1.7", diff --git a/test/server.js b/test/server.js index fa0ab9c0..a373837e 100644 --- a/test/server.js +++ b/test/server.js @@ -3443,13 +3443,12 @@ describe("server", () => { }); describe("response headers", () => { - function testForHeaders(headers, done) { + function testForHeaders(headers, callback) { const engine = listen((port) => { engine.on("connection", (conn) => { conn.transport.once("headers", (headers) => { - expect(headers["X-XSS-Protection"]).to.be("0"); + callback(headers); conn.close(); - done(); }); conn.send("hi"); }); @@ -3465,7 +3464,10 @@ describe("server", () => { "user-agent": "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; Tablet PC 2.0)", }; - testForHeaders(headers, done); + testForHeaders(headers, (headers) => { + expect(headers["X-XSS-Protection"]).to.be("0"); + done(); + }); }); it("should contain X-XSS-Protection: 0 for IE11", (done) => { @@ -3473,7 +3475,17 @@ describe("server", () => { "user-agent": "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko", }; - testForHeaders(headers, done); + testForHeaders(headers, (headers) => { + expect(headers["X-XSS-Protection"]).to.be("0"); + done(); + }); + }); + + it("should include a 'cache-control' header", (done) => { + testForHeaders({}, (headers) => { + expect(headers["cache-control"]).to.be("no-store"); + done(); + }); }); it("should emit a 'initial_headers' event (polling)", (done) => {