UNPKG

@fails-components/webtransport

Version:

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

404 lines (364 loc) 10.7 kB
import { ReadableStream } from 'node:stream/web' import { HttpWTSession } from './session.js' import { WebTransportError } from './error.js' import { Http2WebTransportServer } from './http2/node/index.js' import { isIPv4 } from 'net' import { defer, logger } from './utils.js' const log = logger(`webtransport:httpserver(${process?.pid})`) /** * @type {(arg: { path: string; } | undefined) => void} */ let checkQuicheInit /** * @type {new (arg: import("./types").HttpServerInit) => any} */ let Http3WebTransportServer /** * @type {new (arg: import("./types").HttpServerInit) => any} */ let Http3WebTransportServerSocket const quicheLoaded = new Promise((resolve, reject) => { // @ts-ignore import('@fails-components/webtransport-transport-http3-quiche') .then( /** * @type {import("./types").TransportHttp3Quiche} */ (http3lib) => { ;({ checkQuicheInit, Http3WebTransportServer, Http3WebTransportServerSocket } = http3lib) resolve(undefined) } ) .catch((error) => { console.error('Problem loading http3-quiche transport') console.error( 'Did you install @fails-components/webtransport-transport-http3-quiche?' ) log('Problem loading http3-quiche transport', error) log( 'Did you install @fails-components/webtransport-transport-http3-quiche?' ) reject(error) }) }).catch((error) => { console.log('http3 loader:', error) }) /** * @typedef {import('./types').WebTransportSessionImpl} WebTransportSession * @typedef {import('./types').NativeHttpWTSession} NativeHttpWTSession * @typedef {import('./types').HttpServerEventHandler} HttpServerEventHandler * @typedef {import('./types').HttpWTServerSessionVisitorEvent} HttpWTServerSessionVisitorEvent * @typedef {import('./types').HttpServerListeningEvent} HttpServerListeningEvent * @typedef {import('./types').ServerSessionRequestEvent} ServerSessionRequestEvent * @typedef {import('./types').HttpServerInit} HttpServerInit */ // @ts-ignore class TransportIntServerProxy { /** * @param {Array<any>} transportInts * */ constructor(transportInts) { this.transportsInts = new Set(transportInts) } startServer() { this.transportsInts.forEach((transport) => { if (transport.startServer) transport.startServer() }) } stopServer() { this.transportsInts.forEach((transport) => { if (transport.stopServer) transport.stopServer() }) } /** * @param {string|string[]} cert * @param {string|string[]} privKey * @param {boolean} http2only * */ updateCert(cert, privKey, http2only) { this.transportsInts.forEach((transport) => { if (transport.updateCert) transport.updateCert(cert, privKey, http2only) }) } /** * @param {boolean} hasHandler */ setJSRequestHandler(hasHandler) { this.transportsInts.forEach((transport) => transport.setJSRequestHandler(hasHandler) ) } /** * @param {string} path */ addPath(path) { this.transportsInts.forEach((transport) => transport.addPath(path)) } set jsobj(newJSobj) { this.transportsInts.forEach((transport) => (transport.jsobj = newJSobj)) } get jsobj() { return this.transportsInts.values().next().value } } /** * @implements {HttpServerEventHandler} */ export class HttpServer { /** * * @param {HttpServerInit} args */ constructor(args) { this.args = args /** @type {Record<string, ReadableStream>} */ this.sessionStreams = {} /** @type {Record<string, ReadableStreamDefaultController<HttpWTSession>>} */ this.sessionController = {} this.port = null this.host = null this.defaultDatagramsReadableMode_ = args.defaultDatagramsReadableMode // FIX ME TYPE /** @type {any} */ this.requestHandler = null this._ready = defer() this.ready = this._ready.promise this._closed = defer() this.closed = this._closed.promise /** * @type {string[]} */ this._pendingPaths = [] /** * @type {undefined|boolean} */ this._pendingRequestCallback = undefined } startServer() { this.createTransportInt() .then(() => { if (this.transportInt.startServer) this.transportInt.startServer() while (this._pendingPaths.length > 0) { const path = this._pendingPaths.shift() this.transportInt.addPath(path) } if (typeof this._pendingRequestCallback !== 'undefined') { this.transportInt.setJSRequestHandler(this._pendingRequestCallback) delete this._pendingRequestCallback } }) .catch((error) => { log('Problem in startServer', error) }) } stopServer() { this.transportInt.stopServer() for (const i in this.sessionController) { this.sessionController[i].close() // inform the controller, that we are closing delete this.sessionController[i] } this.stopped = true } /** * @param {string} cert * @param {string} privKey * @param {boolean} http2only * */ updateCert(cert, privKey, http2only) { if (this.transportInt.updateCert) this.transportInt.updateCert(cert, privKey, http2only) } /** * @returns {{ port: number, host: string, family: 'IPv4' | 'IPv6' } | null} */ address() { if (this.port == null || this.host == null) { return null } return { port: this.port, host: this.host, family: isIPv4(this.host) ? 'IPv4' : 'IPv6' } } /** * @param {any} callback */ setRequestCallback(callback) { this.requestHandler = callback if (this.transportInt) this.transportInt.setJSRequestHandler(!!callback) else this._pendingRequestCallback = !!callback } /** * @param {string} path * @param {object} [args] * @param {boolean} [args.noAutoPaths] * @returns {ReadableStream<WebTransportSession>} */ sessionStream(path, args) { if (path in this.sessionStreams) { return this.sessionStreams[path] } this.sessionStreams[path] = new ReadableStream({ start: async (controller) => { this.sessionController[path] = controller } }) if (!args || !args.noAutoPaths) { if (this.transportInt) this.transportInt.addPath(path) else this._pendingPaths.push(path) } return this.sessionStreams[path] } /** * @param {ServerSessionRequestEvent} args */ onSessionRequest(args) { if ((args.promise || args.protocol !== 'http3:libquiche') && args.header) { if (!this.requestHandler) throw new Error('Request handler not set') this.requestHandler({ header: args.header }) .then((/** @type {any} */ result) => { log('oSR', result) this.transportInt.finishSessionRequest({ peerAddress: args.peerAddress, promise: args.promise, header: args.header, session: args.session, head: args.head, transportPrivate: args.transportPrivate, protocol: args.protocol, ...result }) }) .catch((/** @type {any} */ err) => { log.error(err) }) } else throw new Error('onSessionRequest') } /** * @param {HttpWTServerSessionVisitorEvent} args */ onHttpWTSessionVisitor(args) { // create Http3 Visitor const sesobj = new HttpWTSession({ object: args.session, header: args.header, peerAddress: args.peerAddress, userData: args.userData ?? {}, datagramsReadableMode: this.defaultDatagramsReadableMode_, parentobj: this }) args.session.jsobj = sesobj if (this.sessionController[args.path]) this.sessionController[args.path].enqueue(sesobj) } /** * @param {Error} [error] */ onServerError(error) { this._ready.reject(error) } /** * @param {HttpServerListeningEvent} evt */ onServerListening(evt) { if (evt.host) this.host = evt.host if (evt.port) this.port = evt.port this._ready.resolve() } /** */ onServerClose() { this._closed.resolve() } /** * @param{{path: string} |undefined} [args] **/ async createTransportInt(args) { if (this.transportInt != null) { return } if ( // @ts-ignore this.args && // @ts-ignore (this.args?.reliability === 'unreliableOnly' || // @ts-ignore typeof this.args?.reliability === 'undefined' || // @ts-ignore this.args?.reliability === 'both') ) { await quicheLoaded checkQuicheInit(args) } try { let socket // @ts-ignore const reliability = this.args?.reliability || 'unreliableOnly' switch (reliability) { case 'unreliableOnly': socket = new Http3WebTransportServerSocket(this.args) this.transportInt = new Http3WebTransportServer(this.args) this.transportInt.stopServer = socket.stopServer.bind(socket) socket.init() socket.cobj = this.transportInt // @ts-ignore socket.jsobj = this // @ts-ignore this.transportInt.socket = socket break case 'reliableOnly': // @ts-ignore this.transportInt = new Http2WebTransportServer(this.args) break case 'both': { socket = new Http3WebTransportServerSocket(this.args) const server3 = new Http3WebTransportServer(this.args) server3.stopServer = socket.stopServer.bind(socket) socket.init() server3.socket = socket socket.cobj = server3 // @ts-ignore socket.jsobj = this // @ts-ignore const server2 = new Http2WebTransportServer(this.args) server2.jsobj = this this.transportInt = new TransportIntServerProxy([server2, server3]) } break } } catch (/** @type {any} */ err) { log('Problem opening transports:', err) const error = new WebTransportError('Opening transport failed.') error.stack = err.stack throw error } this.transportInt.jsobj = this if (this.transportInt.createTransport) { this.transportInt.createTransport() } } } export class Http3Server extends HttpServer { /** * * @param {HttpServerInit} args */ constructor(args) { super({ ...args, reliability: 'unreliableOnly' }) } } export class Http2Server extends HttpServer { /** * * @param {HttpServerInit} args */ constructor(args) { super({ ...args, reliability: 'reliableOnly' }) } }