@fails-components/webtransport
Version:
A component to add webtransport support (server and client) to node.js using libquiche
249 lines (236 loc) • 8.41 kB
JavaScript
import { Http2WebTransportSession } from '../session.js'
import { BrowserParser } from './browserparser.js'
import { supportedVersions } from '../websocketcommon.js'
import { logger } from '../../utils.js'
const log = logger(`webtransport:http2:browser`)
export class Http2WebTransportBrowser {
/**
* @param {import('../../types.js').NativeClientOptions} args
*/
constructor(args) {
this.port = args?.port || 443
this.hostname = args?.host || 'localhost'
this.protocols = args?.protocols || []
this.initialStreamFlowControlWindow =
args?.initialStreamFlowControlWindow || 16 * 1024 // 16 KB
this.initialSessionFlowControlWindow =
args?.initialSessionFlowControlWindow || 16 * 1024 // 16 KB
this.initialBidirectionalStreams =
args?.initialBidirectionalSendStreams || 100
this.initialUnidirectionalStreams =
args?.initialUnidirectionalSendStreams || 100
this.streamShouldAutoTuneReceiveWindow =
args.streamShouldAutoTuneReceiveWindow || true
this.streamFlowControlWindowSizeLimit =
args?.streamFlowControlWindowSizeLimit || 6 * 1024 * 1024
this.sessionShouldAutoTuneReceiveWindow =
args.sessionShouldAutoTuneReceiveWindow || true
this.sessionFlowControlWindowSizeLimit =
args?.sessionFlowControlWindowSizeLimit || 15 * 1024 * 1024
/** @type {import('../../session.js').HttpClient} */
// @ts-ignore
this.jsobj = undefined // the transport will set this
/** @type {WebSocket} */
// @ts-ignore
this.clientInt = undefined
/** @type {undefined|string} */
this._webtransportProtocol = undefined
}
/**
* @param {{path: string}} arg
*/
createTransport({ path }) {
try {
let url = 'wss://' + this.hostname + ':' + this.port
if (path) url = url + '/' + path
let protocols = supportedVersions.map(
(/** @type {string} */ el) => 'webtransport_' + el
)
if (this.protocols.length > 0) {
const oldprotocols = protocols
protocols = protocols
.filter((el) => el !== 'kDraft1')
.map((el) => this.protocols.map((el2) => el + '_' + el2))
.flat(1)
protocols.push(...oldprotocols) // also need to support no protocol selection
}
// eslint-disable-next-line no-undef
this.clientInt = new WebSocket(url, protocols)
} catch (error) {
log('Failed on WebTransport/Websocket:', error)
this.jsobj.onClientConnected({
success: false
})
return
}
this.clientInt.binaryType = 'arraybuffer'
// eslint-disable-next-line no-unused-vars
this.clientInt.addEventListener('open', (event) => {
const protocol = this.clientInt?.protocol
if (!protocol) {
if (this.clientInt) this.clientInt.close()
this.jsobj.onClientConnected({
success: false
})
}
const aprotocol = protocol.split('_')
if (
!(aprotocol.length === 2 || aprotocol.length >= 3) ||
aprotocol[0] !== 'webtransport' ||
!supportedVersions.includes(aprotocol[1])
) {
if (this.clientInt) this.clientInt.close()
this.jsobj.onClientConnected({
success: false
})
} else {
this._webtransportProtocol =
aprotocol.length >= 3 ? aprotocol.slice(2).join('_') : undefined
this.jsobj.onClientWebTransportSupport(
aprotocol.length >= 3
? { selectedProtocol: aprotocol.slice(2).join('_') }
: {}
)
this.jsobj.onClientConnected({
success: true
})
}
})
this.clientInt.addEventListener('error', (error) => {
log('Failed on WebTransport/Websocket:', error)
if (
!this.jsobj?.sessionobjint ||
this.jsobj?.sessionobjint?.state === 'connecting'
)
this.jsobj.onClientConnected({
success: false
})
else {
if (this?.jsobj?.sessionobjint?.objint)
this.jsobj.sessionobjint.close({
closeCode: 0,
reason: error.toString()
})
}
})
}
/**
* @param {string} path
*/
// eslint-disable-next-line no-unused-vars
openWTSession(path) {
if (!this.clientInt) throw new Error('clientInt not present')
let sessobj
const retObj = {
session: new Http2WebTransportSession({
ws: this.clientInt,
isclient: true,
createParser: (
/** @type {Http2WebTransportSession} */ nativesession
) => {
sessobj = nativesession
const session = new BrowserParser({
ws: this.clientInt,
nativesession,
isclient: true,
initialStreamSendWindowOffsetBidi: 0,
initialStreamSendWindowOffsetUnidi: 0,
initialStreamReceiveWindowOffset:
this.initialStreamFlowControlWindow,
streamShouldAutoTuneReceiveWindow:
this.streamShouldAutoTuneReceiveWindow,
streamReceiveWindowSizeLimit: this.streamFlowControlWindowSizeLimit
})
if (this.clientInt)
this.clientInt.addEventListener('close', (event) => {
let code = event.code
let error = 'Session WebSocket closed'
if (event.reason) {
let tokens = event.reason.split(':')
if (tokens.length > 1) {
code = parseInt(tokens[0])
tokens = tokens.slice(1)
}
error = tokens.join(':')
} else {
switch (code) {
case 1001:
error = 'Going Away'
break
case 1002:
error = 'Protocol error'
break
case 1003:
error = 'Unsupported data'
break
case 1004:
error = 'Reserved'
break
case 1005:
error = 'No Status Rcvd'
break
case 1006:
error = 'Abnormal Closure'
break
case 1007:
error = 'Invalid frame payload data'
break
case 1008:
error = 'Policy Violation'
break
case 1009:
error = 'Message Too Big'
break
case 1010:
error = 'Mandatory Ext.'
break
case 1011:
error = 'Internal error'
break
case 1012:
error = 'Service Restart'
break
case 1013:
error = 'Try Again Later'
break
case 1014:
error = 'Bad Gateway'
break
case 1015:
error = 'TLS handshake'
break
case 1000:
default:
error = ''
break
}
}
nativesession.jsobj.onClose({
errorcode: code,
error
})
})
return session
},
sendWindowOffset: 0,
receiveWindowOffset: this.initialSessionFlowControlWindow,
shouldAutoTuneReceiveWindow: this.sessionShouldAutoTuneReceiveWindow,
receiveWindowSizeLimit: this.sessionFlowControlWindowSizeLimit,
initialBidirectionalSendStreams: this.initialBidirectionalStreams, // TODO, once supported by node, use initial settings
initialBidirectionalReceiveStreams: this.initialBidirectionalStreams,
initialUnidirectionalSendStreams: this.initialUnidirectionalStreams, // TODO, once supported by node, use initial settings
initialUnidirectionalReceiveStreams: this.initialUnidirectionalStreams
}),
reliable: true
}
this.jsobj.onHttpWTSessionVisitor(retObj)
// @ts-ignore
sessobj.jsobj.onReady(
this._webtransportProtocol ? { protocol: this._webtransportProtocol } : {}
)
}
closeClient() {
if (this.clientInt && this.clientInt.readyState === 1)
this.clientInt.close()
}
}