From ceb6e090b40d9ed8cbc02fd982aad8fa6f93d16f Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sun, 27 Apr 2025 13:47:58 +0000 Subject: [PATCH 1/9] Implement prototype for maxDatagramSize --- main/lib/http2/browser/browser.js | 7 ++++- main/lib/http2/browser/browserparser.js | 30 +++++++++++++------ main/lib/http2/node/capsuleparser.js | 35 ++++++++++++++-------- main/lib/http2/node/client.js | 17 ++++++++--- main/lib/http2/node/server.js | 40 ++++++++++++++++++------- main/lib/http2/node/websocketparser.js | 34 +++++++++++++-------- main/lib/http2/parserbase.js | 27 ++++++++++++++++- main/lib/http2/parserbasehttp2.js | 8 +++-- main/lib/http2/session.js | 3 +- main/lib/types.ts | 6 +++- 10 files changed, 154 insertions(+), 53 deletions(-) diff --git a/main/lib/http2/browser/browser.js b/main/lib/http2/browser/browser.js index f26fb056..7cfc30dc 100644 --- a/main/lib/http2/browser/browser.js +++ b/main/lib/http2/browser/browser.js @@ -32,6 +32,9 @@ export class Http2WebTransportBrowser { args.sessionShouldAutoTuneReceiveWindow || true this.sessionFlowControlWindowSizeLimit = args?.sessionFlowControlWindowSizeLimit || 15 * 1024 * 1024 + + this.initialDatagramSize = + args.initialDatagramSize || this.streamFlowControlWindowSizeLimit - 128 /** @type {import('../../session.js').HttpClient} */ // @ts-ignore this.jsobj = undefined // the transport will set this @@ -148,7 +151,9 @@ export class Http2WebTransportBrowser { this.initialStreamFlowControlWindow, streamShouldAutoTuneReceiveWindow: this.streamShouldAutoTuneReceiveWindow, - streamReceiveWindowSizeLimit: this.streamFlowControlWindowSizeLimit + streamReceiveWindowSizeLimit: this.streamFlowControlWindowSizeLimit, + maxDatagramSize: this.initialDatagramSize, + remoteMaxDatagramSize: 2 ** 62 - 1 }) if (this.clientInt) this.clientInt.addEventListener('close', (event) => { diff --git a/main/lib/http2/browser/browserparser.js b/main/lib/http2/browser/browserparser.js index 1050ab25..6dd396d2 100644 --- a/main/lib/http2/browser/browserparser.js +++ b/main/lib/http2/browser/browserparser.js @@ -86,7 +86,9 @@ export class BrowserParser extends ParserBase { initialStreamSendWindowOffsetBidi, initialStreamReceiveWindowOffset, streamShouldAutoTuneReceiveWindow, - streamReceiveWindowSizeLimit + streamReceiveWindowSizeLimit, + maxDatagramSize, + remoteMaxDatagramSize }) { super({ nativesession, @@ -95,7 +97,9 @@ export class BrowserParser extends ParserBase { initialStreamSendWindowOffsetBidi, initialStreamReceiveWindowOffset, streamShouldAutoTuneReceiveWindow, - streamReceiveWindowSizeLimit + streamReceiveWindowSizeLimit, + maxDatagramSize, + remoteMaxDatagramSize }) this.ws = ws /** @type {Buffer|undefined} */ @@ -220,6 +224,9 @@ export class BrowserParser extends ParserBase { case ParserBase.WT_STREAMS_BLOCKED_BIDI: this.onStreamsBlockedBidi(readVarInt(bufferstate)) break + case ParserBase.WT_MAX_DATAGRAM_SIZE: + this.onMaxDatagramSize(readVarInt(bufferstate)) + break case ParserBase.CLOSE_WEBTRANSPORT_SESSION: { const code = readUint32(bufferstate) || 0 @@ -238,13 +245,18 @@ export class BrowserParser extends ParserBase { this.onDrain() break case ParserBase.DATAGRAM: - this.session.jsobj.onDatagramReceived({ - datagram: new Uint8Array( - bufferstate.buffer.buffer, - bufferstate.buffer.byteOffset + bufferstate.offset, - offsetend - bufferstate.offset - ) - }) + if (offsetend - bufferstate.offset <= this.maxDatagramSize) { + // actually for the browser it is already too late + // but to give developers a consitent behaviour, we drop it here as well + // drop too large datagrams + this.session.jsobj.onDatagramReceived({ + datagram: new Uint8Array( + bufferstate.buffer.buffer, + bufferstate.buffer.byteOffset + bufferstate.offset, + offsetend - bufferstate.offset + ) + }) + } break default: diff --git a/main/lib/http2/node/capsuleparser.js b/main/lib/http2/node/capsuleparser.js index bc280184..849b0228 100644 --- a/main/lib/http2/node/capsuleparser.js +++ b/main/lib/http2/node/capsuleparser.js @@ -18,7 +18,9 @@ export class Http2CapsuleParser extends ParserBaseHttp2 { initialStreamSendWindowOffsetUnidi, initialStreamReceiveWindowOffset, streamShouldAutoTuneReceiveWindow, - streamReceiveWindowSizeLimit + streamReceiveWindowSizeLimit, + maxDatagramSize, + remoteMaxDatagramSize }) { super({ stream, @@ -28,7 +30,9 @@ export class Http2CapsuleParser extends ParserBaseHttp2 { initialStreamSendWindowOffsetUnidi, initialStreamReceiveWindowOffset, streamShouldAutoTuneReceiveWindow, - streamReceiveWindowSizeLimit + streamReceiveWindowSizeLimit, + maxDatagramSize, + remoteMaxDatagramSize }) this.mode = 's' // capsule start /** @type {Buffer|undefined} */ @@ -96,8 +100,9 @@ export class Http2CapsuleParser extends ParserBaseHttp2 { if ( type === Http2CapsuleParser.PADDING || type === Http2CapsuleParser.WT_STREAM_WOFIN || - type === - Http2CapsuleParser.WT_STREAM_WFIN /* || type === Http2CapsuleParser.DATAGRAM */ + type === Http2CapsuleParser.WT_STREAM_WFIN || + (type === Http2CapsuleParser.DATAGRAM && + length > this.maxDatagramSize) // if we exceed maximum size of datagram we drop ) { checklength = Math.min(length, 64) // stream id + some Data } @@ -109,7 +114,7 @@ export class Http2CapsuleParser extends ParserBaseHttp2 { // too long abort, could be an attack vector this.session.closeConnection({ code: 63, // QUIC_FLOW_CONTROL_SENT_TOO_MUCH_DATA, // probably the right one... - reason: 'Frame length too big :' + length + reason: 'Frame length too big : ' + length }) return } @@ -223,6 +228,9 @@ export class Http2CapsuleParser extends ParserBaseHttp2 { case Http2CapsuleParser.WT_STREAMS_BLOCKED_BIDI: this.onStreamsBlockedBidi(readVarInt(bufferstate)) break + case Http2CapsuleParser.WT_MAX_DATAGRAM_SIZE: + this.onMaxDatagramSize(readVarInt(bufferstate)) + break case Http2CapsuleParser.CLOSE_WEBTRANSPORT_SESSION: { const code = readUint32(bufferstate) || 0 @@ -241,13 +249,16 @@ export class Http2CapsuleParser extends ParserBaseHttp2 { this.onDrain() break case Http2CapsuleParser.DATAGRAM: - this.session.jsobj.onDatagramReceived({ - datagram: new Uint8Array( - bufferstate.buffer.buffer, - bufferstate.buffer.byteOffset + bufferstate.offset, - offsetend - bufferstate.offset - ) - }) + if (length <= this.maxDatagramSize) { + // drop too large datagrams + this.session.jsobj.onDatagramReceived({ + datagram: new Uint8Array( + bufferstate.buffer.buffer, + bufferstate.buffer.byteOffset + bufferstate.offset, + offsetend - bufferstate.offset + ) + }) + } break default: // do nothing diff --git a/main/lib/http2/node/client.js b/main/lib/http2/node/client.js index 7cccf510..52f1fcfb 100644 --- a/main/lib/http2/node/client.js +++ b/main/lib/http2/node/client.js @@ -38,6 +38,9 @@ export class Http2WebTransportClient { args.sessionShouldAutoTuneReceiveWindow || true this.sessionFlowControlWindowSizeLimit = args?.sessionFlowControlWindowSizeLimit || 15 * 1024 * 1024 + + this.initialDatagramSize = + args.initialDatagramSize || this.streamFlowControlWindowSizeLimit - 128 /** @type {import('../../session.js').HttpClient} */ // @ts-ignore this.jsobj = undefined // the transport will set this @@ -92,10 +95,13 @@ export class Http2WebTransportClient { 0x2b62: this.initialStreamFlowControlWindow, // SETTINGS_WEBTRANSPORT_INITIAL_MAX_STREAM_DATA_UNI 0x2b63: this.initialStreamFlowControlWindow, // SETTINGS_WEBTRANSPORT_INITIAL_MAX_STREAM_DATA_BIDI 0x2b64: this.initialUnidirectionalStreams, // SETTINGS_WEBTRANSPORT_INITIAL_MAX_STREAMS_UNI - 0x2b65: this.initialBidirectionalStreams // SETTINGS_WEBTRANSPORT_INITIAL_MAX_STREAMS_BIDI + 0x2b65: this.initialBidirectionalStreams, // SETTINGS_WEBTRANSPORT_INITIAL_MAX_STREAMS_BIDI + 0x2b66: this.initialDatagramSize // SETTINGS_MAX_DATAGRAM_SIZE } }, - remoteCustomSettings: [0x2b60, 0x2b61, 0x2b62, 0x2b63, 0x2b64, 0x2b65], + remoteCustomSettings: [ + 0x2b60, 0x2b61, 0x2b62, 0x2b63, 0x2b64, 0x2b65, 0x2b66 + ], localPort: this.localPort, // TODO: REMOVE BEFORE RELEASE; UNSAFE SETTING rejectUnauthorized: !this.serverCertificateHashes @@ -231,7 +237,8 @@ export class Http2WebTransportClient { 0x2b64: remoteUnidirectionalStreams = undefined, 0x2b63: remoteBidirectionalStreamFlowControlWindow = undefined, 0x2b62: remoteUnidirectionalStreamFlowControlWindow = undefined, - 0x2b61: remoteSessionFlowControlWindow = undefined + 0x2b61: remoteSessionFlowControlWindow = undefined, + 0x2b66: remoteMaxDatagramSize = 2 ** 62 - 1 // note this exceeds safe integer // @ts-ignore } = this.clientInt.remoteSettings?.customSettings || {} @@ -255,7 +262,9 @@ export class Http2WebTransportClient { this.initialStreamFlowControlWindow, streamShouldAutoTuneReceiveWindow: this.streamShouldAutoTuneReceiveWindow, - streamReceiveWindowSizeLimit: this.streamFlowControlWindowSizeLimit + streamReceiveWindowSizeLimit: this.streamFlowControlWindowSizeLimit, + maxDatagramSize: this.initialDatagramSize, + remoteMaxDatagramSize }), initialBidirectionalSendStreams: remoteBidirectionalStreams || this.initialBidirectionalStreams, diff --git a/main/lib/http2/node/server.js b/main/lib/http2/node/server.js index 9a237d01..7fd9731c 100644 --- a/main/lib/http2/node/server.js +++ b/main/lib/http2/node/server.js @@ -42,6 +42,9 @@ export class Http2WebTransportServer { this.sessionFlowControlWindowSizeLimit = args?.sessionFlowControlWindowSizeLimit || 15 * 1024 * 1024 + this.initialDatagramSize = + args.initialDatagramSize || this.streamFlowControlWindowSizeLimit - 128 + /** @type {Record} */ this.paths = {} this.hasrequesthandler = false @@ -64,10 +67,13 @@ export class Http2WebTransportServer { 0x2b62: this.initialStreamFlowControlWindow, // SETTINGS_WEBTRANSPORT_INITIAL_MAX_STREAM_DATA_UNI 0x2b63: this.initialStreamFlowControlWindow, // SETTINGS_WEBTRANSPORT_INITIAL_MAX_STREAM_DATA_BIDI 0x2b64: this.initialUnidirectionalStreams, // SETTINGS_WEBTRANSPORT_INITIAL_MAX_STREAMS_UNI - 0x2b65: this.initialBidirectionalStreams // SETTINGS_WEBTRANSPORT_INITIAL_MAX_STREAMS_BIDI + 0x2b65: this.initialBidirectionalStreams, // SETTINGS_WEBTRANSPORT_INITIAL_MAX_STREAMS_BIDI + 0x2b66: this.initialDatagramSize // SETTINGS_MAX_DATAGRAM_SIZE } }, - remoteCustomSettings: [0x2b60, 0x2b61, 0x2b62, 0x2b63, 0x2b64, 0x2b65] + remoteCustomSettings: [ + 0x2b60, 0x2b61, 0x2b62, 0x2b63, 0x2b64, 0x2b65, 0x2b66 + ] }) this.serverInt.on('listening', () => { @@ -180,7 +186,9 @@ export class Http2WebTransportServer { streamShouldAutoTuneReceiveWindow: this.streamShouldAutoTuneReceiveWindow, streamReceiveWindowSizeLimit: - this.streamFlowControlWindowSizeLimit + this.streamFlowControlWindowSizeLimit, + maxDatagramSize: this.initialDatagramSize, + remoteMaxDatagramSize: 2 ** 62 - 1 // we wait for the settings frame to be received })) if (head.byteLength > 0) parse.parseData(head) return parse @@ -278,7 +286,8 @@ export class Http2WebTransportServer { 0x2b64: remoteUnidirectionalStreams = undefined, 0x2b63: remoteBidirectionalStreamFlowControlWindow = undefined, 0x2b62: remoteUnidirectionalStreamFlowControlWindow = undefined, - 0x2b61: remoteSessionFlowControlWindow = undefined + 0x2b61: remoteSessionFlowControlWindow = undefined, + 0x2b66: remoteMaxDatagramSize = 2 ** 62 - 1 // note this exceeds safe integer // @ts-ignore } = stream?.session?.remoteSettings?.customSettings || {} const retObj = { @@ -304,7 +313,9 @@ export class Http2WebTransportServer { streamShouldAutoTuneReceiveWindow: this.streamShouldAutoTuneReceiveWindow, streamReceiveWindowSizeLimit: - this.streamFlowControlWindowSizeLimit + this.streamFlowControlWindowSizeLimit, + maxDatagramSize: this.initialDatagramSize, + remoteMaxDatagramSize }) } else { return (this.capsParser = new WebSocketParser({ @@ -318,7 +329,9 @@ export class Http2WebTransportServer { streamShouldAutoTuneReceiveWindow: this.streamShouldAutoTuneReceiveWindow, streamReceiveWindowSizeLimit: - this.streamFlowControlWindowSizeLimit + this.streamFlowControlWindowSizeLimit, + maxDatagramSize: this.initialDatagramSize, + remoteMaxDatagramSize })) } }, @@ -523,7 +536,9 @@ export class Http2WebTransportServer { streamShouldAutoTuneReceiveWindow: this.streamShouldAutoTuneReceiveWindow, streamReceiveWindowSizeLimit: - this.streamFlowControlWindowSizeLimit + this.streamFlowControlWindowSizeLimit, + maxDatagramSize: this.initialDatagramSize, + remoteMaxDatagramSize: 2 ** 62 - 1 // we wait for the frame })) if (head && head.byteLength > 0) parse.parseData(head) return parse @@ -570,7 +585,8 @@ export class Http2WebTransportServer { 0x2b64: remoteUnidirectionalStreams = undefined, 0x2b63: remoteBidirectionalStreamFlowControlWindow = undefined, 0x2b62: remoteUnidirectionalStreamFlowControlWindow = undefined, - 0x2b61: remoteSessionFlowControlWindow = undefined + 0x2b61: remoteSessionFlowControlWindow = undefined, + 0x2b66: remoteMaxDatagramSize = 2 ** 62 - 1 // note this exceeds safe integer // @ts-ignore } = stream?.session?.remoteSettings?.customSettings || {} const retObj = { @@ -596,7 +612,9 @@ export class Http2WebTransportServer { streamShouldAutoTuneReceiveWindow: this.streamShouldAutoTuneReceiveWindow, streamReceiveWindowSizeLimit: - this.streamFlowControlWindowSizeLimit + this.streamFlowControlWindowSizeLimit, + maxDatagramSize: this.initialDatagramSize, + remoteMaxDatagramSize }) } else { return (this.capsParser = new WebSocketParser({ @@ -610,7 +628,9 @@ export class Http2WebTransportServer { streamShouldAutoTuneReceiveWindow: this.streamShouldAutoTuneReceiveWindow, streamReceiveWindowSizeLimit: - this.streamFlowControlWindowSizeLimit + this.streamFlowControlWindowSizeLimit, + maxDatagramSize: this.initialDatagramSize, + remoteMaxDatagramSize })) } }, diff --git a/main/lib/http2/node/websocketparser.js b/main/lib/http2/node/websocketparser.js index b872b253..f1e24824 100644 --- a/main/lib/http2/node/websocketparser.js +++ b/main/lib/http2/node/websocketparser.js @@ -183,7 +183,9 @@ export class WebSocketParser extends ParserBaseHttp2 { initialStreamSendWindowOffsetBidi, initialStreamReceiveWindowOffset, streamShouldAutoTuneReceiveWindow, - streamReceiveWindowSizeLimit + streamReceiveWindowSizeLimit, + maxDatagramSize, + remoteMaxDatagramSize }) { super({ stream, @@ -193,7 +195,9 @@ export class WebSocketParser extends ParserBaseHttp2 { initialStreamSendWindowOffsetBidi, initialStreamReceiveWindowOffset, streamShouldAutoTuneReceiveWindow, - streamReceiveWindowSizeLimit + streamReceiveWindowSizeLimit, + maxDatagramSize, + remoteMaxDatagramSize }) this.mode = 's' // frame start /** @type {Buffer|undefined} */ @@ -433,8 +437,8 @@ export class WebSocketParser extends ParserBaseHttp2 { if ( type === ParserBase.PADDING || type === ParserBase.WT_STREAM_WOFIN || - type === - ParserBase.WT_STREAM_WFIN /* || type === ParserBase.DATAGRAM */ + type === ParserBase.WT_STREAM_WFIN || + (type === ParserBase.DATAGRAM && length > this.maxDatagramSize) // if we exceed maximum size of datagram we drop ) { checklength = Math.min(length, 64) // stream id + some Data } @@ -608,6 +612,9 @@ export class WebSocketParser extends ParserBaseHttp2 { case ParserBase.WT_STREAMS_BLOCKED_BIDI: this.onStreamsBlockedBidi(readVarInt(bufferstate)) break + case ParserBase.WT_MAX_DATAGRAM_SIZE: + this.onMaxDatagramSize(readVarInt(bufferstate)) + break case ParserBase.CLOSE_WEBTRANSPORT_SESSION: { const code = readUint32(bufferstate) || 0 @@ -626,14 +633,17 @@ export class WebSocketParser extends ParserBaseHttp2 { this.onDrain() break case ParserBase.DATAGRAM: - if (wbufferstate) { - this.session.jsobj.onDatagramReceived({ - datagram: new Uint8Array( - wbufferstate.buffer.buffer, - wbufferstate.buffer.byteOffset + wbufferstate.offset, - offsetend - wbufferstate.offset - ) - }) + if (length <= this.maxDatagramSize) { + // drop too large datagrams + if (wbufferstate) { + this.session.jsobj.onDatagramReceived({ + datagram: new Uint8Array( + wbufferstate.buffer.buffer, + wbufferstate.buffer.byteOffset + wbufferstate.offset, + offsetend - wbufferstate.offset + ) + }) + } } break default: diff --git a/main/lib/http2/parserbase.js b/main/lib/http2/parserbase.js index fc3f3abc..07587f02 100644 --- a/main/lib/http2/parserbase.js +++ b/main/lib/http2/parserbase.js @@ -31,6 +31,7 @@ export class ParserBase { static WT_STREAM_DATA_BLOCKED = 0x190b4d42 static WT_STREAMS_BLOCKED_UNIDI = 0x190b4d43 static WT_STREAMS_BLOCKED_BIDI = 0x190b4d44 + static WT_MAX_DATAGRAM_SIZE = 0x190b4d45 static CLOSE_WEBTRANSPORT_SESSION = 0x2843 static DRAIN_WEBTRANSPORT_SESSION = 0x78ae static DATAGRAM = 0x00 @@ -45,7 +46,9 @@ export class ParserBase { initialStreamSendWindowOffsetUnidi, initialStreamReceiveWindowOffset, streamShouldAutoTuneReceiveWindow, - streamReceiveWindowSizeLimit + streamReceiveWindowSizeLimit, + maxDatagramSize, + remoteMaxDatagramSize }) { this.session = nativesession this.isclient = isclient @@ -57,6 +60,12 @@ export class ParserBase { this.initialStreamReceiveWindowOffset = initialStreamReceiveWindowOffset this.streamShouldAutoTuneReceiveWindow = streamShouldAutoTuneReceiveWindow this.streamReceiveWindowSizeLimit = streamReceiveWindowSizeLimit + this.remoteMaxDatagramSize = remoteMaxDatagramSize + this.maxDatagramSize = Math.min( + maxDatagramSize, + this.streamReceiveWindowSizeLimit, + Math.max(this.streamReceiveWindowSizeLimit - 128, 9000) + ) /** @type {Map} */ this.wtstreams = new Map() @@ -110,6 +119,14 @@ export class ParserBase { }) } + sendMaxDatagramSize() { + this.writeCapsule({ + type: ParserBase.WT_MAX_DATAGRAM_SIZE, + headerVints: [this.maxDatagramSize], + payload: undefined + }) + } + /** * @param {bigint} streamid * @param {{sendOrder: bigint,sendGroupId: bigint}} priority @@ -250,6 +267,14 @@ export class ParserBase { // if (object && offset) object.flowController.reportBlocked(offset) } + /** + * @param {bigint|undefined} maxDatagramSize + */ + onMaxDatagramSize(maxDatagramSize) { + if (typeof maxDatagramSize === 'undefined') return + this.remoteMaxDatagramSize = Number(maxDatagramSize) + } + /** * @param {bigint|undefined} maxstreams */ diff --git a/main/lib/http2/parserbasehttp2.js b/main/lib/http2/parserbasehttp2.js index 26588fa4..ad41a5f8 100644 --- a/main/lib/http2/parserbasehttp2.js +++ b/main/lib/http2/parserbasehttp2.js @@ -78,7 +78,9 @@ export class ParserBaseHttp2 extends ParserBase { initialStreamSendWindowOffsetUnidi, initialStreamReceiveWindowOffset, streamShouldAutoTuneReceiveWindow, - streamReceiveWindowSizeLimit + streamReceiveWindowSizeLimit, + maxDatagramSize, + remoteMaxDatagramSize }) { super({ nativesession, @@ -87,7 +89,9 @@ export class ParserBaseHttp2 extends ParserBase { initialStreamSendWindowOffsetUnidi, initialStreamReceiveWindowOffset, streamShouldAutoTuneReceiveWindow, - streamReceiveWindowSizeLimit + streamReceiveWindowSizeLimit, + maxDatagramSize, + remoteMaxDatagramSize }) this.stream = stream this.session = nativesession diff --git a/main/lib/http2/session.js b/main/lib/http2/session.js index 602e593f..2079e0a8 100644 --- a/main/lib/http2/session.js +++ b/main/lib/http2/session.js @@ -122,6 +122,7 @@ export class Http2WebTransportSession { this.flowController.sendWindowUpdate() this.streamIdMngrBi.sendMaxStreamsFrameInitial() this.streamIdMngrUni.sendMaxStreamsFrameInitial() + this.capsParser.sendMaxDatagramSize() } } @@ -256,7 +257,7 @@ export class Http2WebTransportSession { } getMaxDatagramSize() { - return 16384 // this completly arbitry, we do not have a real restriction, but we choose more than quiche, to make things interesting + return this.capsParser.remoteMaxDatagramSize } /* diff --git a/main/lib/types.ts b/main/lib/types.ts index 8e215184..6af53d19 100644 --- a/main/lib/types.ts +++ b/main/lib/types.ts @@ -58,6 +58,7 @@ export interface NativeServerOptions { initialUnidirectionalSendStreams?: number initialUnidirectionalReceiveStreams?: number initialStreamFlowControlWindow?: number + initialDatagramSize?: number streamShouldAutoTuneReceiveWindow?: boolean streamFlowControlWindowSizeLimit?: number initialSessionFlowControlWindow?: number @@ -79,6 +80,7 @@ export interface NativeClientOptions { initialUnidirectionalSendStreams?: number initialUnidirectionalReceiveStreams?: number initialStreamFlowControlWindow?: number + initialDatagramSize?: number streamShouldAutoTuneReceiveWindow?: boolean streamFlowControlWindowSizeLimit?: number initialSessionFlowControlWindow?: number @@ -357,7 +359,9 @@ export interface ParserInit { initialStreamSendWindowOffsetUnidi: number initialStreamReceiveWindowOffset: number streamShouldAutoTuneReceiveWindow: boolean - streamReceiveWindowSizeLimit: number + streamReceiveWindowSizeLimit: number, + maxDatagramSize: number, + remoteMaxDatagramSize: number } export interface ParserHttp2Init extends ParserInit { From db7ff326b09f4a242e93e808c8ca1215de57b997 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sun, 27 Apr 2025 17:59:13 +0000 Subject: [PATCH 2/9] Use initialSessionFlowControlWindow as default for initialDatagramSize --- main/lib/http2/browser/browser.js | 2 +- main/lib/http2/node/client.js | 2 +- main/lib/http2/node/server.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/main/lib/http2/browser/browser.js b/main/lib/http2/browser/browser.js index 7cfc30dc..3b802dc7 100644 --- a/main/lib/http2/browser/browser.js +++ b/main/lib/http2/browser/browser.js @@ -34,7 +34,7 @@ export class Http2WebTransportBrowser { args?.sessionFlowControlWindowSizeLimit || 15 * 1024 * 1024 this.initialDatagramSize = - args.initialDatagramSize || this.streamFlowControlWindowSizeLimit - 128 + args.initialDatagramSize || this.initialSessionFlowControlWindow - 128 /** @type {import('../../session.js').HttpClient} */ // @ts-ignore this.jsobj = undefined // the transport will set this diff --git a/main/lib/http2/node/client.js b/main/lib/http2/node/client.js index 52f1fcfb..ec95e808 100644 --- a/main/lib/http2/node/client.js +++ b/main/lib/http2/node/client.js @@ -40,7 +40,7 @@ export class Http2WebTransportClient { args?.sessionFlowControlWindowSizeLimit || 15 * 1024 * 1024 this.initialDatagramSize = - args.initialDatagramSize || this.streamFlowControlWindowSizeLimit - 128 + args.initialDatagramSize || this.initialSessionFlowControlWindow - 128 /** @type {import('../../session.js').HttpClient} */ // @ts-ignore this.jsobj = undefined // the transport will set this diff --git a/main/lib/http2/node/server.js b/main/lib/http2/node/server.js index 7fd9731c..7df6dcbe 100644 --- a/main/lib/http2/node/server.js +++ b/main/lib/http2/node/server.js @@ -43,7 +43,7 @@ export class Http2WebTransportServer { args?.sessionFlowControlWindowSizeLimit || 15 * 1024 * 1024 this.initialDatagramSize = - args.initialDatagramSize || this.streamFlowControlWindowSizeLimit - 128 + args.initialDatagramSize || this.initialSessionFlowControlWindow - 128 /** @type {Record} */ this.paths = {} From 16a39b8ce2ec21e32eb3775d49f43f906ea367a0 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sun, 27 Apr 2025 18:16:48 +0000 Subject: [PATCH 3/9] Add tests for client sending datagrams dropping --- test/datagrams.spec.js | 94 ++++++++++++++++++++++++++++++++++++ test/fixtures/read-stream.js | 7 ++- test/fixtures/server.js | 34 +++++++++++++ 3 files changed, 134 insertions(+), 1 deletion(-) diff --git a/test/datagrams.spec.js b/test/datagrams.spec.js index 59ab11c1..4c43f873 100644 --- a/test/datagrams.spec.js +++ b/test/datagrams.spec.js @@ -3,6 +3,7 @@ import WebTransport from './fixtures/webtransport.js' import { expect } from './fixtures/chai.js' import { readStream } from './fixtures/read-stream.js' +import { writeStream } from './fixtures/write-stream.js' import { readCertHash } from './fixtures/read-cert-hash.js' import { pTimeout } from './fixtures/p-timeout.js' import { quicheLoaded } from './fixtures/quiche.js' @@ -92,6 +93,99 @@ describe('datagrams', function () { expect(result).to.have.property('closeCode', 0) }) + it('client sends datagrams to the server below and over maxDatagramSize', async () => { + client = new WebTransport( + `${process.env.SERVER_URL}/datagrams_client_send_count`, + wtOptions + ) + await client.ready + + let writable + if (client.datagrams.createWritable) { + writable = client.datagrams.createWritable() + } else { + console.log( + 'createWriteable for datagrams unsupported, fallback to old writable' + ) + writable = client.datagrams.writable + } + expect(client.datagrams.maxDatagramSize).to.be.greaterThan(0) + const maxDatagramSize = Math.min( + client.datagrams.maxDatagramSize, + 1_000_000 + ) + + const datagramsOutgoing = Array(10) + .fill( + [ + 200, + 500, + maxDatagramSize * 0.5, + maxDatagramSize * 0.2, + maxDatagramSize, + 2 * maxDatagramSize, + 3 * maxDatagramSize, + 10 * maxDatagramSize, + Math.min(100 * maxDatagramSize, 10_000_000), + 10 + ] + .map((el) => Math.ceil(el)) + .map((el) => new Uint8Array(el)) + ) + .flat() + + const datagramSizesIncom = [] + + const expected = datagramsOutgoing.reduce( + (prevVal, el) => prevVal + el.byteLength, + 0 + ) + + await Promise.all([ + writeStream(writable, datagramsOutgoing), + Promise.any([ + new Promise((resolve) => setTimeout(resolve, 500)), + readStream(client.datagrams.readable, expected, { + outputreportValue: (value) => { + const array = new Uint32Array( + value.buffer, + value.byteOffset, + value.byteLength / Uint32Array.BYTES_PER_ELEMENT + ) + if (array.length > 0) { + datagramSizesIncom.push(array[0]) + } + } + }) + ]) + ]) + + const datagramsBelowLimit = datagramSizesIncom.filter( + (el) => el <= client.datagrams.maxDatagramSize + ).length + const datagramsOverLimit = datagramSizesIncom.filter( + (el) => el > client.datagrams.maxDatagramSize + ).length + + const datagramsBelowLimitOut = datagramsOutgoing + .map((el) => el.byteLength) + .filter((el) => el <= client.datagrams.maxDatagramSize).length + const datagramsOverLimitOut = datagramsOutgoing + .map((el) => el.byteLength) + .filter((el) => el > client.datagrams.maxDatagramSize).length + + expect(datagramsOverLimit).to.be.equal(0, 'Datagrams over limit received') + expect(datagramsBelowLimit).to.be.at.most( + datagramsBelowLimitOut, + 'More datagrams received than send out' + ) + expect(datagramsOverLimitOut).to.be.at.least(1) + expect(datagramsBelowLimitOut).to.be.at.least(1) + expect(datagramsBelowLimit).to.be.at.least( + Math.ceil(0.3 * datagramsBelowLimitOut), + 'We should least receive a third of the datagrams' + ) + }) it('receives datagrams from the server', async () => { // client context - pipes the server's datagrams back to them client = new WebTransport( diff --git a/test/fixtures/read-stream.js b/test/fixtures/read-stream.js index b558b450..9a6ef51c 100644 --- a/test/fixtures/read-stream.js +++ b/test/fixtures/read-stream.js @@ -6,7 +6,11 @@ * @param {number} [expected] * @returns {T[]} */ -export async function readStream(readable, expected, { outputreportCB } = {}) { +export async function readStream( + readable, + expected, + { outputreportCB, outputreportValue } = {} +) { const reader = readable.getReader() try { @@ -24,6 +28,7 @@ export async function readStream(readable, expected, { outputreportCB } = {}) { if (value != null) { outputlength += value.length outputreportCB?.(outputlength) + outputreportValue?.(value) output.push(value) } diff --git a/test/fixtures/server.js b/test/fixtures/server.js index f5a3a0e8..f55881a4 100644 --- a/test/fixtures/server.js +++ b/test/fixtures/server.js @@ -240,6 +240,40 @@ export async function createServer() { } }, + async () => { + for await (const session of getReaderStream( + server.sessionStream('/datagrams_client_send_count') + )) { + // datagram transport is unreliable, at least one message should make it through + const expected = 100 + let received = 0 + + try { + const reader = await session.datagrams.readable.getReader() + const writer = session.datagrams.createWritable().getWriter() + + while (expected > received) { + const { done, value } = await reader.read() + if (done) { + break + } + if (value != null) { + const outarr = new Uint32Array(1) + outarr[0] = value.length + await writer.write(new Uint8Array(outarr.buffer)) + } + } + await writer.close() + await session.closed + } catch (error) { + session.close({ + closeCode: 500, + reason: error.message + }) + } + } + }, + // echo datagrams, initiated by local async () => { for await (const session of getReaderStream( From b41e6013140d47a80402541fcd5172ae9c8fe374 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Mon, 28 Apr 2025 05:43:52 +0000 Subject: [PATCH 4/9] Increase test listening time --- test/datagrams.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/datagrams.spec.js b/test/datagrams.spec.js index 4c43f873..511d9125 100644 --- a/test/datagrams.spec.js +++ b/test/datagrams.spec.js @@ -144,7 +144,7 @@ describe('datagrams', function () { await Promise.all([ writeStream(writable, datagramsOutgoing), Promise.any([ - new Promise((resolve) => setTimeout(resolve, 500)), + new Promise((resolve) => setTimeout(resolve, 1000)), readStream(client.datagrams.readable, expected, { outputreportValue: (value) => { const array = new Uint32Array( From f392e871cfae2283a1c3845c368ecc83d4822c31 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Mon, 28 Apr 2025 06:18:16 +0000 Subject: [PATCH 5/9] We need to wait before max_datagram capsule arrives --- test/datagrams.spec.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/datagrams.spec.js b/test/datagrams.spec.js index 511d9125..896b3835 100644 --- a/test/datagrams.spec.js +++ b/test/datagrams.spec.js @@ -19,6 +19,9 @@ describe('datagrams', function () { let forceReliable = false if (process.env.USE_HTTP2 === 'true') forceReliable = true + const waitForSettings = + process.env.USE_POLYFILL === 'true' || process.env.USE_PONYFILL === 'true' + const wtOptions = { serverCertificateHashes: [ { @@ -109,6 +112,8 @@ describe('datagrams', function () { ) writable = client.datagrams.writable } + if (waitForSettings) await new Promise((resolve) => setTimeout(resolve, 50)) // we have to wait before initial settings arrive + expect(client.datagrams.maxDatagramSize).to.be.lessThan(1000_000_000) expect(client.datagrams.maxDatagramSize).to.be.greaterThan(0) const maxDatagramSize = Math.min( client.datagrams.maxDatagramSize, From 35d61f076f0d9d2638e1fbb5cc2e57c70b22f567 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Thu, 1 May 2025 13:24:49 +0000 Subject: [PATCH 6/9] Minor fixes --- main/lib/http2/browser/browserparser.js | 2 +- main/lib/http2/node/capsuleparser.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/main/lib/http2/browser/browserparser.js b/main/lib/http2/browser/browserparser.js index 6dd396d2..b4394d8a 100644 --- a/main/lib/http2/browser/browserparser.js +++ b/main/lib/http2/browser/browserparser.js @@ -247,7 +247,7 @@ export class BrowserParser extends ParserBase { case ParserBase.DATAGRAM: if (offsetend - bufferstate.offset <= this.maxDatagramSize) { // actually for the browser it is already too late - // but to give developers a consitent behaviour, we drop it here as well + // but to give developers a consistent behaviour, we drop it here as well // drop too large datagrams this.session.jsobj.onDatagramReceived({ datagram: new Uint8Array( diff --git a/main/lib/http2/node/capsuleparser.js b/main/lib/http2/node/capsuleparser.js index 849b0228..547f03d8 100644 --- a/main/lib/http2/node/capsuleparser.js +++ b/main/lib/http2/node/capsuleparser.js @@ -328,7 +328,7 @@ export class Http2CapsuleParser extends ParserBaseHttp2 { writeVarInt(bufferstate, type) writeVarInt(bufferstate, length) for (const ind in headerVints) writeVarInt(bufferstate, headerVints[ind]) - if (!this.stream) return + if (!this.stream) return true // note it might be illegal to split the write if (!end) { let blocked = !this.stream.write(cdata) From ac5fe33c1d3224393526e7bd33aeb20681922124 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Thu, 1 May 2025 15:31:05 +0000 Subject: [PATCH 7/9] Add maxdatagram server send test --- test/datagrams.spec.js | 91 ++++++++++++++++++++++++++++++++++++++++- test/fixtures/server.js | 45 ++++++++++++++++++++ 2 files changed, 135 insertions(+), 1 deletion(-) diff --git a/test/datagrams.spec.js b/test/datagrams.spec.js index 896b3835..76501e51 100644 --- a/test/datagrams.spec.js +++ b/test/datagrams.spec.js @@ -185,10 +185,11 @@ describe('datagrams', function () { 'More datagrams received than send out' ) expect(datagramsOverLimitOut).to.be.at.least(1) + expect(datagramsBelowLimit).to.be.at.least(1) expect(datagramsBelowLimitOut).to.be.at.least(1) expect(datagramsBelowLimit).to.be.at.least( Math.ceil(0.3 * datagramsBelowLimitOut), - 'We should least receive a third of the datagrams' + 'We should least receive a third of the datagrams' ) }) it('receives datagrams from the server', async () => { @@ -208,4 +209,92 @@ describe('datagrams', function () { ) expect(received).to.have.lengthOf(expected) }) + it('server sends datagrams to the client below and over maxDatagramSize', async () => { + client = new WebTransport( + `${process.env.SERVER_URL}/datagrams_server_send_count`, + wtOptions + ) + await client.ready + + let writable + if (client.datagrams.createWritable) { + writable = client.datagrams.createWritable() + } else { + console.log( + 'createWriteable for datagrams unsupported, fallback to old writable' + ) + writable = client.datagrams.writable + } + if (waitForSettings) await new Promise((resolve) => setTimeout(resolve, 50)) // we have to wait before initial settings arrive + expect(client.datagrams.maxDatagramSize).to.be.lessThan(1000_000_000) + expect(client.datagrams.maxDatagramSize).to.be.greaterThan(0) + + const datagramsOutgoingPlan = Array(10) + .fill([ + { bytesize: 200 }, + { bytesize: 500 }, + { dasize: 0.5 }, + { dasize: 0.2 }, + { dasize: 1.0 }, + { dasize: 2 }, + { dasize: 3 }, + { dasize: 10 }, + { dasize: 100 }, + { bytesize: 10 } + ]) + .flat() + + const datagramsOutgoing = datagramsOutgoingPlan.map((el) => { + const uint32arr = new Uint32Array(2) + uint32arr[0] = el.bytesize + uint32arr[1] = el.dasize + return new Uint8Array(uint32arr.buffer) + }) + + let datagramsBelowLimit = 0 + let datagramsOverLimit = 0 + + await Promise.all([ + writeStream(writable, datagramsOutgoing), + Promise.any([ + new Promise((resolve) => setTimeout(resolve, 1000)), + readStream(client.datagrams.readable, 1000_000_000, { + outputreportValue: (value) => { + const array = new Uint32Array( + value.buffer, + value.byteOffset, + value.byteLength / Uint32Array.BYTES_PER_ELEMENT + ) + if (array.length > 0) { + const mDatagramSize = array[0] + const sendBytelength = array[1] + expect(sendBytelength).to.be.equal(value.byteLength) + if (value.byteLength > mDatagramSize) datagramsOverLimit++ + else datagramsBelowLimit++ + } + } + }) + ]) + ]) + + let datagramsBelowLimitOut = 0 + let datagramsOverLimitOut = 0 + datagramsOutgoingPlan.forEach((el) => { + if (el.dasize > 1) datagramsOverLimitOut++ + else datagramsBelowLimitOut++ + }) + + expect(datagramsOverLimit).to.be.equal(0, 'Datagrams over limit received') + expect(datagramsBelowLimit).to.be.at.most( + datagramsBelowLimitOut, + 'More datagrams received than send out' + ) + expect(datagramsBelowLimit).to.be.at.least(1) + expect(datagramsOverLimitOut).to.be.at.least(1) + expect(datagramsBelowLimitOut).to.be.at.least(1) + expect(datagramsBelowLimit).to.be.at.least( + Math.ceil(0.3 * datagramsBelowLimitOut), + 'We should least receive a third of the datagrams' + ) + }) }) diff --git a/test/fixtures/server.js b/test/fixtures/server.js index f55881a4..1d1fcb40 100644 --- a/test/fixtures/server.js +++ b/test/fixtures/server.js @@ -301,6 +301,51 @@ export async function createServer() { } }, + // echo send datagrams of different sizes, initiated by local + async () => { + for await (const session of getReaderStream( + server.sessionStream('/datagrams_server_send_count') + )) { + const expected = 100 + let received = 0 + + try { + const reader = await session.datagrams.readable.getReader() + const writer = session.datagrams.createWritable().getWriter() + + while (expected > received) { + const { done, value } = await reader.read() + if (done) { + break + } + if (value != null) { + const mDatagramSize = session.datagrams.maxDatagramSize + const tosend = new Uint32Array(value.buffer) + const outarr = new Uint32Array( + (tosend[0] + + Math.min( + Math.ceil(tosend[1] * mDatagramSize), + 10_000_000 + )) / + Uint32Array.BYTES_PER_ELEMENT + ) + outarr[0] = mDatagramSize + outarr[1] = outarr.byteLength + await writer.write(new Uint8Array(outarr.buffer)) + } + } + await writer.close() + await session.closed + } catch (error) { + session.close({ + closeCode: 500, + reason: error.message + }) + await session.closed + } + } + }, + // receive 100+ bidi streams and block async () => { for await (const session of getReaderStream( From bb539b11a7af929b4a50401ace2f0ca7334e9f1f Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Thu, 1 May 2025 16:04:45 +0000 Subject: [PATCH 8/9] Fix test server sending and maxDatagram --- test/datagrams.spec.js | 2 +- test/fixtures/server.js | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/test/datagrams.spec.js b/test/datagrams.spec.js index 76501e51..2f249079 100644 --- a/test/datagrams.spec.js +++ b/test/datagrams.spec.js @@ -247,7 +247,7 @@ describe('datagrams', function () { const datagramsOutgoing = datagramsOutgoingPlan.map((el) => { const uint32arr = new Uint32Array(2) uint32arr[0] = el.bytesize - uint32arr[1] = el.dasize + uint32arr[1] = Math.ceil(el.dasize * 1000) return new Uint8Array(uint32arr.buffer) }) diff --git a/test/fixtures/server.js b/test/fixtures/server.js index 1d1fcb40..fd27258a 100644 --- a/test/fixtures/server.js +++ b/test/fixtures/server.js @@ -321,16 +321,17 @@ export async function createServer() { if (value != null) { const mDatagramSize = session.datagrams.maxDatagramSize const tosend = new Uint32Array(value.buffer) - const outarr = new Uint32Array( - (tosend[0] + + const outarr = new Uint8Array( + tosend[0] + Math.min( Math.ceil(tosend[1] * mDatagramSize), 10_000_000 - )) / - Uint32Array.BYTES_PER_ELEMENT + ) / + 1000 ) - outarr[0] = mDatagramSize - outarr[1] = outarr.byteLength + const outarr32 = new Uint32Array(outarr.buffer, 0, 2) + outarr32[0] = mDatagramSize + outarr32[1] = outarr.byteLength await writer.write(new Uint8Array(outarr.buffer)) } } From 0cdb204313599c18943f1d3487ffeebfb2e406e4 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Thu, 1 May 2025 16:17:30 +0000 Subject: [PATCH 9/9] Fix datagram server size limit in test --- test/fixtures/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/fixtures/server.js b/test/fixtures/server.js index fd27258a..1dcf407f 100644 --- a/test/fixtures/server.js +++ b/test/fixtures/server.js @@ -325,7 +325,7 @@ export async function createServer() { tosend[0] + Math.min( Math.ceil(tosend[1] * mDatagramSize), - 10_000_000 + 10_000_000_000 ) / 1000 )