Skip to content

Commit a306db0

Browse files
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. We will use a format inspired by the WebSocket frame: - first bit indicates whether the payload is binary - the next 7 bits are either: - 125 or less: that's the length of the payload - 126: the next 2 bytes represent the length of the payload - 127: the next 8 bytes represent the length of the payload Reference: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#decoding_payload_length Related: - #687 - #688
1 parent 7dd1350 commit a306db0

File tree

5 files changed

+122
-116
lines changed

5 files changed

+122
-116
lines changed

lib/server.ts

+20-18
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,11 @@ import type { CookieSerializeOptions } from "cookie";
1616
import type { CorsOptions, CorsOptionsDelegate } from "cors";
1717
import type { Duplex } from "stream";
1818
import { WebTransport } from "./transports/webtransport";
19-
import { TextDecoder } from "util";
19+
import { createPacketDecoderStream } from "engine.io-parser";
2020

2121
const debug = debugModule("engine");
2222

2323
const kResponseHeaders = Symbol("responseHeaders");
24-
const TEXT_DECODER = new TextDecoder();
2524

2625
type Transport = "polling" | "websocket";
2726

@@ -149,15 +148,13 @@ type Middleware = (
149148
next: (err?: any) => void
150149
) => void;
151150

152-
function parseSessionId(handshake: string) {
153-
if (handshake.startsWith("0{")) {
154-
try {
155-
const parsed = JSON.parse(handshake.substring(1));
156-
if (typeof parsed.sid === "string") {
157-
return parsed.sid;
158-
}
159-
} catch (e) {}
160-
}
151+
function parseSessionId(data: string) {
152+
try {
153+
const parsed = JSON.parse(data);
154+
if (typeof parsed.sid === "string") {
155+
return parsed.sid;
156+
}
157+
} catch (e) {}
161158
}
162159

163160
export abstract class BaseServer extends EventEmitter {
@@ -536,7 +533,11 @@ export abstract class BaseServer extends EventEmitter {
536533
}
537534

538535
const stream = result.value;
539-
const reader = stream.readable.getReader();
536+
const transformStream = createPacketDecoderStream(
537+
this.opts.maxHttpBufferSize,
538+
"nodebuffer"
539+
);
540+
const reader = stream.readable.pipeThrough(transformStream).getReader();
540541

541542
// reading the first packet of the stream
542543
const { value, done } = await reader.read();
@@ -546,12 +547,13 @@ export abstract class BaseServer extends EventEmitter {
546547
}
547548

548549
clearTimeout(timeout);
549-
const handshake = TEXT_DECODER.decode(value);
550550

551-
// handshake is either
552-
// "0" => new session
553-
// '0{"sid":"xxxx"}' => upgrade
554-
if (handshake === "0") {
551+
if (value.type !== "open") {
552+
debug("invalid WebTransport handshake");
553+
return session.close();
554+
}
555+
556+
if (value.data === undefined) {
555557
const transport = new WebTransport(session, stream, reader);
556558

557559
// note: we cannot use "this.generateId()", because there is no "req" argument
@@ -572,7 +574,7 @@ export abstract class BaseServer extends EventEmitter {
572574
return;
573575
}
574576

575-
const sid = parseSessionId(handshake);
577+
const sid = parseSessionId(value.data);
576578

577579
if (!sid) {
578580
debug("invalid WebTransport handshake");

lib/transports/webtransport.ts

+28-46
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,9 @@
11
import { Transport } from "../transport";
22
import debugModule from "debug";
3+
import { createPacketEncoderStream } from "engine.io-parser";
34

45
const debug = debugModule("engine:webtransport");
56

6-
const BINARY_HEADER = Buffer.of(54);
7-
8-
function shouldIncludeBinaryHeader(packet, encoded) {
9-
// 48 === "0".charCodeAt(0) (OPEN packet type)
10-
// 54 === "6".charCodeAt(0) (NOOP packet type)
11-
return (
12-
packet.type === "message" &&
13-
typeof packet.data !== "string" &&
14-
encoded[0] >= 48 &&
15-
encoded[0] <= 54
16-
);
17-
}
18-
197
/**
208
* Reference: https://developer.mozilla.org/en-US/docs/Web/API/WebTransport_API
219
*/
@@ -24,24 +12,24 @@ export class WebTransport extends Transport {
2412

2513
constructor(private readonly session, stream, reader) {
2614
super({ _query: { EIO: "4" } });
27-
this.writer = stream.writable.getWriter();
15+
16+
const transformStream = createPacketEncoderStream();
17+
transformStream.readable.pipeTo(stream.writable);
18+
this.writer = transformStream.writable.getWriter();
19+
2820
(async () => {
29-
let binaryFlag = false;
30-
while (true) {
31-
const { value, done } = await reader.read();
32-
if (done) {
33-
debug("session is closed");
34-
break;
35-
}
36-
debug("received chunk: %o", value);
37-
if (!binaryFlag && value.byteLength === 1 && value[0] === 54) {
38-
binaryFlag = true;
39-
continue;
21+
try {
22+
while (true) {
23+
const { value, done } = await reader.read();
24+
if (done) {
25+
debug("session is closed");
26+
break;
27+
}
28+
debug("received chunk: %o", value);
29+
this.onPacket(value);
4030
}
41-
this.onPacket(
42-
this.parser.decodePacketFromBinary(value, binaryFlag, "nodebuffer")
43-
);
44-
binaryFlag = false;
31+
} catch (e) {
32+
debug("error while reading: %s", e.message);
4533
}
4634
})();
4735

@@ -58,26 +46,20 @@ export class WebTransport extends Transport {
5846
return true;
5947
}
6048

61-
send(packets) {
49+
async send(packets) {
6250
this.writable = false;
6351

64-
for (let i = 0; i < packets.length; i++) {
65-
const packet = packets[i];
66-
const isLast = i + 1 === packets.length;
67-
68-
this.parser.encodePacketToBinary(packet, (data) => {
69-
if (shouldIncludeBinaryHeader(packet, data)) {
70-
debug("writing binary header");
71-
this.writer.write(BINARY_HEADER);
72-
}
73-
debug("writing chunk: %o", data);
74-
this.writer.write(data);
75-
if (isLast) {
76-
this.writable = true;
77-
this.emit("drain");
78-
}
79-
});
52+
try {
53+
for (let i = 0; i < packets.length; i++) {
54+
const packet = packets[i];
55+
await this.writer.write(packet);
56+
}
57+
} catch (e) {
58+
debug("error while writing: %s", e.message);
8059
}
60+
61+
this.writable = true;
62+
this.emit("drain");
8163
}
8264

8365
doClose(fn) {

package-lock.json

+25-8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
"cookie": "~0.4.1",
4040
"cors": "~2.8.5",
4141
"debug": "~4.3.1",
42-
"engine.io-parser": "~5.1.0",
42+
"engine.io-parser": "~5.2.1",
4343
"ws": "~8.11.0"
4444
},
4545
"devDependencies": {

0 commit comments

Comments
 (0)