@fails-components/webtransport
Version:
A component to add webtransport support (server and client) to node.js using libquiche
347 lines (339 loc) • 12.5 kB
JavaScript
import {
ParserBaseHttp2,
readVarInt,
readUint32,
writeVarInt
} from '../parserbasehttp2.js'
import { lengthVarInt } from '../parserbase.js'
export class Http2CapsuleParser extends ParserBaseHttp2 {
/**
* @param {import('../../types.js').ParserHttp2Init} stream
*/
constructor({
stream,
nativesession,
isclient,
initialStreamSendWindowOffsetBidi,
initialStreamSendWindowOffsetUnidi,
initialStreamReceiveWindowOffset,
streamShouldAutoTuneReceiveWindow,
streamReceiveWindowSizeLimit
}) {
super({
stream,
nativesession,
isclient,
initialStreamSendWindowOffsetBidi,
initialStreamSendWindowOffsetUnidi,
initialStreamReceiveWindowOffset,
streamShouldAutoTuneReceiveWindow,
streamReceiveWindowSizeLimit
})
this.mode = 's' // capsule start
/** @type {Buffer|undefined} */
this.saveddata = undefined
/** @type {Number|undefined} */
this.rtype = undefined
}
/**
* @param {Buffer} data
*/
parseData(data) {
let cdata = data
if (this.saveddata) {
cdata = Buffer.concat([this.saveddata, cdata])
delete this.saveddata
}
const bufferstate = { offset: 0, size: cdata.length, buffer: cdata }
while (bufferstate.size - bufferstate.offset > 0) {
switch (this.mode) {
case 's':
{
const capsulestart =
bufferstate.offset + bufferstate.buffer.byteOffset
const capsulemaxlength =
bufferstate.buffer.byteLength - bufferstate.offset
// we are at capsule start
if (bufferstate.size < 2 + bufferstate.offset) {
this.saveddata = Buffer.from(
bufferstate.buffer.buffer,
capsulestart,
capsulemaxlength
)
return
}
const rtype = readVarInt(bufferstate)
if (
typeof rtype === 'undefined' ||
bufferstate.size < 1 + bufferstate.offset
) {
this.saveddata = Buffer.from(
bufferstate.buffer.buffer,
capsulestart,
capsulemaxlength
)
return
}
const type = Number(rtype)
const rlength = readVarInt(bufferstate)
if (typeof rlength === 'undefined') {
this.saveddata = Buffer.from(
bufferstate.buffer.buffer,
capsulestart,
capsulemaxlength
)
return
}
const length = Number(rlength)
let checklength = length // we want to read most times the full capsule
const offsetend = Math.min(
bufferstate.offset + length,
bufferstate.size
)
const offsetbegin = bufferstate.offset
if (
type === Http2CapsuleParser.PADDING ||
type === Http2CapsuleParser.WT_STREAM_WOFIN ||
type ===
Http2CapsuleParser.WT_STREAM_WFIN /* || type === Http2CapsuleParser.DATAGRAM */
) {
checklength = Math.min(length, 64) // stream id + some Data
}
if (
checklength >
4 * Number(this.session.flowController.receiveWindowSize) ||
length > 8 * Number(this.session.flowController.receiveWindowSize)
) {
// 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
})
return
}
if (bufferstate.size < checklength + bufferstate.offset) {
this.saveddata = Buffer.from(
bufferstate.buffer.buffer,
capsulestart,
capsulemaxlength
)
return
}
let streamid
switch (type) {
case Http2CapsuleParser.PADDING:
// only padding do nothing
break
case Http2CapsuleParser.WT_RESET_STREAM:
case Http2CapsuleParser.WT_STOP_SENDING:
{
const streamid = readVarInt(bufferstate)
if (typeof streamid !== 'undefined') {
const stream = this.wtstreams.get(streamid)
const code = readVarInt(bufferstate)
if (stream && typeof code !== 'undefined') {
stream.onStreamSignal(
type === Http2CapsuleParser.WT_RESET_STREAM
? 'resetStream'
: 'stopSending'
)
stream.jsobj.onStreamRecvSignal({
code: Number(code),
nettask:
type === Http2CapsuleParser.WT_RESET_STREAM
? 'resetStream'
: 'stopSending'
})
}
}
}
break
case Http2CapsuleParser.WT_STREAM_WOFIN:
case Http2CapsuleParser.WT_STREAM_WFIN:
streamid = readVarInt(bufferstate)
if (typeof streamid !== 'undefined') {
let object = this.wtstreams.get(streamid)
if (!object) {
object = this.newStream(streamid, {
sendOrder: 0,
sendGroupId: 0n
})
if (!object) return // stream broken
}
// TODO submit data
if (offsetend - bufferstate.offset >= 0) {
const fin =
type === Http2CapsuleParser.WT_STREAM_WFIN &&
bufferstate.size >= length + offsetbegin
if (fin) object.onFin()
object.recvData({
data:
offsetend - bufferstate.offset > 0
? new Uint8Array(
bufferstate.buffer.buffer,
bufferstate.buffer.byteOffset +
bufferstate.offset,
offsetend - bufferstate.offset
)
: undefined,
fin
})
}
}
break
case Http2CapsuleParser.WT_MAX_DATA:
this.onMaxData(readVarInt(bufferstate))
break
case Http2CapsuleParser.WT_MAX_STREAM_DATA:
{
const streamid = readVarInt(bufferstate)
const offset = readVarInt(bufferstate)
if (
typeof streamid !== 'undefined' &&
typeof offset !== 'undefined'
)
this.onMaxStreamData(streamid, offset)
}
break
case Http2CapsuleParser.WT_MAX_STREAMS_BIDI:
this.onMaxStreamBiDi(readVarInt(bufferstate))
break
case Http2CapsuleParser.WT_MAX_STREAMS_UNIDI:
this.onMaxStreamUniDi(readVarInt(bufferstate))
break
case Http2CapsuleParser.WT_DATA_BLOCKED:
this.onDataBlocked(readVarInt(bufferstate))
break
case Http2CapsuleParser.WT_STREAM_DATA_BLOCKED:
{
const streamid = readVarInt(bufferstate)
const offset = readVarInt(bufferstate)
if (
typeof streamid !== 'undefined' &&
typeof offset !== 'undefined'
)
this.onStreamDataBlocked(streamid, offset)
}
break
case Http2CapsuleParser.WT_STREAMS_BLOCKED_UNIDI:
this.onStreamsBlockedUnidi(readVarInt(bufferstate))
break
case Http2CapsuleParser.WT_STREAMS_BLOCKED_BIDI:
this.onStreamsBlockedBidi(readVarInt(bufferstate))
break
case Http2CapsuleParser.WT_CLOSE_SESSION:
{
const code = readUint32(bufferstate) || 0
const decoder = new TextDecoder()
const reason = decoder.decode(
new Uint8Array(
bufferstate.buffer.buffer,
bufferstate.buffer.byteOffset + bufferstate.offset,
offsetend - bufferstate.offset
)
)
this.onCloseWebTransportSession({ code, reason })
}
break
case Http2CapsuleParser.WT_DRAIN_SESSION:
this.onDrain()
break
case Http2CapsuleParser.DATAGRAM:
this.session.jsobj.onDatagramReceived({
datagram: new Uint8Array(
bufferstate.buffer.buffer,
bufferstate.buffer.byteOffset + bufferstate.offset,
offsetend - bufferstate.offset
)
})
break
default:
// do nothing
}
if (bufferstate.size < length + offsetbegin) {
this.remainlength = length + offsetbegin - bufferstate.size
this.mode = 'c'
if (streamid) {
this.rstreamid = streamid
this.rfin = type === Http2CapsuleParser.WT_STREAM_WFIN
}
}
bufferstate.offset = offsetend
}
break
case 'c':
{
const clength = Math.min(
bufferstate.size - bufferstate.offset,
this.remainlength
)
if (this.rstreamid) {
// TODO submitData
const object = this.wtstreams.get(this.rstreamid)
const fin =
this.rtype === Http2CapsuleParser.WT_STREAM_WFIN &&
this.remainlength === clength
// TODO submit data
if (object) {
if (fin) object.onFin()
object.recvData({
data: new Uint8Array(
bufferstate.buffer.buffer,
bufferstate.buffer.byteOffset + bufferstate.offset,
clength
),
fin
})
}
}
this.remainlength = this.remainlength - clength
bufferstate.offset += clength
if (this.remainlength === 0) {
this.mode = 's'
delete this.rfin
delete this.rstreamid
}
}
break
}
}
}
/**
* @param{{type: Number, headerVints: Array<Number>, payload: Uint8Array|undefined, end?: () => void}} bs
*/
writeCapsule({ type, headerVints, payload, end }) {
let length = 0
for (const ind in headerVints) length += lengthVarInt(headerVints[ind])
let headlength = length
if (payload) length += payload.byteLength
headlength += lengthVarInt(length) + lengthVarInt(type)
const cdata = Buffer.alloc(headlength)
const bufferstate = { offset: 0, size: cdata.length, buffer: cdata }
writeVarInt(bufferstate, type)
writeVarInt(bufferstate, length)
for (const ind in headerVints) writeVarInt(bufferstate, headerVints[ind])
if (!this.stream) return
// note it might be illegal to split the write
if (!end) {
let blocked = !this.stream.write(cdata)
if (payload) blocked = !this.stream.write(payload) || blocked
// do something if blocked
if (blocked) this.blocked = true
return blocked
} else {
if (!payload) this.stream.end(cdata, end)
else this.stream.write(cdata)
if (payload) this.stream.end(payload, end)
}
}
/**
* @param {number} code
*/
closeHttp2Stream(code) {
this.stream.close(code)
this.stream.destroy()
}
initialParametersMandatory() {
return false
}
}