Node.js server implementation of the WebTransport protocol, using the WTransport Rust library.
pnpm add wtransport # or whichever package manager you are using
This library is ESM-only.
Currently, it only works on Linux OS on x86-64 and ARM CPUs (support for other platforms is coming soon).
- obtain an identity
- create a server
- specify how requests are handled
Servers are created using the WtServer
class accepting these parameters:
config
: the server configuration objecthandler
: the server handler object
WtServer
objects have these members:
close(errorCode, reason)
: closes all connections immediately and ceases accepting new connectionsidle
: a promise that resolves when all connections are shut downaddress
: the local address the underlying socket is bound toopenConnections
: the number of currently open connectionspatchConfig(config)
: updates some properties of the server config, rebinding to a new socket if needed
Once created, a server immediately begins to listen for requests until close
-d.
Example:
import {WtServer, WtIdentityFromPemFiles} from 'wtransport'
const server = new WtServer( {
identity: WtIdentityFromPemFiles('certificate_chain.pem', 'private_key.pem'),
bind: 7777,
}, {} )
server.address() //⇒ {address: '::', port: 7777, stack: 'v6'} or something like that
- common properties of a config
identity
: aWtIdentity
maxIdleTimeout?
: duration of inactivity to accept before timing out the connection; defaults to an infinite timeoutkeepAliveInterval?
: period of inactivity before sending a keep-alive packet; must be set lower thanmaxIdleTimeout
of both peers to be effective; defaults to not sending keep-alive packets
- mutually exclusive property groups specifying where the server should listen for requests; one and only one of these groups may be specified
- bind to a local socket
bind
: the socket portbindTo?
: either'local'
(LOCALHOST) or'any'
(INADDR_ANY; listens on all local interfaces); default is'any'
bindStack?
: either'v4'
,'v6'
or'dual'
; default isdual
- bind to an IPv4 socket specified by the address
bindV4
: the address
- bind to an IPv6 socket specified by the address
bindV6
: the addressbindV6Dual?
: IPv6 dual stack config; either'osDefault'
,'deny'
or'allow'
; default is'osDefault'
- bind to a local socket
WtIdentity
objects represents TLS identities consisting of a certificate chain and a private key. There are several ways to obtain one:
WtIdentityFromPem(certificateChainPem, privateKeyPem)
: from.pem
file contentsWtIdentityFromPemFiles(certificateChainPath, privateKeyPath)
: from.pem
file paths
Note that it may be tricky to get the self-signed development certificates accepted by your browser. Consider reading this project's local development docs
The handler is an object through the members of which the client processing pipeline is configured. The members are the following:
onRequest
: called after recieving an HTTP/3 requestonConnection
: called after establishing a QUIC connectiononSession
: called after establishing a WebTransport session
Accepts an object of the following shape:
address
: an UDP address of the peer
Valid return values are:
'refuse'
: to reject this connection attempt'ignore'
: to ignore this connection attempt, not sending any packet in response'retry'
: to respond with a retry packet, requiring the client to retry with address validationundefined
: to accept this connection attempt
Accepts an object of the following shape:
address
: an UDP address of the peerauthority
: the:authority
field of the requestpath
: the:path
field of the requestorigin
: theorigin
field of the request if presentuserAgent
: theuser-agent
field of the request if presentheaders
: all header fields associated with the request
Valid return values are:
'forbidden'
: rejects the request by replying with 403 status code'notFound'
: rejects the client request by replying with 404 status code.'tooManyRequests'
: rejects the client request by replying with 429 status code.undefined
: to establish the session
Accepts an object of the following shape:
address
: an UDP address of the peer; note that this may change during the session's lifetimeopenUni()
: opens a new outgoing unidirectional streamopenBi()
: opens a new outgoing bidirectional streamacceptUni()
: waits for an incoming unidirectional stream to be available and returns itacceptBi()
: waits for an incoming bidirectional stream to be available and returns itdatagramRecieve()
: waits for the next incoming datagram and returns its payloaddatagramSend(payload)
: sends a datagram, suddenlydatagramMaxSize
: the maximum size of datagrams that can be sent;undefined
if datagrams are unsupported; may change according to variation in the path MTU estimate; is at least a little over a kilobyte unless the peer specifies a lesser limitidUnstable
: an unstable WebTransport session identifierid
: a stable identifier for this connectionrtt
: current best estimate of this connection’s latency (round-trip-time) in millisecondsclosed
: whether the session is closedclose()
: immediately closes the connection
onSession
handler is expected to return nothing.
All streams have these members:
id
: numeric identifier of the stream
If a stream is readable (either incoming unidirectional or bidirectional), the following members are available:
readExact(count)
: reads exactlycount
bytes from the stream, failing if the stream finishes prematurelyreadTo(buffer)
: reads data contiguously from the stream tobuffer
and returns the number of bytes readreadNext(limit)
: reads the next segment of data not exceedinglimit
bytes, returningnull
if the stream finishes prematurelyreadChunk(limit)
: reads the next segment of data not exceedinglimit
bytes and returns a stream chunk ornull
if the stream finishes prematurelystop()
: discards unread data and notifies the peer to stop transmitting; the stream stops being readable after calling this
If a stream is writable (either outgoing unidirectional or bidirectional), the following members are available:
write(bytes)
: sendbytes
to the peerpriority
: a getter/setter; data from streams with higher priority will be transmitted before data from streams with lower priority; changing the priority of a stream with pending data may only take effect after that data has been transmitted; using many different priority levels per connection may have a negative impact on performancefinish()
: shuts down the stream gracefullyreset(errorCode?)
: closes the write stream immediatelystopped()
: waits for the stream to be stopped for any reason, which is returned:'closed'
,'notConnected'
,'quicProtocolError'
or'stopped'
An address is an object of the following shape:
toString()
ip
: an IPv4 or IPv6 address depending onstack
stack
:'v4
or'v6'
port
A stream chunk is an object of the following shape:
bytes
: data of the chunkoffset
: absolute offset of the chunk in the stream
To run examples locally, download their sources and replace fragments of code related to obtaining an identity and importing the library. Alternatively, refer to this project's development docs to learn how to pnpm -C examples/<name> dev
Source code: examples/echo_server
This example implements a WebTransport server that accepts a bidirectional stream and echoes back any data received from the client until the stream is closed.
import {WtIdentityFromPemFiles, WtServer} from 'wtransport'
new WtServer( {
identity: WtIdentityFromPemFiles('certificate_chain.pem', 'private_key.key'),
bind: 7777,
}, { async onSession(session) {
const id = session.id
console.log(`[${id}] established the session`)
const stream = await session.acceptBi()
console.log(`[${id}] accepted the bidirectional stream`)
for(;;) {
var chunk = await stream.readNext(1024)
if(!chunk) break
console.log(`[${id}] recieved bytes:`, chunk.values().toArray())
await stream.write(chunk)
}
session.close()
console.log(`[${id}] closed the session`)
} } )
console.log('server started')
This example implements a WebTransport server that echoes back all recieved datagrams.
Source code: examples/datagram_echo_server
Snippet:
import {WtIdentityFromPemFiles, WtServer} from 'wtransport'
new WtServer( {
identity: WtIdentityFromPemFiles('certificate_chain.pem', 'private_key.key'),
bind: 7777,
}, { async onSession(session) {
const id = session.id
console.log(`[${id}] established the session`)
for(;;) {
const limit = session.datagramMaxSize
if(!limit) {
console.log(`[${id}] cannot accept datagrams`)
break
}
const datagram = await session.datagramRecieve()
console.log(`[${id}] recieved a datagram:`, datagram.values().toArray())
for(
let start = 0, end = limit;
end < datagram.length;
start = end, end += limit
) {
await session.datagramSend( datagram.slice(start, end) )
}
}
session.close()
console.log(`[${id}] closed the session`)
} } )
console.log('server started')
AGPL-3.0-or-later