diff --git a/main/lib/http2/browser/browser.js b/main/lib/http2/browser/browser.js index f26fb056..3b802dc7 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.initialSessionFlowControlWindow - 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..b4394d8a 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 consistent 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..547f03d8 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 @@ -317,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) diff --git a/main/lib/http2/node/client.js b/main/lib/http2/node/client.js index 7cccf510..ec95e808 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.initialSessionFlowControlWindow - 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..7df6dcbe 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.initialSessionFlowControlWindow - 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 { diff --git a/test/datagrams.spec.js b/test/datagrams.spec.js index 59ab11c1..2f249079 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' @@ -18,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: [ { @@ -92,6 +96,102 @@ 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 + } + 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, + 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, 1000)), + 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(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' + ) + }) it('receives datagrams from the server', async () => { // client context - pipes the server's datagrams back to them client = new WebTransport( @@ -109,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] = Math.ceil(el.dasize * 1000) + 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/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..1dcf407f 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( @@ -267,6 +301,52 @@ 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 Uint8Array( + tosend[0] + + Math.min( + Math.ceil(tosend[1] * mDatagramSize), + 10_000_000_000 + ) / + 1000 + ) + const outarr32 = new Uint32Array(outarr.buffer, 0, 2) + outarr32[0] = mDatagramSize + outarr32[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(