diff --git a/CHANGELOG.md b/CHANGELOG.md index 527cd080..1df1a2ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## 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) - [6.4.2](#642-2023-05-02) (May 2023) - [6.4.1](#641-2023-02-20) (Feb 2023) @@ -48,6 +50,39 @@ # Release notes +## [6.5.2](https://github.com/socketio/engine.io/compare/6.5.1...6.5.2) (2023-08-01) + + +### Bug Fixes + +* **webtransport:** add proper framing ([a306db0](https://github.com/socketio/engine.io/commit/a306db09e8ddb367c7d62f45fec920f979580b7c)) + + +### Dependencies + +- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change) + + + +## [6.5.1](https://github.com/socketio/engine.io/compare/6.5.0...6.5.1) (2023-06-27) + + +### Bug Fixes + +* prevent crash when accessing TextDecoder ([#684](https://github.com/socketio/engine.io/issues/684)) ([6dd2bc4](https://github.com/socketio/engine.io/commit/6dd2bc4f68edd7575c3844ae8ceadde649be95b2)) + + +### Credits + +Huge thanks to [@iowaguy](https://github.com/iowaguy) for helping! + + +### Dependencies + +- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change) + + + ## [6.5.0](https://github.com/socketio/engine.io/compare/6.4.2...6.5.0) (2023-06-16) diff --git a/examples/latency/package-lock.json b/examples/latency/package-lock.json index 6445ee2f..73658c3e 100644 --- a/examples/latency/package-lock.json +++ b/examples/latency/package-lock.json @@ -18,9 +18,9 @@ } }, "@types/node": { - "version": "18.11.15", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.15.tgz", - "integrity": "sha512-VkhBbVo2+2oozlkdHXLrb3zjsRkpdnaU2bXmX8Wgle3PUi569eLRaHGlgETQHR7lLL1w7GiG3h9SnePhxNDecw==" + "version": "18.16.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.3.tgz", + "integrity": "sha512-OPs5WnnT1xkCBiuQrZA4+YAV4HEJejmHneyraIaxsbev5yCEr6KMwINNFP9wQeFIw8FWcoTqF3vQsa5CDaI+8Q==" }, "Base64": { "version": "0.2.1", @@ -37,12 +37,27 @@ } }, "accepts": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", - "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "requires": { - "mime-types": "~2.1.24", - "negotiator": "0.6.2" + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "dependencies": { + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + } } }, "acorn": { @@ -512,9 +527,9 @@ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" }, "engine.io": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.1.tgz", - "integrity": "sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.2.tgz", + "integrity": "sha512-FKn/3oMiJjrOEOeUub2WCox6JhxBXq/Zn3fZOMCBxKnNYtsdKjxhl7yR3fZhM9PV+rdE75SU5SYMc+2PGzo+Tg==", "requires": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", @@ -525,7 +540,7 @@ "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.0.3", - "ws": "~8.2.3" + "ws": "~8.11.0" }, "dependencies": { "cookie": { @@ -542,9 +557,9 @@ } }, "engine.io-parser": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz", - "integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==" + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz", + "integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==" }, "ms": { "version": "2.1.2", @@ -552,9 +567,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "ws": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", - "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==" + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==" } } }, @@ -1056,9 +1071,9 @@ "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=" }, "negotiator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" }, "object-assign": { "version": "4.1.1", diff --git a/examples/latency/package.json b/examples/latency/package.json index c97a6b9e..d90c57c1 100644 --- a/examples/latency/package.json +++ b/examples/latency/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "dependencies": { "enchilada": "0.13.0", - "engine.io": "^6.2.1", + "engine.io": "^6.4.2", "engine.io-client": "^4.1.4", "express": "^4.18.2", "smoothie": "1.19.0" diff --git a/lib/server.ts b/lib/server.ts index 2dba26db..5cec3d61 100644 --- a/lib/server.ts +++ b/lib/server.ts @@ -16,11 +16,11 @@ import type { CookieSerializeOptions } from "cookie"; import type { CorsOptions, CorsOptionsDelegate } from "cors"; import type { Duplex } from "stream"; import { WebTransport } from "./transports/webtransport"; +import { createPacketDecoderStream } from "engine.io-parser"; const debug = debugModule("engine"); const kResponseHeaders = Symbol("responseHeaders"); -const TEXT_DECODER = new TextDecoder(); type Transport = "polling" | "websocket"; @@ -148,15 +148,13 @@ type Middleware = ( next: (err?: any) => void ) => void; -function parseSessionId(handshake: string) { - if (handshake.startsWith("0{")) { - try { - const parsed = JSON.parse(handshake.substring(1)); - if (typeof parsed.sid === "string") { - return parsed.sid; - } - } catch (e) {} - } +function parseSessionId(data: string) { + try { + const parsed = JSON.parse(data); + if (typeof parsed.sid === "string") { + return parsed.sid; + } + } catch (e) {} } export abstract class BaseServer extends EventEmitter { @@ -535,7 +533,11 @@ export abstract class BaseServer extends EventEmitter { } const stream = result.value; - const reader = stream.readable.getReader(); + const transformStream = createPacketDecoderStream( + this.opts.maxHttpBufferSize, + "nodebuffer" + ); + const reader = stream.readable.pipeThrough(transformStream).getReader(); // reading the first packet of the stream const { value, done } = await reader.read(); @@ -545,12 +547,13 @@ export abstract class BaseServer extends EventEmitter { } clearTimeout(timeout); - const handshake = TEXT_DECODER.decode(value); - // handshake is either - // "0" => new session - // '0{"sid":"xxxx"}' => upgrade - if (handshake === "0") { + if (value.type !== "open") { + debug("invalid WebTransport handshake"); + return session.close(); + } + + if (value.data === undefined) { const transport = new WebTransport(session, stream, reader); // note: we cannot use "this.generateId()", because there is no "req" argument @@ -571,7 +574,7 @@ export abstract class BaseServer extends EventEmitter { return; } - const sid = parseSessionId(handshake); + const sid = parseSessionId(value.data); if (!sid) { debug("invalid WebTransport handshake"); diff --git a/lib/transports/webtransport.ts b/lib/transports/webtransport.ts index b79b8164..4f6f6877 100644 --- a/lib/transports/webtransport.ts +++ b/lib/transports/webtransport.ts @@ -1,21 +1,9 @@ import { Transport } from "../transport"; import debugModule from "debug"; +import { createPacketEncoderStream } from "engine.io-parser"; const debug = debugModule("engine:webtransport"); -const BINARY_HEADER = Buffer.of(54); - -function shouldIncludeBinaryHeader(packet, encoded) { - // 48 === "0".charCodeAt(0) (OPEN packet type) - // 54 === "6".charCodeAt(0) (NOOP packet type) - return ( - packet.type === "message" && - typeof packet.data !== "string" && - encoded[0] >= 48 && - encoded[0] <= 54 - ); -} - /** * Reference: https://developer.mozilla.org/en-US/docs/Web/API/WebTransport_API */ @@ -24,24 +12,24 @@ export class WebTransport extends Transport { constructor(private readonly session, stream, reader) { super({ _query: { EIO: "4" } }); - this.writer = stream.writable.getWriter(); + + const transformStream = createPacketEncoderStream(); + transformStream.readable.pipeTo(stream.writable); + this.writer = transformStream.writable.getWriter(); + (async () => { - let binaryFlag = false; - while (true) { - const { value, done } = await reader.read(); - if (done) { - debug("session is closed"); - break; - } - debug("received chunk: %o", value); - if (!binaryFlag && value.byteLength === 1 && value[0] === 54) { - binaryFlag = true; - continue; + try { + while (true) { + const { value, done } = await reader.read(); + if (done) { + debug("session is closed"); + break; + } + debug("received chunk: %o", value); + this.onPacket(value); } - this.onPacket( - this.parser.decodePacketFromBinary(value, binaryFlag, "nodebuffer") - ); - binaryFlag = false; + } catch (e) { + debug("error while reading: %s", e.message); } })(); @@ -58,26 +46,20 @@ export class WebTransport extends Transport { return true; } - send(packets) { + async send(packets) { this.writable = false; - for (let i = 0; i < packets.length; i++) { - const packet = packets[i]; - const isLast = i + 1 === packets.length; - - this.parser.encodePacketToBinary(packet, (data) => { - if (shouldIncludeBinaryHeader(packet, data)) { - debug("writing binary header"); - this.writer.write(BINARY_HEADER); - } - debug("writing chunk: %o", data); - this.writer.write(data); - if (isLast) { - this.writable = true; - this.emit("drain"); - } - }); + try { + for (let i = 0; i < packets.length; i++) { + const packet = packets[i]; + await this.writer.write(packet); + } + } catch (e) { + debug("error while writing: %s", e.message); } + + this.writable = true; + this.emit("drain"); } doClose(fn) { diff --git a/package-lock.json b/package-lock.json index 95421dce..7508dec7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "engine.io", - "version": "6.4.2", + "version": "6.5.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "engine.io", - "version": "6.4.2", + "version": "6.5.1", "license": "MIT", "dependencies": { "@types/cookie": "^0.4.1", @@ -17,7 +17,7 @@ "cookie": "~0.4.1", "cors": "~2.8.5", "debug": "~4.3.1", - "engine.io-parser": "~5.1.0", + "engine.io-parser": "~5.2.1", "ws": "~8.11.0" }, "devDependencies": { @@ -38,7 +38,7 @@ "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.30.0" }, "engines": { - "node": ">=10.0.0" + "node": ">=10.2.0" } }, "node_modules/@babel/code-frame": { @@ -819,10 +819,19 @@ "node": ">=0.4.0" } }, - "node_modules/engine.io-parser": { + "node_modules/engine.io-client/node_modules/engine.io-parser": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.1.0.tgz", "integrity": "sha512-enySgNiK5tyZFynt3z7iqBR+Bto9EVVVvDFuTT0ioHCGbzirZVGDGiQjZzEp8hWl6hd5FSVytJGuScX1C1C35w==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", + "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==", "engines": { "node": ">=10.0.0" } @@ -3115,6 +3124,14 @@ "engine.io-parser": "~5.1.0", "ws": "~8.11.0", "xmlhttprequest-ssl": "~2.0.0" + }, + "dependencies": { + "engine.io-parser": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.1.0.tgz", + "integrity": "sha512-enySgNiK5tyZFynt3z7iqBR+Bto9EVVVvDFuTT0ioHCGbzirZVGDGiQjZzEp8hWl6hd5FSVytJGuScX1C1C35w==", + "dev": true + } } }, "engine.io-client-v3": { @@ -3180,9 +3197,9 @@ } }, "engine.io-parser": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.1.0.tgz", - "integrity": "sha512-enySgNiK5tyZFynt3z7iqBR+Bto9EVVVvDFuTT0ioHCGbzirZVGDGiQjZzEp8hWl6hd5FSVytJGuScX1C1C35w==" + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", + "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==" }, "escalade": { "version": "3.1.1", diff --git a/package.json b/package.json index 5eae7112..5753cdc1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "engine.io", - "version": "6.5.0", + "version": "6.5.2", "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", @@ -39,7 +39,7 @@ "cookie": "~0.4.1", "cors": "~2.8.5", "debug": "~4.3.1", - "engine.io-parser": "~5.1.0", + "engine.io-parser": "~5.2.1", "ws": "~8.11.0" }, "devDependencies": { @@ -79,6 +79,6 @@ "wrapper.mjs" ], "engines": { - "node": ">=10.0.0" + "node": ">=10.2.0" } } diff --git a/test/webtransport.mjs b/test/webtransport.mjs index d26f9dfd..3d54e554 100644 --- a/test/webtransport.mjs +++ b/test/webtransport.mjs @@ -85,12 +85,15 @@ function setup(opts, cb) { const reader = stream.readable.getReader(); const writer = stream.writable.getWriter(); - engine.on("connection", (socket) => { + engine.on("connection", async (socket) => { + await reader.read(); // header + await reader.read(); // payload (handshake) + cb({ engine, h3Server, socket, client, stream, reader, writer }); }); + await writer.write(Uint8Array.of(1)); await writer.write(TEXT_ENCODER.encode("0")); - await reader.read(); // handshake }); } @@ -130,11 +133,11 @@ describe("WebTransport", () => { const writer = stream.writable.getWriter(); (async function read() { - const { done, value } = await reader.read(); + const header = await reader.read(); - if (done) { - return; - } + expect(header.value).to.eql(Uint8Array.of(107)); + + const { value } = await reader.read(); const handshake = TEXT_DECODER.decode(value); expect(handshake.startsWith("0{")).to.be(true); @@ -142,7 +145,8 @@ describe("WebTransport", () => { partialDone(); })(); - await writer.write(TEXT_ENCODER.encode("0")); + writer.write(Uint8Array.of(1)); + writer.write(TEXT_ENCODER.encode("0")); }); }); @@ -194,6 +198,10 @@ describe("WebTransport", () => { const writer = stream.writable.getWriter(); (async function read() { + const header = await reader.read(); + + expect(header.value).to.eql(Uint8Array.of(6)); + const { done, value } = await reader.read(); if (done) { @@ -206,10 +214,13 @@ describe("WebTransport", () => { partialDone(); })(); + await writer.write(Uint8Array.of(31)); await writer.write( TEXT_ENCODER.encode(`0{"sid":"${payload.sid}"}`) ); + await writer.write(Uint8Array.of(6)); await writer.write(TEXT_ENCODER.encode(`2probe`)); + await writer.write(Uint8Array.of(1)); await writer.write(TEXT_ENCODER.encode(`5`)); }); } @@ -281,10 +292,14 @@ describe("WebTransport", () => { }, async ({ engine, h3Server, reader, writer }) => { for (let i = 0; i < 5; i++) { + const header = await reader.read(); + expect(header.value).to.eql(Uint8Array.of(1)); + const packet = await reader.read(); const value = TEXT_DECODER.decode(packet.value); expect(value).to.eql("2"); + writer.write(Uint8Array.of(1)); writer.write(TEXT_ENCODER.encode("3")); } @@ -338,6 +353,7 @@ describe("WebTransport", () => { success(engine, h3Server, done); }); + writer.write(Uint8Array.of(6)); writer.write(TEXT_ENCODER.encode("4hello")); }); }); @@ -346,6 +362,9 @@ describe("WebTransport", () => { setup({}, async ({ engine, h3Server, socket, reader }) => { socket.send("hello"); + const header = await reader.read(); + expect(header.value).to.eql(Uint8Array.of(6)); + const { value } = await reader.read(); const decoded = TEXT_DECODER.decode(value); expect(decoded).to.eql("4hello"); @@ -363,6 +382,7 @@ describe("WebTransport", () => { success(engine, h3Server, done); }); + writer.write(Uint8Array.of(131)); writer.write(Uint8Array.of(1, 2, 3)); }); }); @@ -371,64 +391,49 @@ describe("WebTransport", () => { setup({}, async ({ engine, h3Server, socket, reader }) => { socket.send(Buffer.of(1, 2, 3)); - const { value } = await reader.read(); - expect(value).to.eql(Uint8Array.of(1, 2, 3)); - - success(engine, h3Server, done); - }); - }); - - it("should send some binary data (client to server) (with binary flag)", (done) => { - setup({}, async ({ engine, h3Server, socket, writer }) => { - socket.on("data", (data) => { - expect(Buffer.isBuffer(data)).to.be(true); - expect(data).to.eql(Buffer.of(48, 1, 2, 3)); - - success(engine, h3Server, done); - }); - - writer.write(Uint8Array.of(54)); - writer.write(Uint8Array.of(48, 1, 2, 3)); - }); - }); - - it("should send some binary data (server to client) (with binary flag)", (done) => { - setup({}, async ({ engine, h3Server, socket, reader }) => { - socket.send(Buffer.of(48, 1, 2, 3)); - const header = await reader.read(); - expect(header.value).to.eql(Uint8Array.of(54)); + expect(header.value).to.eql(Uint8Array.of(131)); const { value } = await reader.read(); - expect(value).to.eql(Uint8Array.of(48, 1, 2, 3)); + expect(value).to.eql(Uint8Array.of(1, 2, 3)); success(engine, h3Server, done); }); }); - it("should send some binary data (client to server) (binary flag)", (done) => { + it("should send some big binary data (client to server)", (done) => { setup({}, async ({ engine, h3Server, socket, writer }) => { + const payload = Buffer.allocUnsafe(1e6); + socket.on("data", (data) => { expect(Buffer.isBuffer(data)).to.be(true); - expect(data).to.eql(Buffer.of(54)); + expect(data).to.eql(payload); success(engine, h3Server, done); }); - writer.write(Uint8Array.of(54)); - writer.write(Uint8Array.of(54)); + writer.write(Uint8Array.of(255, 0, 0, 0, 0, 0, 15, 66, 64)); + writer.write(payload); }); }); - it("should send some binary data (server to client) (binary flag)", (done) => { + it("should send some big binary data (server to client)", (done) => { setup({}, async ({ engine, h3Server, socket, reader }) => { - socket.send(Buffer.of(54)); + const payload = Buffer.allocUnsafe(1e6); + + socket.send(payload); const header = await reader.read(); - expect(header.value).to.eql(Uint8Array.of(54)); + expect(header.value).to.eql( + Uint8Array.of(255, 0, 0, 0, 0, 0, 15, 66, 64) + ); - const { value } = await reader.read(); - expect(value).to.eql(Uint8Array.of(54)); + const chunk1 = await reader.read(); + // the size of the chunk is implementation-specific (maxDatagramSize) + expect(chunk1.value).to.eql(payload.slice(0, 1228)); + + const chunk2 = await reader.read(); + expect(chunk2.value).to.eql(payload.slice(1228, 2456)); success(engine, h3Server, done); });