UNPKG

@fails-components/webtransport

Version:

A component to add webtransport support (server and client) to node.js using libquiche

339 lines (307 loc) 9.42 kB
import { Http2WebTransportStream } from './stream.js' import { logger } from '../utils.js' import { PriorityScheduler } from './priorityscheduler.js' const pid = typeof process !== 'undefined' ? process.pid : 0 const log = logger(`webtransport:parserbase(${pid})`) /** * @param{Number|bigint} int * @returns {Number} */ export function lengthVarInt(int) { if (BigInt(int) < 64n) return 1 if (BigInt(int) < 16384n) return 2 if (BigInt(int) < 1073741824n) return 4 /* if (BigInt(int) < 4611686018427387904 ) */ return 8 } export class ParserBase { static PADDING = 0x190b4d38 static WT_RESET_STREAM = 0x190b4d39 static WT_STOP_SENDING = 0x190b4d3a static WT_STREAM_WOFIN = 0x190b4d3b static WT_STREAM_WFIN = 0x190b4d3c static WT_MAX_DATA = 0x190b4d3d static WT_MAX_STREAM_DATA = 0x190b4d3e static WT_MAX_STREAMS_BIDI = 0x190b4d3f static WT_MAX_STREAMS_UNIDI = 0x190b4d40 static WT_DATA_BLOCKED = 0x190b4d41 static WT_STREAM_DATA_BLOCKED = 0x190b4d42 static WT_STREAMS_BLOCKED_UNIDI = 0x190b4d43 static WT_STREAMS_BLOCKED_BIDI = 0x190b4d44 static WT_CLOSE_SESSION = 0x2843 static WT_DRAIN_SESSION = 0x78ae static DATAGRAM = 0x00 /** * @param {import('../types').ParserInit} arg */ constructor({ nativesession, isclient, initialStreamSendWindowOffsetBidi, initialStreamSendWindowOffsetUnidi, initialStreamReceiveWindowOffset, streamShouldAutoTuneReceiveWindow, streamReceiveWindowSizeLimit }) { this.session = nativesession this.isclient = isclient /** @type {boolean} */ this.blocked = false this.initialStreamSendWindowOffsetUnidi = initialStreamSendWindowOffsetUnidi this.initialStreamSendWindowOffsetBidi = initialStreamSendWindowOffsetBidi this.initialStreamReceiveWindowOffset = initialStreamReceiveWindowOffset this.streamShouldAutoTuneReceiveWindow = streamShouldAutoTuneReceiveWindow this.streamReceiveWindowSizeLimit = streamReceiveWindowSizeLimit /** @type {Map<bigint,Http2WebTransportStream>} */ this.wtstreams = new Map() this.scheduler = new PriorityScheduler() } /** * @abstract * @param {Buffer|Uint8Array} data */ // eslint-disable-next-line no-unused-vars parseData(data) { throw new Error('Implement parseData in derived Class') } /** * @abstract * @param{{type: Number, headerVints: Array<Number|bigint>, payload: Uint8Array|undefined, end?: () => void}} bs */ // eslint-disable-next-line no-unused-vars writeCapsule({ type, headerVints, payload, end }) { throw new Error('Implement writeCapsule in derived Class') } /** * @abstract * @return{boolean} */ initialParametersMandatory() { throw new Error('Implement initialParametersMandatory in derived Class') } /** * @param{{code: Number, reason: string}}arg */ sendClose({ code, reason }) { const encoder = new TextEncoder() const payload = encoder.encode('AAAA' + reason) payload[0] = (code >> 24) & 0xff payload[1] = (code >> 16) & 0xff payload[2] = (code >> 8) & 0xff payload[3] = code & 0xff this.writeCapsule({ type: ParserBase.WT_CLOSE_SESSION, headerVints: [], payload, end: () => { this.closeHttp2Stream(code) } }) } /** * @param {bigint} streamid * @param {{sendOrder: bigint,sendGroupId: bigint}} priority */ newStream(streamid, priority) { const incoming = this.isclient ? !(streamid & 0x1n) : !!(streamid & 0x1n) const streamIdManager = streamid & 0x2n ? this.session.streamIdMngrUni : this.session.streamIdMngrBi if (incoming) { // only check incoming streams const res = streamIdManager.maybeIncreaseLargestPeerStreamId(streamid) if (res.error) { // ok someone overstayed its welcome this.session.closeConnection({ code: 20, // QUIC_STREAM_STREAM_CREATION_ERROR , // probably the right one... reason: res.error }) return undefined } } const unidirectional = !!(streamid & 0x2n) const stream = new Http2WebTransportStream({ streamid, unidirectional, incoming, capsuleParser: this, sendWindowOffset: unidirectional ? this.initialStreamSendWindowOffsetUnidi : this.initialStreamSendWindowOffsetBidi, receiveWindowOffset: this.initialStreamReceiveWindowOffset, shouldAutoTuneReceiveWindow: this.streamShouldAutoTuneReceiveWindow, receiveWindowSizeLimit: this.streamReceiveWindowSizeLimit, sessionFlowController: this.session.flowController, streamIdManager }) this.wtstreams.set(streamid, stream) this.scheduler.Register(streamid, priority) this.session.jsobj.onStream({ bidirectional: !(streamid & 0x2n), incoming, stream, sendGroupId: priority.sendGroupId, sendOrder: priority.sendOrder }) return stream } scheduleDrainWrites() { if (this._scheduledDrainWriteCall) return const prom = Promise.resolve() this._scheduledDrainWriteCall = prom prom .then(() => { delete this._scheduledDrainWriteCall this.drainWrites() }) .catch((error) => log('Error in drainWrites', error)) } drainWrites() { if (!this.blocked) this.session.drainWrites() while (!this.blocked) { const frontId = this.scheduler.PopFront() if (typeof frontId === 'undefined') break const stream = this.wtstreams.get(frontId) if (!stream) break stream.drainWrites() } } /** * @param {bigint|undefined} val */ onMaxData(val) { if (val && this.session.flowController.updateSendWindowOffset(val)) { let pending = false this.wtstreams.forEach((stream, streamid) => { if (stream.hasPendingData()) { pending = true this.scheduleDrainWriteStream(streamid) } }) if (pending) this.drainWrites() } } /** * @param {bigint} streamid * @param {bigint} offset */ onMaxStreamData(streamid, offset) { const object = this.wtstreams.get(streamid) if (object && offset) { if (object.flowController.updateSendWindowOffset(offset)) { this.scheduleDrainWriteStream(streamid) this.drainWrites() } } } /** * @param {bigint|undefined} maxOpenStreams */ onMaxStreamUniDi(maxOpenStreams) { if (typeof maxOpenStreams === 'undefined') return this.session.streamIdMngrUni.maybeAllowNewOutgoingStreams(maxOpenStreams) this.session.trySendingUnidirectionalStreams() } /** * @param {bigint|undefined} maxOpenStreams */ onMaxStreamBiDi(maxOpenStreams) { if (typeof maxOpenStreams === 'undefined') return this.session.streamIdMngrBi.maybeAllowNewOutgoingStreams(maxOpenStreams) this.session.trySendingBidirectionalStreams() } /** * @param {bigint|undefined} val */ onDataBlocked(val) { log('Session received blocked frame ' + val) // this.session.flowController.reportBlocked(val) } /** * @param {bigint} streamid * @param {bigint} offset */ onStreamDataBlocked(streamid, offset) { log('Stream ' + streamid + ' received blocked frame ' + offset) // const object = this.wtstreams.get(streamid) // if (object && offset) object.flowController.reportBlocked(offset) } /** * @param {bigint|undefined} maxstreams */ onStreamsBlockedBidi(maxstreams) { if (typeof maxstreams === 'undefined') return const ret = this.session.streamIdMngrBi.onStreamsBlockedFrame(maxstreams) if (ret.error) { this.session.closeConnection({ code: 105, // QUIC_STREAMS_BLOCKED_DATA , // probably the right one... reason: ret.error }) } } /** * @param {bigint|undefined} maxstreams */ onStreamsBlockedUnidi(maxstreams) { if (typeof maxstreams === 'undefined') return const ret = this.session.streamIdMngrUni.onStreamsBlockedFrame(maxstreams) if (ret.error) { this.session.closeConnection({ code: 105, // QUIC_STREAMS_BLOCKED_DATA , // probably the right one... reason: ret.error }) } } /** * @param {{code: number, reason: string}} opts */ onCloseWebTransportSession({ code, reason }) { this.session.jsobj.onClose({ errorcode: code, error: reason }) this.closeHttp2Stream(code) // is this necessary } onDrain() { this.session.jsobj.onGoAwayReceived() } /** * * @param {bigint} streamid */ shouldYieldStream(streamid) { return this.scheduler.ShouldYield(streamid) } /** * * @param {bigint} streamid */ scheduleDrainWriteStream(streamid) { this.scheduler.Schedule(streamid) } /** * * @param {bigint} streamid */ removeStream(streamid) { this.scheduler.Unregister(streamid) //this.wtstreams.delete(streamid) // why does this create troubles } /** * * @param {bigint} streamid * @param {{sendOrder: bigint, sendGroupId: bigint}} arg2 */ streamUpdateSendOrderAndGroup(streamid, { sendOrder, sendGroupId }) { this.scheduler.UpdateSendGroup(streamid, sendGroupId) this.scheduler.UpdateSendOrder(streamid, sendOrder) } /** * @param {number} code */ // eslint-disable-next-line no-unused-vars closeHttp2Stream(code) { throw new Error('Implement closeHttp2Stream in derived Class') } }