From d55c39e0ed5cb7b3a34875a398efc111c91184f6 Mon Sep 17 00:00:00 2001 From: Damien Arrachequesne Date: Wed, 2 Aug 2023 01:18:30 +0200 Subject: [PATCH] fix(webtransport): add proper framing WebTransport being a stream-based protocol, the chunking boundaries are not necessarily preserved. That's why we need a header indicating the type of the payload (plain text or binary) and its length. See also: https://github.com/socketio/engine.io/commit/a306db09e8ddb367c7d62f45fec920f979580b7c --- lib/transports/webtransport.ts | 65 ++++++++++++---------------------- package-lock.json | 38 ++++++++++---------- package.json | 4 +-- test/webtransport.mjs | 2 ++ 4 files changed, 46 insertions(+), 63 deletions(-) diff --git a/lib/transports/webtransport.ts b/lib/transports/webtransport.ts index 8b23e4d11..9f411a636 100644 --- a/lib/transports/webtransport.ts +++ b/lib/transports/webtransport.ts @@ -1,25 +1,14 @@ import { Transport } from "../transport.js"; import { nextTick } from "./websocket-constructor.js"; import { - encodePacketToBinary, - decodePacketFromBinary, Packet, + createPacketDecoderStream, + createPacketEncoderStream, } from "engine.io-parser"; import debugModule from "debug"; // debug() const debug = debugModule("engine.io-client:webtransport"); // debug() -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 - ); -} - export class WT extends Transport { private transport: any; private writer: any; @@ -52,10 +41,16 @@ export class WT extends Transport { // note: we could have used async/await, but that would require some additional polyfills this.transport.ready.then(() => { this.transport.createBidirectionalStream().then((stream) => { - const reader = stream.readable.getReader(); - this.writer = stream.writable.getWriter(); + const decoderStream = createPacketDecoderStream( + Number.MAX_SAFE_INTEGER, + // TODO expose binarytype + "arraybuffer" + ); + const reader = stream.readable.pipeThrough(decoderStream).getReader(); - let binaryFlag; + const encoderStream = createPacketEncoderStream(); + encoderStream.readable.pipeTo(stream.writable); + this.writer = encoderStream.writable.getWriter(); const read = () => { reader @@ -66,15 +61,7 @@ export class WT extends Transport { return; } debug("received chunk: %o", value); - if (!binaryFlag && value.byteLength === 1 && value[0] === 54) { - binaryFlag = true; - } else { - // TODO expose binarytype - this.onPacket( - decodePacketFromBinary(value, binaryFlag, "arraybuffer") - ); - binaryFlag = false; - } + this.onPacket(value); read(); }) .catch((err) => { @@ -84,10 +71,11 @@ export class WT extends Transport { read(); - const handshake = this.query.sid ? `0{"sid":"${this.query.sid}"}` : "0"; - this.writer - .write(new TextEncoder().encode(handshake)) - .then(() => this.onOpen()); + const packet: Packet = { type: "open" }; + if (this.query.sid) { + packet.data = `{"sid":"${this.query.sid}"}`; + } + this.writer.write(packet).then(() => this.onOpen()); }); }); } @@ -99,20 +87,13 @@ export class WT extends Transport { const packet = packets[i]; const lastPacket = i === packets.length - 1; - encodePacketToBinary(packet, (data) => { - if (shouldIncludeBinaryHeader(packet, data)) { - debug("writing binary header"); - this.writer.write(Uint8Array.of(54)); + this.writer.write(packet).then(() => { + if (lastPacket) { + nextTick(() => { + this.writable = true; + this.emitReserved("drain"); + }, this.setTimeoutFn); } - debug("writing chunk: %o", data); - this.writer.write(data).then(() => { - if (lastPacket) { - nextTick(() => { - this.writable = true; - this.emitReserved("drain"); - }, this.setTimeoutFn); - } - }); }); } } diff --git a/package-lock.json b/package-lock.json index a1a23aba6..fe5ab0671 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "name": "engine.io-client", - "version": "6.4.0", + "version": "6.5.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "engine.io-client", - "version": "6.4.0", + "version": "6.5.1", "license": "MIT", "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", - "engine.io-parser": "~5.1.0", + "engine.io-parser": "~5.2.1", "ws": "~8.11.0", "xmlhttprequest-ssl": "~2.0.0" }, @@ -29,7 +29,7 @@ "@types/sinonjs__fake-timers": "^6.0.3", "babel-loader": "^8.2.2", "blob": "0.0.5", - "engine.io": "^6.5.0-alpha.1", + "engine.io": "^6.5.2-alpha.1", "expect.js": "^0.3.1", "express": "^4.17.1", "mocha": "^10.2.0", @@ -4460,9 +4460,9 @@ } }, "node_modules/engine.io": { - "version": "6.5.0-alpha.1", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.0-alpha.1.tgz", - "integrity": "sha512-RNvg3XX23Zb8jf5H/+wZSZA+slDY80em2hB0qW/kK4MRpAIiUeH/+yrKRP2zK29swa60kHrIOuYF1liGV/Y9Eg==", + "version": "6.5.2-alpha.1", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.2-alpha.1.tgz", + "integrity": "sha512-VLohbV4MECyOSa9FLxbb+M1fqoy+1GB9mR1FvXrIVKgsequ1KAOx2JSnYmfjyYdxzN4MyeUKk9rezfy7NWiZvQ==", "dev": true, "dependencies": { "@types/cookie": "^0.4.1", @@ -4473,17 +4473,17 @@ "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" }, "engines": { - "node": ">=10.0.0" + "node": ">=10.2.0" } }, "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==", + "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" } @@ -17891,9 +17891,9 @@ } }, "engine.io": { - "version": "6.5.0-alpha.1", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.0-alpha.1.tgz", - "integrity": "sha512-RNvg3XX23Zb8jf5H/+wZSZA+slDY80em2hB0qW/kK4MRpAIiUeH/+yrKRP2zK29swa60kHrIOuYF1liGV/Y9Eg==", + "version": "6.5.2-alpha.1", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.2-alpha.1.tgz", + "integrity": "sha512-VLohbV4MECyOSa9FLxbb+M1fqoy+1GB9mR1FvXrIVKgsequ1KAOx2JSnYmfjyYdxzN4MyeUKk9rezfy7NWiZvQ==", "dev": true, "requires": { "@types/cookie": "^0.4.1", @@ -17904,14 +17904,14 @@ "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" } }, "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==" }, "enhanced-resolve": { "version": "4.3.0", diff --git a/package.json b/package.json index 2a15c1e26..383a05e53 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", - "engine.io-parser": "~5.1.0", + "engine.io-parser": "~5.2.1", "ws": "~8.11.0", "xmlhttprequest-ssl": "~2.0.0" }, @@ -63,7 +63,7 @@ "@types/sinonjs__fake-timers": "^6.0.3", "babel-loader": "^8.2.2", "blob": "0.0.5", - "engine.io": "^6.5.0-alpha.1", + "engine.io": "^6.5.2-alpha.1", "expect.js": "^0.3.1", "express": "^4.17.1", "mocha": "^10.2.0", diff --git a/test/webtransport.mjs b/test/webtransport.mjs index 3ee27d21b..1ff7b917b 100644 --- a/test/webtransport.mjs +++ b/test/webtransport.mjs @@ -5,9 +5,11 @@ import { Server } from "engine.io"; import { Socket } from "../build/esm-debug/index.js"; import { generateWebTransportCertificate } from "./util-wt.mjs"; import { createServer } from "http"; +import { TransformStream } from "stream/web"; if (typeof window === "undefined") { global.WebTransport = WebTransport; + global.TransformStream = TransformStream; } async function setup(opts, cb) {