@fails-components/webtransport
Version:
A component to add webtransport support (server and client) to node.js using libquiche
295 lines (274 loc) • 8.49 kB
JavaScript
import { ParserBase, lengthVarInt } from '../parserbase.js'
import { logger } from '../../utils.js'
const log = logger(`webtransport:http2:browserparser`)
/**
* @param{{offset: Number, buffer: Uint8Array, size: Number}} bs
*/
function readVarInt(bs) {
if (bs.offset + 1 > bs.size) return undefined
let val = BigInt(bs.buffer[bs.offset])
bs.offset++
const prefix = Number(val) >>> 6
const intlength = 1 << prefix
if (bs.offset + intlength - 1 > bs.size) {
return undefined
}
val = val & 0x3fn
for (let i = 0; i < intlength - 1; i++) {
val = (val << 8n) | BigInt(bs.buffer[bs.offset])
bs.offset++
}
return val
}
/**
* @param{{offset: Number, buffer: Uint8Array, size: Number}} bs
*/
function readUint32(bs) {
if (bs.offset + 4 > bs.size) return undefined
let val = bs.buffer[bs.offset]
bs.offset++
val = (val << 8) | bs.buffer[bs.offset]
bs.offset++
val = (val << 8) | bs.buffer[bs.offset]
bs.offset++
val = (val << 8) | bs.buffer[bs.offset]
bs.offset++
return val
}
/**
* @param{{offset: Number, buffer: Uint8Array, size: Number}} bs
* @param{Number|bigint} int
*/
export function writeVarInt(bs, int) {
let numbytes = 8n
let msb = 0xc0n
const bint = BigInt(int)
if (bint < 64n) {
numbytes = 1n
msb = 0x0n
} else if (bint < 16384n) {
numbytes = 2n
msb = 0x40n
} else if (bint < 1073741824n) {
numbytes = 4n
msb = 0x80n
}
bs.buffer[bs.offset] = Number(
msb | ((bint >> ((numbytes - 1n) * 8n)) & 0xffn)
)
bs.offset++
for (let i = numbytes - 2n; i >= 0n; i--) {
bs.buffer[bs.offset] = Number((bint >> (i * 8n)) & 0xffn)
bs.offset++
}
}
export class BrowserParser extends ParserBase {
static WS_CONTINUE = 0x0
static WS_TEXT = 0x1
static WS_BINARY = 0x2
static WS_CLOSE = 0x8
static WS_PING = 0x9
static WS_PONG = 0xa
/**
* @param {import('../../types.js').ParserWebsocketInit} stream
*/
constructor({
ws,
nativesession,
isclient,
initialStreamSendWindowOffsetUnidi,
initialStreamSendWindowOffsetBidi,
initialStreamReceiveWindowOffset,
streamShouldAutoTuneReceiveWindow,
streamReceiveWindowSizeLimit
}) {
super({
nativesession,
isclient,
initialStreamSendWindowOffsetUnidi,
initialStreamSendWindowOffsetBidi,
initialStreamReceiveWindowOffset,
streamShouldAutoTuneReceiveWindow,
streamReceiveWindowSizeLimit
})
this.ws = ws
/** @type {Buffer|undefined} */
this.saveddata = undefined
/** @type {Number|undefined} */
this.rtype = undefined
this.closesend = false
this.ws.addEventListener('message', (event) => {
if (event.data instanceof ArrayBuffer) {
// binary frame
this.parseData(new Uint8Array(event.data, 0, event.data.byteLength))
} else {
// text frame
log('Illegal text frame', event.data)
}
})
}
/**
* @param {Uint8Array} data
*/
parseData(data) {
const bufferstate = { offset: 0, size: data.byteLength, buffer: data }
const offsetend = bufferstate.size
const rtype = readVarInt(bufferstate)
if (typeof rtype === 'undefined') return
const type = Number(rtype)
switch (type) {
case ParserBase.PADDING:
// only padding do nothing
break
case ParserBase.WT_RESET_STREAM:
case ParserBase.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 === ParserBase.WT_RESET_STREAM
? 'resetStream'
: 'stopSending'
)
stream.jsobj.onStreamRecvSignal({
code: Number(code),
nettask:
type === ParserBase.WT_RESET_STREAM
? 'resetStream'
: 'stopSending'
})
}
}
}
break
case ParserBase.WT_STREAM_WOFIN:
case ParserBase.WT_STREAM_WFIN:
{
const streamid = readVarInt(bufferstate)
if (typeof streamid !== 'undefined') {
let object = this.wtstreams.get(streamid)
if (!object) {
object = this.newStream(streamid, {
sendOrder: 0n,
sendGroupId: 0n
})
if (!object) return // stream broken
}
// TODO submit data
if (offsetend - bufferstate.offset >= 0) {
const fin = type === ParserBase.WT_STREAM_WFIN
if (fin) object.onFin()
object.recvData({
data: new Uint8Array(
bufferstate.buffer.buffer,
bufferstate.buffer.byteOffset + bufferstate.offset,
offsetend - bufferstate.offset
),
fin
})
}
}
}
break
case ParserBase.WT_MAX_DATA:
this.onMaxData(readVarInt(bufferstate))
break
case ParserBase.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 ParserBase.WT_MAX_STREAMS_BIDI:
this.onMaxStreamBiDi(readVarInt(bufferstate))
break
case ParserBase.WT_MAX_STREAMS_UNIDI:
this.onMaxStreamUniDi(readVarInt(bufferstate))
break
case ParserBase.WT_DATA_BLOCKED:
this.onDataBlocked(readVarInt(bufferstate))
break
case ParserBase.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 ParserBase.WT_STREAMS_BLOCKED_UNIDI:
this.onStreamsBlockedUnidi(readVarInt(bufferstate))
break
case ParserBase.WT_STREAMS_BLOCKED_BIDI:
this.onStreamsBlockedBidi(readVarInt(bufferstate))
break
case ParserBase.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 ParserBase.WT_DRAIN_SESSION:
this.onDrain()
break
case ParserBase.DATAGRAM:
this.session.jsobj.onDatagramReceived({
datagram: new Uint8Array(
bufferstate.buffer.buffer,
bufferstate.buffer.byteOffset + bufferstate.offset,
offsetend - bufferstate.offset
)
})
break
default:
// do nothing
}
bufferstate.offset = offsetend
}
/**
* @param{{type: Number, headerVints: Array<Number|bigint>, payload: Uint8Array|undefined, end?: () => void}} bs
*/
writeCapsule({ type, headerVints, payload, end }) {
let plength = 0
for (const ind in headerVints) plength += lengthVarInt(headerVints[ind])
plength += lengthVarInt(type)
const hlength = plength
if (payload) plength += payload.byteLength
const cdata = new Uint8Array(plength)
const bufferstate = { offset: 0, size: cdata.length, buffer: cdata }
writeVarInt(bufferstate, type)
for (const ind in headerVints) writeVarInt(bufferstate, headerVints[ind])
const dest = new Uint8Array(cdata.buffer, cdata.byteOffset + hlength)
if (payload) dest.set(payload)
this.ws.send(cdata)
if (end) end()
/* const blocked = this.ws.bufferedAmount > 1024 * 256
// do something if blocked
if (blocked) this.blocked = true
return blocked */
return false
}
/**
* @param {number} code
*/
// eslint-disable-next-line no-unused-vars
closeHttp2Stream(code) {
this.ws.close(1000)
}
initialParametersMandatory() {
return true
}
}