joule-moq-relay: a WebTransport session closes with its MOQT code and reason (WT_CLOSE_SESSION), on a vendored wtransport 0.7.2 #365

Merged
dcharlot merged 11 commits from joule-moq/wt-session-close into main 2026-10-20 17:21:17 -04:00
36 changed files with 9947 additions and 41 deletions

View file

@ -204,6 +204,15 @@ jobs:
shell: bash
run: cargo test --locked -p joule-moq-relay --all-targets
# The relay's WebTransport layer, wtransport 0.7.2 vendored with a session close
# (vendor/wtransport/VENDORED.md): its unit tests, the WT_CLOSE_SESSION capsule's bytes
# against the layout of draft-ietf-webtrans-http3-16 §6 among them. It is not a member
# of the workspace, so `--all-targets` above does not reach it.
- name: The vendored wtransport's unit tests
working-directory: joule-code/joule-code-rs
shell: bash
run: cargo test --locked -p wtransport --lib
# The WHIP and WHEP gateway. Every test is on loopback, against a relay that checks
# every receipt at its door: a WHIP publisher (str0m as the client) of H.264 and Opus
# whose frames a relay subscriber receives whole with every receipt verified, and DELETE

View file

@ -23,6 +23,7 @@ on:
- 'joule-code/joule-code-rs/crates/joule-moq/**'
- 'joule-code/joule-code-rs/crates/joule-moq-relay/**'
- 'joule-code/joule-code-rs/crates/jwp/**'
- 'joule-code/joule-code-rs/vendor/**'
- 'wai/wai-rs/**'
- 'wai/wai-ops/**'
- '.github/workflows/joule-moq-room.yml'

View file

@ -43,6 +43,7 @@ on:
- 'joule-code/joule-code-rs/crates/jwp/**'
- 'joule-code/joule-code-rs/Cargo.toml'
- 'joule-code/joule-code-rs/Cargo.lock'
- 'joule-code/joule-code-rs/vendor/**'
- 'wai/wai-live-web/**'
- 'wai/wai-rs/**'
- 'wai/wai-ops/**'
@ -57,6 +58,7 @@ on:
- 'joule-code/joule-code-rs/crates/jwp/**'
- 'joule-code/joule-code-rs/Cargo.toml'
- 'joule-code/joule-code-rs/Cargo.lock'
- 'joule-code/joule-code-rs/vendor/**'
- 'wai/wai-live-web/**'
- 'wai/wai-rs/**'
- 'wai/wai-ops/**'

View file

@ -4,6 +4,38 @@ All notable changes to the Joule Code standard are recorded here. The format fol
## [Unreleased]
### Changed — `joule-moq-relay`: a session over WebTransport closes with its code and reason
- **The relay closes a WebTransport session, not the QUIC connection under it**
(`draft-ietf-moq-transport-22` §6.6): a WT_CLOSE_SESSION capsule on the CONNECT stream
carries the MOQT session termination code as the session's 32-bit application error code,
unmapped, and the reason (`draft-ietf-webtrans-http3-16` §6); the QUIC connection closes
after the peer has closed its side of that stream, or two seconds on. A page now reads the
relay's code and reason over WebTransport as it does over the WebSocket binding: a grant
presented without its proof, for one, is `UNAUTHORIZED` (2) with its live-rooms §10.5
reason, where the page saw only that the session was lost.
- **Nothing of the session overtakes its close.** A stream the relay lets go once a
WebTransport session is closing is held until the QUIC connection has closed, so no FIN or
STOP_SENDING reaches the page ahead of the capsule (a page whose stream was stopped just
before the close was seen to lose its renderer); a stream the relay refuses before SETUP
is kept until then too.
- **A peer's close is read the same way.** `closed()` over WebTransport gives the code and
reason of the peer's WT_CLOSE_SESSION (`by_peer: true`), so a page's close code reaches
the session's signed summary; a QUIC connection closed with the session still open gives
no MOQT code (its code is HTTP/3's).
- **`wtransport` 0.7.2 is vendored** (`joule-code-rs/vendor/wtransport`, MIT OR
Apache-2.0, patched in for the joule-code workspace and `joule-moq-room`), because the
release reads WT_CLOSE_SESSION but cannot send it. The change gives it
`Connection::close_session` and `ConnectionError::SessionClosed`, and makes `closed()`
report the session's end; `VENDORED.md` lists every change and why. Its unit tests, the
capsule's bytes against the draft's layout among them, run in the relay's CI job.
- **Tests.** `tests/webtransport_close.rs`: the relay refusing a grant without its proof,
refusing a stream that breaks a rule before SETUP (with no STOP_SENDING ahead of the
close), refusing a session past its session count, and ending one at its time limit, each
read by the client as the exact code and reason; and a client's own close, recorded as the
peer's in the relay's summary. The browser end-to-end check of a bare grant now requires
code 2 and the reason over WebTransport too.
### Added — DVR on the relay (L7b): `joule-moq-relay`'s DVR store, the seek power, the room page's Seek back
- **`joule-moq-relay` keeps a DVR window on disk** (`[dvr]`, `[[dvr.window]]`; WAI

View file

@ -4648,8 +4648,6 @@ checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
[[package]]
name = "wtransport"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4273ce3157a3262a68665f8d3f20a0ac0c5b8a69ffd67f05ae986832ebec036"
dependencies = [
"bytes",
"pem",

View file

@ -50,3 +50,9 @@ serde_json = "1"
base64 = "0.22"
hex = "0.4"
thiserror = "2"
[patch.crates-io]
# wtransport 0.7.2 with a WebTransport session close: a CLOSE_WEBTRANSPORT_SESSION
# capsule on the CONNECT stream (draft-ietf-webtrans-http3 §6), which the release
# cannot send. What was changed, and why, is in vendor/wtransport/VENDORED.md.
wtransport = { path = "vendor/wtransport" }

View file

@ -39,9 +39,9 @@ a room grant signs (below, *Tokens and room grants*).
Native QUIC and WebTransport share one quinn endpoint: the relay reads the ALPN of each
connection and either runs MOQT on it directly or hands it to the WebTransport layer.
**The WebTransport implementation** is `wtransport` 0.7 (MIT OR Apache-2.0). It was
chosen because it runs on the same quinn 0.11 as `joule-moq`, so one UDP endpoint serves
both ALPNs; it gives the server the CONNECT request's headers and lets it answer with its
**The WebTransport implementation** is `wtransport` 0.7.2 (MIT OR Apache-2.0), vendored
(below). It was chosen because it runs on the same quinn 0.11 as `joule-moq`, so one UDP
endpoint serves both ALPNs; it gives the server the CONNECT request's headers and lets it answer with its
own, which is what `WT-Available-Protocols` and `WT-Protocol` need; and its client checks
a certificate by hash under the rules a browser applies to `serverCertificateHashes`
(ECDSA P-256, a validity of fourteen days at most), so the relay's tests connect the way a
@ -49,6 +49,28 @@ page does. Stream bytes are read and written on the quinn streams underneath; ap
error codes on WebTransport streams are mapped into the HTTP/3 error space as
`draft-ietf-webtrans-http3-16` §4.4 gives.
**Closing a WebTransport session.** A MOQT session over WebTransport is closed by closing
the WebTransport session (`draft-ietf-moq-transport-22` §6.6). The relay sends a
`WT_CLOSE_SESSION` capsule (`0x2843`) on the session's CONNECT stream, then a FIN
(`draft-ietf-webtrans-http3-16` §6): the MOQT session termination code (§12.2) is the
capsule's 32-bit application error code as it stands, not mapped into the HTTP/3 space as
stream codes are, and the reason, at most 1,024 bytes of UTF-8, is its message. The QUIC
connection is closed with `H3_NO_ERROR` once the peer has closed its side of the CONNECT
stream, or two seconds after the capsule, so the connection's close cannot overtake it.
Nothing else of the session overtakes it either: a stream the relay lets go once the session
is closing (the client's control stream of a refused session, say) is held until the QUIC
connection has closed, rather than ended at once with a FIN or STOP_SENDING that would reach
the page before the capsule; a page whose stream was stopped just ahead of the close was
seen to lose its renderer. A page reads the code and reason from `WebTransport.closed`
(`closeCode`, `reason`), as it reads them from the WebSocket binding's `WT_CLOSE_SESSION`. A session the client closes is
read the same way: the relay's `closed()` gives the client's code and reason, and the
session's summary records the code as the peer's. A QUIC connection that closes with the
session still open gives no MOQT code; its code is an HTTP/3 error code.
The release of `wtransport` reads `WT_CLOSE_SESSION` but cannot send it, so the joule-code
workspace vendors 0.7.2 with a session close added (`vendor/wtransport`, patched in with
`[patch.crates-io]`; `vendor/wtransport/VENDORED.md` lists what changed and why).
## How a browser connects
**WebTransport, in development.** Start the relay with `--dev` (or with no certificate

View file

@ -120,24 +120,33 @@ pub fn fetch_range(f: Option<LocationFilter>, largest: Location) -> Option<(Loca
// ── The session ────────────────────────────────────────────────────────────
/// The client's SETUP, from its control stream; data streams that come first wait in
/// `early`. A stream that breaks a rule is put there too, so that it ends after the
/// session's close, not before it (`transport::webtransport` holds what is dropped then).
async fn read_setup(conn: &SharedConnection, max_payload: u64, early: &mut Vec<UniReader>) -> Result<(Setup, UniReader), String> {
loop {
let recv = conn.accept_uni().await.map_err(|e| e.to_string())?;
let mut r = UniReader::new(recv, max_payload);
match r.start().await.map_err(|e| e.to_string())? {
StreamHeader::Control => {
return match r.next().await.map_err(|e| e.to_string())? {
Some(UniEvent::Control(ControlMessage::Setup(s))) => Ok((s, r)),
_ => Err("the control stream does not begin with SETUP".into()),
};
}
_ => {
if early.len() >= EARLY_STREAMS {
return Err("too many data streams before SETUP".into());
}
let first = match r.start().await.map_err(|e| e.to_string()) {
Ok(StreamHeader::Control) => r.next().await.map_err(|e| e.to_string()),
Ok(_) if early.len() < EARLY_STREAMS => {
early.push(r);
continue;
}
}
Ok(_) => Err("too many data streams before SETUP".into()),
Err(e) => Err(e),
};
return match first {
Ok(Some(UniEvent::Control(ControlMessage::Setup(s)))) => Ok((s, r)),
Ok(_) => {
early.push(r);
Err("the control stream does not begin with SETUP".into())
}
Err(e) => {
early.push(r);
Err(e)
}
};
}
}

View file

@ -10,12 +10,32 @@
//! bytes are read and written on the quinn stream. Application error codes on WebTransport
//! streams are 32-bit and ride RESET_STREAM and STOP_SENDING mapped into the HTTP/3 error
//! space (webtrans-http3 §4.4); [`wt_to_h3`] and [`h3_to_wt`] are that mapping.
//!
//! The session closes by closing the WebTransport session (moq-transport-22 §6.6): a
//! WT_CLOSE_SESSION capsule on the CONNECT stream carries the MOQT session termination code
//! as the session's 32-bit application error code, unmapped, and the reason
//! (webtrans-http3 §6). The vendored wtransport sends it (`vendor/wtransport/VENDORED.md`),
//! and reports the one a peer sends, so a browser reads the relay's code and reason as it
//! does over the WebSocket binding. The QUIC connection closes after; its code is an HTTP/3
//! error code, not the session's.
//!
//! The capsule must reach the peer before any of the session's streams ends: §6 ends them
//! after the session, and a page whose stream was stopped just ahead of the close was seen
//! to lose its renderer. A stream dropped once the session is closing would send its FIN or
//! STOP_SENDING at once, so it is held (`Closing`) until the QUIC connection has closed,
//! when it ends with nothing more on the wire.
use super::quic::{conn_err, READ_CHUNK};
use super::{BoxRecv, BoxSend, Connection, RecvStream, SendStream, TResult, TransportError, TransportKind};
use async_trait::async_trait;
use bytes::Bytes;
use wtransport::VarInt;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
/// How long the streams of a closed session are held at most: past the vendored driver's
/// wait for the peer (two seconds) with room to spare.
const HOLD: Duration = Duration::from_secs(10);
/// The first HTTP/3 error code of the WebTransport application range.
const WT_FIRST: u64 = 0x52e4_a40f_a8db;
@ -68,11 +88,29 @@ pub fn select_protocol(header: Option<&str>) -> Option<&'static str> {
pub struct WtConnection {
conn: wtransport::Connection,
target: Option<String>,
closing: Arc<Closing>,
}
/// The streams dropped while a session this end closed is closing, held until its QUIC
/// connection has closed (module docs).
#[derive(Default)]
struct Closing {
holding: AtomicBool,
held: Mutex<Vec<Box<dyn Send>>>,
}
impl Closing {
/// Keep a stream while the session is closing; otherwise it is dropped here, as usual.
fn drop_or_hold(&self, h: Box<dyn Send>) {
if self.holding.load(Ordering::Acquire) {
self.held.lock().unwrap().push(h);
}
}
}
impl WtConnection {
pub fn new(conn: wtransport::Connection) -> Self {
WtConnection { conn, target: None }
WtConnection { conn, target: None, closing: Arc::default() }
}
/// The `//authority/path` of the CONNECT that opened the session.
@ -85,23 +123,71 @@ impl WtConnection {
fn wt_conn_err(e: &wtransport::error::ConnectionError) -> TransportError {
use wtransport::error::ConnectionError as E;
match e {
E::ApplicationClosed(c) => TransportError::Closed {
// The peer closed the session (WT_CLOSE_SESSION, or a FIN on the CONNECT stream,
// which reads as code 0): the code is the MOQT session's.
E::SessionClosed(c) => TransportError::Closed {
code: c.code().into_inner(),
reason: String::from_utf8_lossy(c.reason()).into_owned(),
by_peer: true,
},
// The connection closed with the session still open: an HTTP/3 error code, which
// says nothing of MOQT's.
E::ApplicationClosed(c) => TransportError::Closed {
code: u64::MAX,
reason: format!(
"the connection closed without closing the session (HTTP/3 error {:#x}){}{}",
c.code().into_inner(),
if c.reason().is_empty() { "" } else { ": " },
String::from_utf8_lossy(c.reason())
),
by_peer: true,
},
E::LocallyClosed => TransportError::Closed { code: 0, reason: "closed by this endpoint".into(), by_peer: false },
other => TransportError::Closed { code: u64::MAX, reason: other.to_string(), by_peer: true },
}
}
pub struct WtSend(wtransport::SendStream);
pub struct WtRecv(wtransport::RecvStream);
pub struct WtSend(Option<wtransport::SendStream>, Arc<Closing>);
pub struct WtRecv(Option<wtransport::RecvStream>, Arc<Closing>);
impl WtSend {
fn new(s: wtransport::SendStream, closing: &Arc<Closing>) -> Self {
WtSend(Some(s), closing.clone())
}
fn quic(&mut self) -> &mut quinn::SendStream {
self.0.as_mut().expect("held until dropped").quic_stream_mut()
}
}
impl WtRecv {
fn new(r: wtransport::RecvStream, closing: &Arc<Closing>) -> Self {
WtRecv(Some(r), closing.clone())
}
fn quic(&mut self) -> &mut quinn::RecvStream {
self.0.as_mut().expect("held until dropped").quic_stream_mut()
}
}
impl Drop for WtSend {
fn drop(&mut self) {
if let Some(s) = self.0.take() {
self.1.drop_or_hold(Box::new(s));
}
}
}
impl Drop for WtRecv {
fn drop(&mut self) {
if let Some(r) = self.0.take() {
self.1.drop_or_hold(Box::new(r));
}
}
}
#[async_trait]
impl SendStream for WtSend {
async fn write_all(&mut self, buf: &[u8]) -> TResult<()> {
match self.0.quic_stream_mut().write_all(buf).await {
match self.quic().write_all(buf).await {
Ok(()) => Ok(()),
Err(quinn::WriteError::Stopped(code)) => Err(TransportError::Stopped(h3_to_moqt(code.into_inner()))),
Err(quinn::WriteError::ConnectionLost(c)) => Err(conn_err(&c)),
@ -109,16 +195,16 @@ impl SendStream for WtSend {
}
}
fn finish(&mut self) -> TResult<()> {
self.0.quic_stream_mut().finish().map_err(TransportError::other)
self.quic().finish().map_err(TransportError::other)
}
fn reset(&mut self, code: u64) {
let _ = self.0.quic_stream_mut().reset(moqt_code_to_h3(code));
let _ = self.quic().reset(moqt_code_to_h3(code));
}
fn set_priority(&mut self, priority: i32) {
self.0.set_priority(priority);
let _ = self.quic().set_priority(priority);
}
async fn delivered(&mut self) -> TResult<Option<u64>> {
match self.0.quic_stream_mut().stopped().await {
match self.quic().stopped().await {
Ok(None) => Ok(None),
Ok(Some(code)) => Ok(Some(h3_to_moqt(code.into_inner()))),
Err(quinn::StoppedError::ConnectionLost(c)) => Err(conn_err(&c)),
@ -130,7 +216,7 @@ impl SendStream for WtSend {
#[async_trait]
impl RecvStream for WtRecv {
async fn read_chunk(&mut self) -> TResult<Option<Bytes>> {
match self.0.quic_stream_mut().read_chunk(READ_CHUNK, true).await {
match self.quic().read_chunk(READ_CHUNK, true).await {
Ok(Some(c)) => Ok(Some(c.bytes)),
Ok(None) => Ok(None),
Err(quinn::ReadError::Reset(code)) => Err(TransportError::Reset(h3_to_moqt(code.into_inner()))),
@ -139,7 +225,7 @@ impl RecvStream for WtRecv {
}
}
fn stop(&mut self, code: u64) {
let _ = self.0.quic_stream_mut().stop(moqt_code_to_h3(code));
let _ = self.quic().stop(moqt_code_to_h3(code));
}
}
@ -157,20 +243,20 @@ impl Connection for WtConnection {
async fn open_uni(&self) -> TResult<BoxSend> {
let opening = self.conn.open_uni().await.map_err(|e| wt_conn_err(&e))?;
let s = opening.await.map_err(TransportError::other)?;
Ok(Box::new(WtSend(s)))
Ok(Box::new(WtSend::new(s, &self.closing)))
}
async fn open_bi(&self) -> TResult<(BoxSend, BoxRecv)> {
let opening = self.conn.open_bi().await.map_err(|e| wt_conn_err(&e))?;
let (s, r) = opening.await.map_err(TransportError::other)?;
Ok((Box::new(WtSend(s)), Box::new(WtRecv(r))))
Ok((Box::new(WtSend::new(s, &self.closing)), Box::new(WtRecv::new(r, &self.closing))))
}
async fn accept_uni(&self) -> TResult<BoxRecv> {
let r = self.conn.accept_uni().await.map_err(|e| wt_conn_err(&e))?;
Ok(Box::new(WtRecv(r)))
Ok(Box::new(WtRecv::new(r, &self.closing)))
}
async fn accept_bi(&self) -> TResult<(BoxSend, BoxRecv)> {
let (s, r) = self.conn.accept_bi().await.map_err(|e| wt_conn_err(&e))?;
Ok((Box::new(WtSend(s)), Box::new(WtRecv(r))))
Ok((Box::new(WtSend::new(s, &self.closing)), Box::new(WtRecv::new(r, &self.closing))))
}
fn send_datagram(&self, payload: Bytes) -> TResult<()> {
self.conn.send_datagram(&payload).map_err(TransportError::other)
@ -179,9 +265,23 @@ impl Connection for WtConnection {
let d = self.conn.receive_datagram().await.map_err(|e| wt_conn_err(&e))?;
Ok(d.payload())
}
/// WT_CLOSE_SESSION with the MOQT code as the session's application error code (a code
/// past 32 bits as `u32::MAX`) and the reason, then the connection's close; the streams
/// dropped from here are held until the connection has closed (module docs).
fn close(&self, code: u64, reason: &[u8]) {
let code = VarInt::try_from_u64(code).unwrap_or(VarInt::MAX);
self.conn.close(code, reason);
if let Ok(rt) = tokio::runtime::Handle::try_current()
&& !self.closing.holding.swap(true, Ordering::AcqRel)
{
let quic = self.conn.quic_connection().clone();
let closing = self.closing.clone();
rt.spawn(async move {
let _ = tokio::time::timeout(HOLD, quic.closed()).await;
closing.holding.store(false, Ordering::Release);
closing.held.lock().unwrap().clear();
});
}
let code = u32::try_from(code).unwrap_or(u32::MAX);
self.conn.close_session(code, &String::from_utf8_lossy(reason));
}
async fn closed(&self) -> TransportError {
wt_conn_err(&self.conn.closed().await)

View file

@ -0,0 +1,119 @@
//! A session over WebTransport is closed by closing the WebTransport session
//! (`draft-ietf-moq-transport-22` §6.6): the MOQT session termination code and the reason
//! ride a WT_CLOSE_SESSION capsule on the CONNECT stream (`draft-ietf-webtrans-http3-16`
//! §6), and the other end reads both exactly, as it does over the WebSocket binding. A QUIC
//! connection close alone carries an HTTP/3 error code, which no end reads as MOQT's.
//!
//! The relay closes a session four ways here: refusing a room grant presented without its
//! proof, after SETUP; refusing a stream that breaks a rule before SETUP, which it stops only
//! after the session's close; refusing a session past its session count, before any MOQT
//! message; and ending an established session at its time limit. And a client closes one,
//! whose code the relay's signed summary records as the peer's.
mod common;
use common::*;
use ed25519_dalek::SigningKey;
use joule_moq::draft22::{SessionError, Token};
use joule_moq_relay::client::ClientOptions;
use joule_moq_relay::config::Grant;
use joule_moq_relay::transport::{webtransport, Connection, TransportError};
use joule_moq_relay::RelayServer;
use std::time::Duration;
use tokio::time::timeout;
/// A record shaped as a room grant (`WRG1`, version 1, a room, the principal), which is
/// all the relay reads of it: its rule names the record by its SHA-256.
fn grant_record(room: u8, principal: &SigningKey) -> Vec<u8> {
let mut g = b"WRG1\x01".to_vec();
g.extend_from_slice(&[room; 32]);
g.extend_from_slice(&principal.verifying_key().to_bytes());
g.extend_from_slice(&[0x5a; 160]);
g.extend_from_slice(&[0x77; 64]);
g
}
async fn closed(conn: &dyn Connection, what: &str) -> TransportError {
timeout(STEP, conn.closed()).await.unwrap_or_else(|_| panic!("{what}: not closed in time"))
}
fn by_relay(code: SessionError, reason: &str) -> TransportError {
TransportError::Closed { code: code.0, reason: reason.into(), by_peer: true }
}
/// A WebTransport session to `server` with no MOQT on it yet.
async fn session(server: &RelayServer) -> webtransport::WtConnection {
let url = format!("https://127.0.0.1:{}{}", server.quic_addr.unwrap().port(), server.relay().config().path);
timeout(STEP, webtransport::connect(&url, Some(server.identity.leaf_sha256), loopback()))
.await
.expect("in time")
.expect("a WebTransport session")
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn a_session_over_webtransport_closes_with_its_code_and_reason() {
timeout(Duration::from_secs(120), run()).await.expect("the test finishes in time");
}
async fn run() {
// A room grant presented as a bare token: the relay refuses the session after SETUP,
// UNAUTHORIZED (WAI live-rooms §10.5.2).
let host = SigningKey::from_bytes(&[0x51; 32]);
let record = grant_record(3, &host);
let hash = hex::encode(joule_moq_relay::auth::sha256(&record));
let rooms = relay(|c| {
c.auth.required = true;
c.auth.grants = vec![Grant { prefix: vec!["room".into()], token_sha256: hash, publish: true, subscribe: true, proof: true, unlimited: false, seek: false }];
})
.await;
let bare = ClientOptions { tokens: vec![Token::UseValue { token_type: 0, value: record }], ..ClientOptions::default() };
let (c, _) = connect(&rooms, Via::WebTransport, bare).await;
assert_eq!(closed(c.conn.as_ref(), "a grant with no proof").await, by_relay(SessionError::UNAUTHORIZED, "live-rooms §10.5: pop_missing"));
// A stream that breaks a rule before SETUP: refused, PROTOCOL_VIOLATION. The relay ends
// that stream only after the session's close, so the client reads the close and never
// a STOP_SENDING ahead of it.
let raw = session(&rooms).await;
let mut s = timeout(STEP, raw.open_uni()).await.expect("in time").expect("a stream");
s.write_all(&[0x3f; 4]).await.expect("written");
match closed(&raw, "a stream that breaks a rule").await {
TransportError::Closed { code, reason, by_peer: true } if code == SessionError::PROTOCOL_VIOLATION.0 => assert!(reason.contains("invalid flags"), "{reason}"),
other => panic!("a stream that breaks a rule: {other:?}"),
}
match timeout(STEP, s.delivered()).await.expect("in time") {
Err(TransportError::Closed { .. }) => {}
other => panic!("the relay stopped the stream ahead of the session's close: {other:?}"),
}
// A client closes its session with a code of its own: the relay reads it as the peer's,
// and its summary records it; the client's own end reads its close as local.
let (c, _) = connect(&rooms, Via::WebTransport, ClientOptions::default()).await;
c.close(SessionError::PROTOCOL_VIOLATION.0, "a client's own close");
assert_eq!(
closed(c.conn.as_ref(), "the client's close").await,
TransportError::Closed { code: 0, reason: "closed by this endpoint".into(), by_peer: false }
);
let recorded = until(STEP, || {
rooms.relay().summaries().iter().any(|s| s.summary.transport == "webtransport" && s.summary.close_code == Some(SessionError::PROTOCOL_VIOLATION.0))
})
.await;
let codes: Vec<_> = rooms.relay().summaries().iter().map(|s| (s.summary.transport.clone(), s.summary.close_code)).collect();
assert!(recorded, "the relay records the client's code: {codes:?}");
rooms.shutdown().await;
// A relay that holds one session at a time, each for three seconds at most.
let limited = relay(|c| {
c.limits.max_sessions = 1;
c.limits.max_session_ms = 3_000;
})
.await;
let (held, _) = connect(&limited, Via::WebTransport, ClientOptions::default()).await;
assert!(until(STEP, || limited.relay().session_diagnostics().len() == 1).await, "the first session is held");
// Past its session count: refused before any MOQT message, INTERNAL_ERROR.
let refused = session(&limited).await;
assert_eq!(closed(&refused, "a session past the count").await, by_relay(SessionError::INTERNAL_ERROR, "the relay is at its session limit"));
// At its time limit, an established session is ended with NO_ERROR and the reason, which
// a FIN on the CONNECT stream alone (code 0, no reason) would not carry.
assert_eq!(closed(held.conn.as_ref(), "a session at its time limit").await, by_relay(SessionError::NO_ERROR, "the relay's session limit of 3 s"));
limited.shutdown().await;
}

View file

@ -2627,8 +2627,6 @@ checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
[[package]]
name = "wtransport"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4273ce3157a3262a68665f8d3f20a0ac0c5b8a69ffd67f05ae986832ebec036"
dependencies = [
"bytes",
"pem",

View file

@ -12,6 +12,11 @@ repository = "https://github.com/transaction-science/open-standards"
# joule-code workspace.
[workspace]
# The relay's wtransport, vendored with its WebTransport session close; the joule-code
# workspace patches it the same way (vendor/wtransport/VENDORED.md).
[patch.crates-io]
wtransport = { path = "../../vendor/wtransport" }
[lib]
path = "src/lib.rs"

View file

@ -301,13 +301,12 @@ async function scenarioRoom(run, transport) {
fs.writeFileSync(path.join(dir, "receipts-sizes.json"), JSON.stringify(sv, null, 1));
// A grant presented as a bare token, with no proof of its key: the relay refuses the
// session (live-rooms §10.5.2), UNAUTHORIZED. Over the WebSocket binding the page reads
// the code (2) and the reason; over WebTransport the relay's transport closes the QUIC
// connection rather than the WebTransport session, so the page sees the session lost,
// and the relay's log gives the reason.
// session (live-rooms §10.5.2), UNAUTHORIZED. The page reads the code (2) and the reason
// over either transport: over WebTransport the relay closes the WebTransport session
// with them (WT_CLOSE_SESSION, moq-transport-22 §6.6), as over the WebSocket binding.
const bare = await alice.evaluate(() => window.roomTest.tryBareGrant());
const refusals = relay.lines.filter((l) => l.includes("without a proof that passes"));
const coded = bare.transport === "websocket" ? !!bare.closed && bare.closed.code === 2 : !!bare.closed;
const coded = !!bare.closed && bare.closed.code === 2 && /pop_missing/.test(bare.closed.reason);
check(`${label}: a grant presented without its proof is refused the session`, coded && refusals.some((l) => l.includes("pop_missing")),
`${JSON.stringify(bare).slice(0, 200)}; relay: ${(refusals[0] || "").replace(/^.*INFO /, "").slice(0, 200)}`);
check(`${label}: the relay refused that session and no other`, refusals.length === 1, `${refusals.length} refused`);

View file

@ -0,0 +1,6 @@
{
"git": {
"sha1": "d022a6ad5f6260cb526e06d8ca42d09516ce6440"
},
"path_in_vcs": "wtransport"
}

1941
joule-code/joule-code-rs/vendor/wtransport/Cargo.lock generated vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,177 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
rust-version = "1.88"
name = "wtransport"
version = "0.7.2"
authors = ["Biagio Festa"]
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Implementation of the WebTransport (over HTTP3) protocol"
readme = "README.md"
keywords = ["webtransport"]
categories = [
"network-programming",
"asynchronous",
]
license = "MIT OR Apache-2.0"
repository = "https://github.com/BiagioFesta/wtransport"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = [
"--cfg",
"docsrs",
]
[package.metadata.cargo_check_external_types]
allowed_external_types = [
"bytes::bytes::Bytes",
"quinn",
"quinn::connection::Connecting",
"quinn::connection::Connection",
"quinn::incoming::Incoming",
"quinn::recv_stream::ReadError",
"quinn::recv_stream::RecvStream",
"quinn::send_stream::SendStream",
"quinn::send_stream::WriteError",
"quinn_proto::config::ClientConfig",
"quinn_proto::config::EndpointConfig",
"quinn_proto::config::ServerConfig",
"quinn_proto::config::transport::TransportConfig",
"quinn_proto::connection::ConnectionError",
"rustls",
"rustls::client::client_conn::ClientConfig",
"rustls::server::server_conn::ServerConfig",
"rustls::verify::ServerCertVerifier",
"rustls::webpki::anchors::RootCertStore",
"time",
"time::Duration",
"time::offset_date_time::OffsetDateTime",
"tokio::io::async_read::AsyncRead",
"tokio::io::async_write::AsyncWrite",
"wtransport_proto",
"wtransport_proto::WEBTRANSPORT_ALPN",
"wtransport_proto::ids::SessionId",
"wtransport_proto::ids::StreamId",
"wtransport_proto::varint::VarInt",
]
[features]
aws-lc-rs = [
"quinn/aws-lc-rs",
"quinn/rustls-aws-lc-rs",
"rustls/aws-lc-rs",
"rcgen?/aws_lc_rs",
]
aws-lc-rs-fips = [
"quinn/aws-lc-rs-fips",
"quinn/rustls-aws-lc-rs-fips",
"rustls/fips",
"rcgen?/fips",
]
dangerous-configuration = []
default = [
"self-signed",
"ring",
]
fips = ["aws-lc-rs-fips"]
quinn = []
quinn-log = ["quinn/log"]
ring = [
"quinn/ring",
"quinn/rustls",
"rustls/ring",
"rcgen?/ring",
]
self-signed = ["dep:rcgen"]
[lib]
name = "wtransport"
path = "src/lib.rs"
[dependencies.bytes]
version = "1.4.0"
[dependencies.pem]
version = "3.0.4"
[dependencies.quinn]
version = "0.11.6"
features = ["runtime-tokio"]
default-features = false
[dependencies.rcgen]
version = "0.14.5"
optional = true
default-features = false
[dependencies.rustls]
version = "0.23.23"
default-features = false
[dependencies.rustls-native-certs]
version = "0.8.0"
[dependencies.rustls-pki-types]
version = "1.13.2"
[dependencies.sha2]
version = "0.11.0"
[dependencies.socket2]
version = "0.6.3"
[dependencies.thiserror]
version = "2.0.3"
[dependencies.time]
version = "0.3.21"
[dependencies.tokio]
version = "1.28.1"
features = [
"macros",
"fs",
"io-util",
"time",
]
default-features = false
[dependencies.tracing]
version = "0.1.37"
[dependencies.url]
version = "2.4.0"
[dependencies.wtransport-proto]
version = "0.7.2"
features = ["async"]
[dependencies.x509-parser]
version = "0.18.1"
[lints.clippy]
manual_is_variant_and = "warn"
manual_let_else = "warn"
unnecessary_cast = "deny"
[lints.rust]
unfulfilled_lint_expectations = "deny"
unit_bindings = "deny"
unsafe_op_in_unsafe_fn = "deny"
unused_qualifications = "warn"

View file

@ -0,0 +1,105 @@
[package]
name = "wtransport"
version = "0.7.2"
license = "MIT OR Apache-2.0"
authors = ["Biagio Festa"]
description = "Implementation of the WebTransport (over HTTP3) protocol"
repository = "https://github.com/BiagioFesta/wtransport"
keywords = ["webtransport"]
categories = [ "network-programming", "asynchronous" ]
edition = "2021"
readme = "../README.md"
workspace = ".."
rust-version = "1.88"
[[example]]
name = "client"
required-features = ["dangerous-configuration"]
[[example]]
name = "full"
required-features = ["self-signed"]
[[example]]
name = "server"
required-features = ["self-signed"]
[[example]]
name = "gencert"
required-features = ["self-signed"]
[dependencies]
bytes = "1.4.0"
pem = "3.0.4"
quinn = { version = "0.11.6", default-features = false, features = ["runtime-tokio"] }
rcgen = { version = "0.14.5", default-features = false, optional = true }
rustls = { version = "0.23.23", default-features = false }
rustls-native-certs = "0.8.0"
rustls-pki-types = "1.13.2"
sha2 = "0.11.0"
socket2 = "0.6.3"
thiserror = "2.0.3"
time = "0.3.21"
tokio = { version = "1.28.1", default-features = false, features = ["macros", "fs", "io-util"] }
tracing = "0.1.37"
url = "2.4.0"
wtransport-proto = { version = "0.7.2", path = "../wtransport-proto", features = ["async"] }
x509-parser = "0.18.1"
[dev-dependencies]
anyhow = "1.0.71"
axum = "0.7.1"
hyper = "1.0.1"
tokio = { version = "1.28.1", default-features = false, features = ["rt-multi-thread"] }
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
[features]
default = ["self-signed", "ring"]
aws-lc-rs = ["quinn/aws-lc-rs", "quinn/rustls-aws-lc-rs", "rustls/aws-lc-rs", "rcgen?/aws_lc_rs"]
aws-lc-rs-fips = ["quinn/aws-lc-rs-fips", "quinn/rustls-aws-lc-rs-fips", "rustls/fips", "rcgen?/fips"]
dangerous-configuration = []
fips = ["aws-lc-rs-fips"]
quinn = []
quinn-log = ["quinn/log"]
ring = ["quinn/ring", "quinn/rustls", "rustls/ring", "rcgen?/ring"]
self-signed = ["dep:rcgen"]
[lints]
workspace = true
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[package.metadata.cargo_check_external_types]
allowed_external_types = [
"bytes::bytes::Bytes",
"quinn",
"quinn::connection::Connecting",
"quinn::connection::Connection",
"quinn::incoming::Incoming",
"quinn::recv_stream::ReadError",
"quinn::recv_stream::RecvStream",
"quinn::send_stream::SendStream",
"quinn::send_stream::WriteError",
"quinn_proto::config::ClientConfig",
"quinn_proto::config::EndpointConfig",
"quinn_proto::config::ServerConfig",
"quinn_proto::config::transport::TransportConfig",
"quinn_proto::connection::ConnectionError",
"rustls",
"rustls::client::client_conn::ClientConfig",
"rustls::server::server_conn::ServerConfig",
"rustls::verify::ServerCertVerifier",
"rustls::webpki::anchors::RootCertStore",
"time",
"time::Duration",
"time::offset_date_time::OffsetDateTime",
"tokio::io::async_read::AsyncRead",
"tokio::io::async_write::AsyncWrite",
"wtransport_proto",
"wtransport_proto::WEBTRANSPORT_ALPN",
"wtransport_proto::ids::SessionId",
"wtransport_proto::ids::StreamId",
"wtransport_proto::varint::VarInt",
]

View file

@ -0,0 +1,176 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Biagio Festa
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,139 @@
<p align="center">
<img src="https://raw.githubusercontent.com/BiagioFesta/wtransport/master/imgs/logo.svg" alt="WTransport Logo" />
</p>
[![Documentation](https://docs.rs/wtransport/badge.svg)](https://docs.rs/wtransport/)
[![Crates.io](https://img.shields.io/crates/v/wtransport.svg)](https://crates.io/crates/wtransport)
[![CI](https://github.com/BiagioFesta/wtransport/actions/workflows/ci.yml/badge.svg)](https://github.com/BiagioFesta/wtransport/actions/workflows/ci.yml)
[![Chat](https://img.shields.io/badge/chat-join-blue?logo=discord)](https://discord.gg/KPrrbWe5zg)
# WTransport
[WebTransport](https://datatracker.ietf.org/doc/html/draft-ietf-webtrans-http3/) protocol, pure-*rust*, *async*-friendly.
## Introduction
WebTransport is a new protocol being developed to enable *low-latency*, *bidirectional* communication between clients and servers over the web.
It aims to address the limitations of existing protocols like *HTTP* and *WebSocket* by offering a more *efficient* and *flexible* transport layer.
### Benefits of WebTransport
* 🚀 **Low latency**: WebTransport is designed to minimize latency, making it suitable for real-time applications such as gaming, video streaming, and collaborative editing.
* 🔄 **Bidirectional communication**: WebTransport allows simultaneous data exchange between the client and server, enabling efficient back-and-forth communication without the need for multiple requests.
* 🔀 **Multiplexing**: With WebTransport, multiple streams can be multiplexed over a single connection, reducing overhead and improving performance.
* 🔒 **Security**: WebTransport benefits from the security features provided by the web platform, including transport encryption and same-origin policy.
* 🌐 **Native Browser Support**: WebTransport is natively supported in modern web browsers, ensuring seamless integration and enhanced performance for web applications.
<p align="center">
<a href="https://docs.rs/wtransport/latest/wtransport/">Check Library Documentation</a>
</p>
### Notes
Please be aware that WebTransport is still a *draft* and not yet standardized.
The *WTransport* library, while functional, is not considered completely production-ready.
It should be used with caution and may undergo changes as the WebTransport specification evolves.
## Simple API
<table>
<tr>
<th> Server </th>
<th> Client </th>
</tr>
<tr>
<td>
```rust
#[tokio::main]
async fn main() -> Result<()> {
let config = ServerConfig::builder()
.with_bind_default(4433)
.with_identity(&identity)
.build();
let connection = Endpoint::server(config)?
.accept()
.await // Awaits connection
.await? // Awaits session request
.accept() // Accepts request
.await?; // Awaits ready session
let stream = connection.accept_bi().await?;
// ...
}
```
</td>
<td>
```rust
#[tokio::main]
async fn main() -> Result<()> {
let config = ClientConfig::default();
let connection = Endpoint::client(config)?
.connect("https://[::1]:4433")
.await?;
let stream = connection.open_bi().await?.await?;
// ...
}
```
</td>
</tr>
</table>
## Browser Integration
WebTransport [is supported](https://caniuse.com/mdn-api_webtransport) in modern browsers,
enhancing the capabilities of web applications.
For instance, you can create a native *browser WebTransport* client connecting to a *Rust
server* using the following JavaScript code:
```javascript
// Create a WebTransport instance connecting to the Rust server
let transport = new WebTransport('https://[::1]:4433');
await transport.ready;
// Create a bidirectional stream
let stream = await transport.createBidirectionalStream();
// Send data from the client to the server
await stream.writable.getWriter().write(new TextEncoder().encode("hello"));
// Read data reply from the server
let data = await stream.readable.getReader().read();
console.log(data);
```
Check out the [W3C WebTransport API documentation](https://w3c.github.io/webtransport/) for more details and to
explore the full capabilities of WebTransport in the browser.
## Getting Started
### Clone the Repository
```bash
git clone https://github.com/BiagioFesta/wtransport.git
```
```bash
cd wtransport/
```
### Run `Full` Example
The [`examples/full.rs`](wtransport/examples/full.rs) is a minimal but complete server example that demonstrates the usage of WebTransport.
You can run this example using [*Cargo*](https://rustup.rs/), Rust's package manager, with the following command:
```bash
cargo run --example full
```
This example initiates an *echo* WebTransport server that can receive messages. It also includes an integrated HTTP server.
Open [a supported web browser](https://caniuse.com/mdn-api_webtransport) and navigate to the page http://127.0.0.1:8080.
## Examples
* [Local Examples](https://github.com/BiagioFesta/wtransport/tree/master/wtransport/examples)
## Other Languages
WTransport has bindings for the following languages:
- Elixir: [wtransport-elixir](https://github.com/bugnano/wtransport-elixir)

View file

@ -0,0 +1,62 @@
# wtransport 0.7.2, vendored
**Upstream.** The `wtransport` crate, version 0.7.2, exactly as published on crates.io: the
`.crate` file's SHA-256 is `b4273ce3157a3262a68665f8d3f20a0ac0c5b8a69ffd67f05ae986832ebec036`,
the checksum the joule-code lock held for it, and `.cargo_vcs_info.json` names the upstream
commit it was packaged from (`d022a6ad5f6260cb526e06d8ca42d09516ce6440`, directory
`wtransport`). `LICENSE-MIT` and `LICENSE-APACHE` are the upstream's, from that commit; the
crate is MIT OR Apache-2.0, and these changes are offered under the same terms. The
crate's companion, `wtransport-proto` 0.7.2, is not vendored: nothing in it changes.
**Where it is used.** `[patch.crates-io]` in `joule-code/joule-code-rs/Cargo.toml` and in
`crates/joule-moq-room/Cargo.toml` (a workspace of its own that builds the relay). Its
`Cargo.lock` and `Cargo.toml.orig` are the upstream's, kept with the source; cargo reads
neither, and `scripts/check-lockfiles.sh` skips the lock.
## Why
A MOQT session over WebTransport is closed by closing the WebTransport session
(`draft-ietf-moq-transport-22` §6.6, which calls the capsule by its earlier name,
CLOSE_WEBTRANSPORT_SESSION): a WT_CLOSE_SESSION capsule on the session's CONNECT stream
carries a 32-bit application error code and a UTF-8 reason of at most 1024 bytes, followed
by a FIN (`draft-ietf-webtrans-http3-16` §6). The release reads that capsule but cannot
send it: its CONNECT stream belongs to the driver's worker, and `Connection::close` closes
the QUIC connection, whose code a browser's WebTransport API does not expose. A page whose
session joule-moq-relay refused saw only that the session was lost.
## What changed
- `Connection::close_session(error_code: u32, reason: &str)`. It hands the driver's worker
a request (`Driver::close_session`); the worker writes one HTTP/3 DATA frame holding the
capsule (`close_session_capsule`, in `src/driver/streams/connect.rs`) on the CONNECT
stream at the highest send priority, sends the FIN, and then reads that stream until the
peer's FIN or reset, at most two seconds (`SESSION_CLOSE_LINGER`), before it closes the
QUIC connection with H3_NO_ERROR: §6 asks an endpoint to wait for the peer to close its
CONNECT stream first, so the connection's close cannot overtake the capsule. The reason
is cut to 1024 bytes at a UTF-8 character boundary, as §6 requires of a sender that
truncates.
- After `close_session` this end opens no stream and sends no datagram (§6): `open_uni`,
`open_bi` and `send_datagram` fail at once.
- A close requested just as the session's last handle is dropped still goes out: the
worker's loop can see the handles gone first, so `Worker::run` takes a pending request
before it ends.
- `ConnectionError::SessionClosed`, for a session the peer closed (the capsule's code and
reason, or code 0 and no reason for a FIN alone, §6). The release reported that as
`ApplicationClosed`, the variant it also uses for the QUIC connection's close, whose code
under HTTP/3 is an HTTP/3 error code; the two could not be told apart.
- `Connection::closed()` resolves with how the session ended, as the driver saw it, rather
than with how the QUIC connection closed after: `SessionClosed` with the peer's code and
reason, or `LocallyClosed` as soon as this end's capsule is queued. The release's
`closed()` gave the QUIC close, so a peer's capsule was never reported there.
- The tokio feature `time` is named, for the wait's bound (it was on already through quinn).
- The upstream's examples and dev-dependencies are left out, so that the crate's unit tests,
the upstream's and those of the capsule's layout, run from the joule-code workspace:
`cargo test --locked -p wtransport --lib` (cargo tests a non-member crate only when it
has no dev-dependencies). The CI job of joule-moq-relay runs them.
## What did not change
The streams of a session this end closes are not reset one by one with WT_SESSION_GONE, as
§6 asks of an endpoint whose connection carries on; the driver does not track them. They end
with the QUIC connection, at most two seconds later. joule-moq-relay serves one session per
connection, and the session ends at the capsule for both ends.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,454 @@
//! # WebTransport Connection
//!
//! [`Connection`] provides an essential building block for managing WebTransport
//! connections. It allows you to initiate, accept, and control data *streams*, send and receive
//! *datagrams*, monitor connection status, and interact with various aspects of your WebTransport
//! communication.
//!
//! WebTransport exchanges data either via [*streams*](crate#streams) or [*datagrams*](crate#datagrams).
//!
//! ## Streams
//! WebTransport streams provide a lightweight, ordered byte-stream abstraction.
//!
//! There are two fundamental types of streams:
//! - *Unidirectional* streams carry data in a single direction, from the stream initiator to its peer.
//! - *Bidirectional* streams allow for data to be sent in both directions.
//!
//! Both server and client endpoints have the capability to create an arbitrary number of streams to
//! operate concurrently.
//!
//! Each stream can be independently cancelled by both side.
//!
//! ### Examples
//! #### Open a stream
//! ```no_run
//! # use anyhow::Result;
//! # async fn foo(connection: wtransport::Connection) -> Result<()> {
//! use wtransport::Connection;
//!
//! // Open a bi-directional stream
//! let (mut send_stream, mut recv_stream) = connection.open_bi().await?.await?;
//!
//! // Send data on the stream
//! send_stream.write_all(b"Hello, wtransport!").await?;
//!
//! // Receive data from the stream
//! let mut buffer = vec![0; 1024];
//! let bytes_read = recv_stream.read(&mut buffer).await?;
//!
//! // Open an uni-directional stream (can only send data)
//! let mut send_stream = connection.open_uni().await?.await?;
//!
//! // Send data on the stream
//! send_stream.write_all(b"Hello, wtransport!").await?;
//! # Ok(())
//! # }
//! ```
//!
//! #### Accept a stream
//! ```no_run
//! # use anyhow::Result;
//! # async fn foo(connection: wtransport::Connection) -> Result<()> {
//! use wtransport::Connection;
//!
//! // Await the peer opens a bi-directional stream
//! let (mut send_stream, mut recv_stream) = connection.accept_bi().await?;
//!
//! // Can send and receive data on peer's stream
//! send_stream.write_all(b"Hello, wtransport!").await?;
//! # let mut buffer = vec![0; 1024];
//! let bytes_read = recv_stream.read(&mut buffer).await?;
//!
//! // Await the peer opens an uni-directional stream (can only receive data)
//! let mut recv_stream = connection.accept_uni().await?;
//!
//! // Receive data on the stream
//! let bytes_read = recv_stream.read(&mut buffer).await?;
//! # Ok(())
//! # }
//! ```
//!
//! ## Datagrams
//! WebTransport datagrams are similar to UDP datagrams but come with an
//! added layer of security through *encryption* and *congestion control*.
//! Datagrams can arrive out of order or might not arrive at all, offering
//! flexibility in data exchange scenarios.
//!
//! Unlike streams, which operate as byte-stream abstractions, WebTransport
//! datagrams act more like messages.
//!
//! ### Examples
//! ```no_run
//! # use anyhow::Result;
//! # async fn foo(connection: wtransport::Connection) -> Result<()> {
//! use wtransport::Connection;
//!
//! // Send datagram message
//! connection.send_datagram(b"Hello, wtransport!")?;
//!
//! // Receive a datagram message
//! let message = connection.receive_datagram().await?;
//! # Ok(())
//! # }
//! ```
use crate::datagram::Datagram;
use crate::driver::utils::varint_w2q;
use crate::driver::Driver;
use crate::error::ConnectionError;
use crate::error::ExportKeyingMaterialError;
use crate::error::SendDatagramError;
use crate::stream::OpeningBiStream;
use crate::stream::OpeningUniStream;
use crate::stream::RecvStream;
use crate::stream::SendStream;
use crate::tls::Certificate;
use crate::tls::CertificateChain;
use crate::tls::HandshakeData;
use crate::SessionId;
use crate::VarInt;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
/// A WebTransport session connection.
///
/// For more details, see the [module documentation](crate::connection).
///
/// May be cloned to obtain another handle to the same connection.
#[derive(Clone, Debug)]
pub struct Connection {
quic_connection: quinn::Connection,
driver: Arc<Driver>,
session_id: SessionId,
}
impl Connection {
pub(crate) fn new(
quic_connection: quinn::Connection,
driver: Driver,
session_id: SessionId,
) -> Self {
Self {
quic_connection,
driver: Arc::new(driver),
session_id,
}
}
/// Asynchronously accepts a unidirectional stream.
///
/// This method is used to accept incoming unidirectional streams that have been initiated
/// by the remote peer.
/// It waits for the next unidirectional stream to be available, then wraps it in a
/// [`RecvStream`] that can be used to read data from the stream.
///
/// # Cancel safety
///
/// This method is cancel safe.
pub async fn accept_uni(&self) -> Result<RecvStream, ConnectionError> {
let stream = self
.driver
.accept_uni(self.session_id)
.await
.map_err(|driver_error| {
ConnectionError::with_driver_error(driver_error, &self.quic_connection)
})?
.into_stream();
Ok(RecvStream::new(stream))
}
/// Asynchronously accepts a bidirectional stream.
///
/// This method is used to accept incoming bidirectional streams that have been initiated
/// by the remote peer.
/// It waits for the next bidirectional stream to be available, then wraps it in a
/// tuple containing a [`SendStream`] for sending data and a [`RecvStream`] for receiving
/// data on the stream.
///
/// # Cancel safety
///
/// This method is cancel safe.
pub async fn accept_bi(&self) -> Result<(SendStream, RecvStream), ConnectionError> {
let stream = self
.driver
.accept_bi(self.session_id)
.await
.map_err(|driver_error| {
ConnectionError::with_driver_error(driver_error, &self.quic_connection)
})?
.into_stream();
Ok((SendStream::new(stream.0), RecvStream::new(stream.1)))
}
/// Asynchronously opens a new unidirectional stream.
///
/// This method is used to initiate the opening of a new unidirectional stream.
///
/// # Asynchronous Behavior
///
/// This method is asynchronous and involves two `await` points:
///
/// 1. The first `await` occurs during the initial phase of opening the stream, which may involve awaiting
/// the flow controller. This wait is necessary to ensure proper resource allocation and flow control.
/// It is safe to cancel this `await` point if needed.
///
/// 2. The second `await` is internal to the returned [`OpeningUniStream`] object when it is used to initialize
/// the WebTransport stream. Cancelling this latter future before it completes may result in the stream
/// being closed during initialization.
///
/// # Example
///
/// ```no_run
/// # use wtransport::Connection;
/// # use anyhow::Result;
/// # async fn run(connection: Connection) -> Result<()> {
/// let send_stream = connection.open_uni().await?.await?;
/// # Ok(())
/// # }
/// ```
pub async fn open_uni(&self) -> Result<OpeningUniStream, ConnectionError> {
self.driver
.open_uni(self.session_id)
.await
.map_err(|driver_error| {
ConnectionError::with_driver_error(driver_error, &self.quic_connection)
})
}
/// Asynchronously opens a new bidirectional stream.
///
/// This method is used to initiate the opening of a new bidirectional stream.
///
/// # Asynchronous Behavior
///
/// This method is asynchronous and involves two `await` points:
///
/// 1. The first `await` occurs during the initial phase of opening the stream, which may involve awaiting
/// the flow controller. This wait is necessary to ensure proper resource allocation and flow control.
/// It is safe to cancel this `await` point if needed.
///
/// 2. The second `await` is internal to the returned [`OpeningBiStream`] object when it is used to initialize
/// the WebTransport stream. Cancelling this latter future before it completes may result in the stream
/// being closed during initialization.
///
/// # Example
///
/// ```no_run
/// # use wtransport::Connection;
/// # use anyhow::Result;
/// # async fn run(connection: Connection) -> Result<()> {
/// let (send_stream, recv_stream) = connection.open_bi().await?.await?;
/// # Ok(())
/// # }
/// ```
pub async fn open_bi(&self) -> Result<OpeningBiStream, ConnectionError> {
self.driver
.open_bi(self.session_id)
.await
.map_err(|driver_error| {
ConnectionError::with_driver_error(driver_error, &self.quic_connection)
})
}
/// Asynchronously receives an application datagram from the remote peer.
///
/// This method is used to receive an application datagram sent by the remote
/// peer over the connection.
/// It waits for a datagram to become available and returns the received [`Datagram`].
///
/// # Example
///
/// ```no_run
/// # use wtransport::Connection;
/// # use anyhow::Result;
/// # async fn run(connection: Connection) -> Result<()> {
/// let datagram = connection.receive_datagram().await?;
/// # Ok(())
/// # }
/// ```
pub async fn receive_datagram(&self) -> Result<Datagram, ConnectionError> {
self.driver
.receive_datagram(self.session_id)
.await
.map_err(|driver_error| {
ConnectionError::with_driver_error(driver_error, &self.quic_connection)
})
}
/// Sends an application datagram to the remote peer.
///
/// This method is used to send an application datagram to the remote peer
/// over the connection.
/// The datagram payload is provided as a reference to a slice of bytes.
///
/// # Example
///
/// ```no_run
/// # use wtransport::Connection;
/// # use anyhow::Result;
/// # async fn run(connection: Connection) -> Result<()> {
/// connection.send_datagram(b"Hello, wtransport!")?;
/// # Ok(())
/// # }
/// ```
pub fn send_datagram<D>(&self, payload: D) -> Result<(), SendDatagramError>
where
D: AsRef<[u8]>,
{
self.driver.send_datagram(self.session_id, payload.as_ref())
}
/// Closes the connection immediately.
///
/// This closes the QUIC connection: the peer learns no more than that the session is
/// gone. To close the session with an application error code and reason the peer can
/// read, use [`close_session`](Self::close_session).
pub fn close(&self, error_code: VarInt, reason: &[u8]) {
self.quic_connection.close(varint_w2q(error_code), reason);
}
/// Closes the WebTransport session with a 32-bit application error code and a reason
/// (draft-ietf-webtrans-http3-16 §6), then the connection, which carries this session
/// alone.
///
/// A WT_CLOSE_SESSION capsule carrying `error_code` and `reason` (cut to 1024 bytes at
/// a character boundary) goes out on the session's CONNECT stream, with a FIN, ahead of
/// anything else queued. The session is over from here: this end opens no stream and
/// sends no datagram, and [`closed`](Self::closed) resolves with
/// [`ConnectionError::LocallyClosed`]. The connection is closed with H3_NO_ERROR once
/// the peer has closed its side of the CONNECT stream, as it must on reading the
/// capsule, or after two seconds, so that the connection's close cannot overtake the
/// capsule. The streams of the session end with the connection.
pub fn close_session(&self, error_code: u32, reason: &str) {
self.driver.close_session(error_code, reason);
}
/// Waits for the session to be closed for any reason.
///
/// A session the peer closed resolves with [`ConnectionError::SessionClosed`], its code
/// and reason, rather than with how the connection closed after it.
pub async fn closed(&self) -> ConnectionError {
match self.driver.closed().await {
Some(driver_error) => {
ConnectionError::with_driver_error(driver_error, &self.quic_connection)
}
None => self.quic_connection.closed().await.into(),
}
}
/// Returns the WebTransport session identifier.
#[inline(always)]
pub fn session_id(&self) -> SessionId {
self.session_id
}
/// Returns the peer's UDP address.
///
/// **Note**: as QUIC supports migration, remote address may change
/// during connection. Furthermore, when IPv6 support is enabled, IPv4
/// addresses may be mapped to IPv6.
#[inline(always)]
pub fn remote_address(&self) -> SocketAddr {
self.quic_connection.remote_address()
}
/// A stable identifier for this connection.
///
/// Peer addresses and connection IDs can change, but this value will remain
/// fixed for the lifetime of the connection.
#[inline(always)]
pub fn stable_id(&self) -> usize {
self.quic_connection.stable_id()
}
/// Computes the maximum size of datagrams that may be passed to
/// [`send_datagram`](Self::send_datagram).
///
/// Returns `None` if datagrams are unsupported by the peer or disabled locally.
///
/// This may change over the lifetime of a connection according to variation in the path MTU
/// estimate. The peer can also enforce an arbitrarily small fixed limit, but if the peer's
/// limit is large this is guaranteed to be a little over a kilobyte at minimum.
///
/// Not necessarily the maximum size of received datagrams.
#[inline(always)]
pub fn max_datagram_size(&self) -> Option<usize> {
self.quic_connection
.max_datagram_size()
.map(|quic_max_size| quic_max_size - Datagram::header_size(self.session_id))
}
/// Current best estimate of this connection's latency (round-trip-time).
#[inline(always)]
pub fn rtt(&self) -> Duration {
self.quic_connection.rtt()
}
/// Derive keying material from this connection's TLS session secrets.
///
/// When both peers call this method with the same `label` and `context`
/// arguments and `output` buffers of equal length, they will get the
/// same sequence of bytes in `output`. These bytes are cryptographically
/// strong and pseudorandom, and are suitable for use as keying material.
///
/// See [RFC5705](https://tools.ietf.org/html/rfc5705) for more information.
pub fn export_keying_material(
&self,
output: &mut [u8],
label: &[u8],
context: &[u8],
) -> Result<(), ExportKeyingMaterialError> {
self.quic_connection
.export_keying_material(output, label, context)
.map_err(|_: quinn::crypto::ExportKeyingMaterialError| ExportKeyingMaterialError)
}
/// Returns the peer's identity as a certificate chain if available.
///
/// This function returns an `Option` containing a [`CertificateChain`]. If the peer's identity
/// is available, it is converted into a `CertificateChain` and returned. If the peer's identity
/// is not available, `None` is returned.
pub fn peer_identity(&self) -> Option<CertificateChain> {
self.quic_connection.peer_identity().map(|any| {
any.downcast::<Vec<rustls_pki_types::CertificateDer<'static>>>()
.expect("rustls certificate vector")
.into_iter()
.map(Certificate::from_rustls_pki)
.collect()
})
}
/// Retrieves handshake data associated with the connection.
pub fn handshake_data(&self) -> HandshakeData {
let hd = self
.quic_connection
.handshake_data()
.expect("fully established connection")
.downcast::<quinn::crypto::rustls::HandshakeData>()
.expect("valid downcast");
HandshakeData {
alpn: hd.protocol,
server_name: hd.server_name,
}
}
/// Returns a reference to the inner QUIC connection.
#[cfg(feature = "quinn")]
#[cfg_attr(docsrs, doc(cfg(feature = "quinn")))]
#[inline(always)]
pub fn quic_connection(&self) -> &quinn::Connection {
&self.quic_connection
}
/// Returns a mutable reference to the inner QUIC connection.
#[cfg(feature = "quinn")]
#[cfg_attr(docsrs, doc(cfg(feature = "quinn")))]
#[inline(always)]
pub fn quic_connection_mut(&mut self) -> &mut quinn::Connection {
&mut self.quic_connection
}
}

View file

@ -0,0 +1,76 @@
use crate::SessionId;
use bytes::Bytes;
use std::ops::Deref;
use wtransport_proto::datagram::Datagram as H3Datagram;
use wtransport_proto::error::ErrorCode;
use wtransport_proto::ids::QStreamId;
/// An application Datagram.
#[derive(Debug)]
pub struct Datagram {
quic_dgram: Bytes,
payload_offset: usize,
session_id: SessionId,
}
impl Datagram {
/// Returns the datagram payload.
#[inline(always)]
pub fn payload(&self) -> Bytes {
self.quic_dgram.slice(self.payload_offset..)
}
pub(crate) fn read(quic_dgram: Bytes) -> Result<Self, ErrorCode> {
let h3dgram = H3Datagram::read(&quic_dgram)?;
let payload_offset = quic_dgram.len() - h3dgram.payload().len();
let session_id = h3dgram.qstream_id().into_session_id();
Ok(Self {
quic_dgram,
payload_offset,
session_id,
})
}
pub(crate) fn write(session_id: SessionId, payload: &[u8]) -> Self {
let h3dgram = H3Datagram::new(QStreamId::from_session_id(session_id), payload);
let mut buffer = vec![0; h3dgram.write_size()].into_boxed_slice();
h3dgram.write(&mut buffer).expect("Preallocated capacity");
let quic_dgram = Bytes::from(buffer);
let payload_offset = quic_dgram.len() - payload.len();
Self {
quic_dgram,
payload_offset,
session_id,
}
}
#[inline(always)]
pub(crate) fn header_size(session_id: SessionId) -> usize {
H3Datagram::header_size(QStreamId::from_session_id(session_id))
}
/// Returns the associated [`SessionId`].
#[inline(always)]
pub fn session_id(&self) -> SessionId {
self.session_id
}
#[inline(always)]
pub(crate) fn into_quic_bytes(self) -> Bytes {
self.quic_dgram
}
}
impl Deref for Datagram {
type Target = [u8];
#[inline(always)]
fn deref(&self) -> &Self::Target {
&self.quic_dgram[self.payload_offset..]
}
}

View file

@ -0,0 +1,763 @@
use crate::datagram::Datagram;
use crate::driver::streams::biremote::StreamBiRemoteH3;
use crate::driver::streams::biremote::StreamBiRemoteWT;
use crate::driver::streams::session::StreamSession;
use crate::driver::streams::uniremote::StreamUniRemoteWT;
use crate::driver::streams::Stream;
use crate::driver::utils::bichannel;
use crate::driver::utils::shared_result;
use crate::driver::utils::SendError;
use crate::driver::utils::SharedResultGet;
use crate::driver::utils::SharedResultSet;
use crate::error::ApplicationClose;
use crate::error::SendDatagramError;
use crate::stream::OpeningBiStream;
use crate::stream::OpeningUniStream;
use crate::SessionId;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::time::Duration;
use tokio::sync::mpsc;
use tokio::sync::Mutex;
use tracing::debug;
use tracing::debug_span;
use tracing::instrument;
use tracing::trace;
use tracing::Instrument;
use utils::BiChannelEndpoint;
use wtransport_proto::error::ErrorCode;
use wtransport_proto::frame::Frame;
use wtransport_proto::session::SessionRequest;
use wtransport_proto::settings::Settings;
/// How long a session closed by this end waits for the peer to close its side of the
/// CONNECT stream before the connection is closed (see [`Driver::close_session`]).
const SESSION_CLOSE_LINGER: Duration = Duration::from_secs(2);
#[derive(Clone, Debug)]
pub enum DriverError {
Proto(ErrorCode),
ApplicationClosed(ApplicationClose),
/// This endpoint closed the session ([`Driver::close_session`]).
LocallyClosed,
NotConnected,
}
#[derive(Debug)]
pub struct Driver {
quic_connection: quinn::Connection,
ready_settings: Mutex<mpsc::Receiver<Settings>>,
ready_sessions: BiChannelEndpoint<StreamSession>,
ready_uni_wt_streams: Mutex<mpsc::Receiver<StreamUniRemoteWT>>,
ready_bi_wt_streams: Mutex<mpsc::Receiver<StreamBiRemoteWT>>,
ready_datagrams: Mutex<mpsc::Receiver<Datagram>>,
driver_result: SharedResultGet<DriverError>,
close_requests: mpsc::UnboundedSender<(u32, String)>,
session_closing: AtomicBool,
}
impl Driver {
pub fn init(quic_connection: quinn::Connection) -> Self {
let ready_settings = mpsc::channel(1);
let ready_sessions = bichannel(1);
let ready_uni_wt_streams = mpsc::channel(4);
let ready_bi_wt_streams = mpsc::channel(1);
let ready_datagrams = mpsc::channel(1);
let driver_result = shared_result();
let close_requests = mpsc::unbounded_channel();
tokio::spawn(
worker::Worker::new(
quic_connection.clone(),
ready_settings.0,
ready_sessions.0,
ready_uni_wt_streams.0,
ready_bi_wt_streams.0,
ready_datagrams.0,
driver_result.0,
close_requests.1,
)
.run()
.instrument(debug_span!("Driver", quic_id = quic_connection.stable_id())),
);
Self {
quic_connection,
ready_settings: Mutex::new(ready_settings.1),
ready_sessions: ready_sessions.1,
ready_uni_wt_streams: Mutex::new(ready_uni_wt_streams.1),
ready_bi_wt_streams: Mutex::new(ready_bi_wt_streams.1),
ready_datagrams: Mutex::new(ready_datagrams.1),
driver_result: driver_result.1,
close_requests: close_requests.0,
session_closing: AtomicBool::new(false),
}
}
/// Closes the session from this end: the worker sends WT_CLOSE_SESSION with `error_code`
/// and `reason` on the CONNECT stream, then closes the connection (see
/// [`Connection::close_session`](crate::Connection::close_session)). From here this end
/// opens no stream and sends no datagram (draft-ietf-webtrans-http3-16 §6).
pub fn close_session(&self, error_code: u32, reason: &str) {
self.session_closing.store(true, Ordering::Release);
// An error means the worker has ended, and the session with it.
let _ = self.close_requests.send((error_code, reason.to_owned()));
}
/// Why the worker ended, once it has; `None` if it stopped without saying.
pub async fn closed(&self) -> Option<DriverError> {
self.driver_result.result().await
}
fn closing(&self) -> bool {
self.session_closing.load(Ordering::Acquire)
}
pub async fn accept_settings(&self) -> Result<Settings, DriverError> {
let mut lock = self.ready_settings.lock().await;
match lock.recv().await {
Some(settings) => Ok(settings),
None => Err(self.result().await),
}
}
pub async fn accept_session(&self) -> Result<StreamSession, DriverError> {
match self.ready_sessions.recv().await {
Some(session) => Ok(session),
None => Err(self.result().await),
}
}
pub async fn open_session(
&self,
session_request: SessionRequest,
) -> Result<StreamSession, DriverError> {
let stream = Stream::open_bi(&self.quic_connection)
.await
.ok_or(DriverError::NotConnected)?
.upgrade()
.into_session(session_request);
Ok(stream)
}
pub async fn register_session(&self, stream_session: StreamSession) -> Result<(), DriverError> {
match self.ready_sessions.send(stream_session).await {
Ok(()) => Ok(()),
Err(SendError) => Err(self.result().await),
}
}
pub async fn accept_uni(
&self,
session_id: SessionId,
) -> Result<StreamUniRemoteWT, DriverError> {
let mut lock = self.ready_uni_wt_streams.lock().await;
loop {
let Some(stream) = lock.recv().await else {
return Err(self.result().await);
};
if stream.session_id() == session_id {
return Ok(stream);
}
debug!(
"Discarding WT stream (stream_id: {}, session_id: {})",
stream.id(),
stream.session_id()
);
stream
.into_stream()
.stop(ErrorCode::BufferedStreamRejected.to_code())
.expect("Stream not already stopped");
}
}
pub async fn accept_bi(&self, session_id: SessionId) -> Result<StreamBiRemoteWT, DriverError> {
let mut lock = self.ready_bi_wt_streams.lock().await;
loop {
let Some(stream) = lock.recv().await else {
return Err(self.result().await);
};
if stream.session_id() == session_id {
return Ok(stream);
}
debug!(
"Discarding WT stream (stream_id: {}, session_id: {})",
stream.id(),
stream.session_id()
);
stream
.into_stream()
.1
.stop(ErrorCode::BufferedStreamRejected.to_code())
.expect("Stream not already stopped");
}
}
pub async fn receive_datagram(&self, session_id: SessionId) -> Result<Datagram, DriverError> {
let mut lock = self.ready_datagrams.lock().await;
loop {
let Some(datagram) = lock.recv().await else {
return Err(self.result().await);
};
if datagram.session_id() == session_id {
return Ok(datagram);
}
debug!(
"Incoming datagram discarded (session_id: {})",
datagram.session_id()
);
}
}
pub async fn open_uni(&self, session_id: SessionId) -> Result<OpeningUniStream, DriverError> {
if self.closing() {
return Err(DriverError::LocallyClosed);
}
let quic_stream = Stream::open_uni(&self.quic_connection)
.await
.ok_or(DriverError::NotConnected)?;
Ok(OpeningUniStream::new(session_id, quic_stream))
}
pub async fn open_bi(&self, session_id: SessionId) -> Result<OpeningBiStream, DriverError> {
if self.closing() {
return Err(DriverError::LocallyClosed);
}
let quic_stream = Stream::open_bi(&self.quic_connection)
.await
.ok_or(DriverError::NotConnected)?;
Ok(OpeningBiStream::new(session_id, quic_stream))
}
pub fn send_datagram(
&self,
session_id: SessionId,
payload: &[u8],
) -> Result<(), SendDatagramError> {
if self.closing() {
return Err(SendDatagramError::NotConnected);
}
let quic_datagram = Datagram::write(session_id, payload).into_quic_bytes();
match self.quic_connection.send_datagram(quic_datagram) {
Ok(()) => Ok(()),
Err(quinn::SendDatagramError::UnsupportedByPeer) => {
Err(SendDatagramError::UnsupportedByPeer)
}
Err(quinn::SendDatagramError::Disabled) => {
unreachable!()
}
Err(quinn::SendDatagramError::TooLarge) => Err(SendDatagramError::TooLarge),
Err(quinn::SendDatagramError::ConnectionLost(_)) => {
Err(SendDatagramError::NotConnected)
}
}
}
async fn result(&self) -> DriverError {
match self.driver_result.result().await {
Some(error) => error,
None => panic!("Driver worker panic!"),
}
}
}
mod worker {
use super::*;
use crate::driver::streams::qpack::RemoteQPackDecStream;
use crate::driver::streams::qpack::RemoteQPackEncStream;
use crate::driver::streams::settings::LocalSettingsStream;
use crate::driver::streams::settings::RemoteSettingsStream;
use crate::driver::streams::uniremote::StreamUniRemoteH3;
use crate::driver::streams::ProtoReadError;
use crate::driver::streams::ProtoWriteError;
use crate::driver::utils::TrySendError;
use streams::connect::ConnectStream;
use utils::varint_w2q;
use wtransport_proto::frame::FrameKind;
use wtransport_proto::headers::Headers;
use wtransport_proto::session::HeadersParseError;
use wtransport_proto::stream_header::StreamHeader;
use wtransport_proto::stream_header::StreamKind;
pub struct Worker {
quic_connection: quinn::Connection,
ready_settings: mpsc::Sender<Settings>,
ready_sessions: BiChannelEndpoint<StreamSession>,
ready_uni_wt_streams: mpsc::Sender<StreamUniRemoteWT>,
ready_bi_wt_streams: mpsc::Sender<StreamBiRemoteWT>,
ready_datagrams: mpsc::Sender<Datagram>,
driver_result: SharedResultSet<DriverError>,
local_settings_stream: LocalSettingsStream,
remote_settings_stream: RemoteSettingsStream,
remote_qpack_enc_stream: RemoteQPackEncStream,
remote_qpack_dec_stream: RemoteQPackDecStream,
connect_stream: ConnectStream,
close_requests: mpsc::UnboundedReceiver<(u32, String)>,
}
impl Worker {
pub fn new(
quic_connection: quinn::Connection,
ready_settings: mpsc::Sender<Settings>,
ready_sessions: BiChannelEndpoint<StreamSession>,
ready_uni_wt_streams: mpsc::Sender<StreamUniRemoteWT>,
ready_bi_wt_streams: mpsc::Sender<StreamBiRemoteWT>,
ready_datagrams: mpsc::Sender<Datagram>,
driver_result: SharedResultSet<DriverError>,
close_requests: mpsc::UnboundedReceiver<(u32, String)>,
) -> Self {
Self {
quic_connection,
ready_settings,
ready_sessions,
ready_uni_wt_streams,
ready_bi_wt_streams,
ready_datagrams,
driver_result,
local_settings_stream: LocalSettingsStream::empty(),
remote_settings_stream: RemoteSettingsStream::empty(),
remote_qpack_enc_stream: RemoteQPackEncStream::empty(),
remote_qpack_dec_stream: RemoteQPackDecStream::empty(),
connect_stream: ConnectStream::empty(),
close_requests,
}
}
pub async fn run(mut self) {
debug!("Started");
let mut error = self
.run_impl()
.await
.expect_err("Worker must return an error");
// A close asked for as the session's last handle was dropped: the loop can see
// the handles gone first. The close still goes out.
if matches!(error, DriverError::NotConnected)
&& self.quic_connection.close_reason().is_none()
{
if let Ok((error_code, reason)) = self.close_requests.try_recv() {
error = self.close_session(error_code, &reason).await;
}
}
debug!("Ended with error: {:?}", error);
match &error {
DriverError::ApplicationClosed(_) | DriverError::LocallyClosed => {
// Termination procedure
// TODO Reset send sides of all streams with SessionGone error
self.quic_connection
.close(varint_w2q(ErrorCode::NoError.to_code()), b"");
}
DriverError::Proto(error_code) => {
self.quic_connection
.close(varint_w2q(error_code.to_code()), b"");
}
DriverError::NotConnected => (),
}
self.driver_result.set(error);
}
async fn run_impl(&mut self) -> Result<(), DriverError> {
let mut remote_settings_watcher = self.remote_settings_stream.subscribe();
let mut ready_uni_h3_streams = mpsc::channel(4);
let mut ready_bi_h3_streams = mpsc::channel(1);
self.open_and_send_settings().await?;
loop {
tokio::select! {
result = Self::accept_uni(&self.quic_connection,
&ready_uni_h3_streams.0,
&self.ready_uni_wt_streams) => {
result?;
}
result = Self::accept_bi(&self.quic_connection,
&ready_bi_h3_streams.0,
&self.ready_bi_wt_streams) => {
result?;
}
result = Self::accept_datagram(&self.quic_connection,
&self.ready_datagrams) => {
result?;
}
uni_h3_stream = ready_uni_h3_streams.1.recv() => {
let uni_h3_stream = uni_h3_stream.expect("Sender cannot be dropped")?;
self.handle_uni_h3_stream(uni_h3_stream)?;
}
bi_h3_stream = ready_bi_h3_streams.1.recv() => {
let (bi_h3_stream, first_frame) = bi_h3_stream.expect("Sender cannot be dropped")?;
self.handle_bi_h3_stream(bi_h3_stream, first_frame)?;
}
settings = remote_settings_watcher.accept_settings() => {
let settings = settings.expect("Channel cannot be dropped");
self.handle_remote_settings(settings)?;
}
stream_session = self.ready_sessions.recv() => {
match stream_session {
Some(stream_session) => {
if self.connect_stream.is_empty() {
self.connect_stream.set_stream(stream_session);
}
}
None => return Err(DriverError::NotConnected),
};
}
error = Self::run_control_streams(&mut self.local_settings_stream,
&mut self.remote_settings_stream,
&mut self.remote_qpack_enc_stream,
&mut self.remote_qpack_dec_stream,
&mut self.connect_stream) => {
return Err(error);
}
Some((error_code, reason)) = self.close_requests.recv() => {
return Err(self.close_session(error_code, &reason).await);
}
() = self.driver_result.closed() => {
return Err(DriverError::NotConnected);
}
}
}
}
/// Closes the session from this end (draft-ietf-webtrans-http3-16 §6): WT_CLOSE_SESSION
/// and a FIN on the CONNECT stream, then a wait of at most [`SESSION_CLOSE_LINGER`]
/// for the peer to close its side. The session is over from here: its result is set
/// before the capsule is written; [`run`](Self::run) closes the connection after the
/// wait.
async fn close_session(&mut self, error_code: u32, reason: &str) -> DriverError {
// `register_session` hands the CONNECT stream over before the session's
// `Connection` exists, but this loop may not have taken it yet.
if self.connect_stream.is_empty() {
let registered =
tokio::time::timeout(SESSION_CLOSE_LINGER, self.ready_sessions.recv()).await;
if let Ok(Some(stream)) = registered {
self.connect_stream.set_stream(stream);
}
}
self.driver_result.set(DriverError::LocallyClosed);
self.connect_stream
.close(error_code, reason, SESSION_CLOSE_LINGER)
.await;
DriverError::LocallyClosed
}
async fn open_and_send_settings(&mut self) -> Result<(), DriverError> {
assert!(self.local_settings_stream.is_empty());
let stream = match Stream::open_uni(&self.quic_connection)
.await
.ok_or(DriverError::NotConnected)?
.upgrade(StreamHeader::new_control())
.await
{
Ok(h3_stream) => h3_stream,
Err(ProtoWriteError::NotConnected) => return Err(DriverError::NotConnected),
Err(ProtoWriteError::Stopped) => {
return Err(DriverError::Proto(ErrorCode::ClosedCriticalStream));
}
};
self.local_settings_stream.set_stream(stream);
self.local_settings_stream.send_settings().await
}
async fn accept_uni(
quic_connection: &quinn::Connection,
ready_uni_h3_streams: &mpsc::Sender<Result<StreamUniRemoteH3, DriverError>>,
ready_uni_wt_streams: &mpsc::Sender<StreamUniRemoteWT>,
) -> Result<(), DriverError> {
trace!("H3 uni queue capacity: {}", ready_uni_h3_streams.capacity());
let h3_slot = ready_uni_h3_streams
.clone()
.reserve_owned()
.await
.expect("Receiver cannot be dropped");
let wt_slot = match ready_uni_wt_streams.clone().reserve_owned().await {
Ok(wt_slot) => wt_slot,
Err(mpsc::error::SendError(_)) => return Err(DriverError::NotConnected),
};
let stream_quic = Stream::accept_uni(quic_connection)
.await
.ok_or(DriverError::NotConnected)?;
let stream_id = stream_quic.id();
debug!("New incoming uni stream ({})", stream_id);
tokio::spawn(
async move {
let stream_h3 = match stream_quic.upgrade().await {
Ok(stream_h3) => stream_h3,
Err(ProtoReadError::H3(error_code)) => {
h3_slot.send(Err(DriverError::Proto(error_code)));
return;
}
Err(ProtoReadError::IO(_)) => {
return;
}
};
let stream_kind = stream_h3.kind();
debug!("Type: {:?}", stream_kind);
if matches!(stream_kind, StreamKind::WebTransport) {
let stream_wt = stream_h3.upgrade();
wt_slot.send(stream_wt);
} else {
h3_slot.send(Ok(stream_h3));
}
}
.instrument(debug_span!("Stream", "id={}", stream_id)),
);
Ok(())
}
async fn accept_bi(
quic_connection: &quinn::Connection,
ready_bi_h3_streams: &mpsc::Sender<
Result<(StreamBiRemoteH3, Frame<'static>), DriverError>,
>,
ready_bi_wt_streams: &mpsc::Sender<StreamBiRemoteWT>,
) -> Result<(), DriverError> {
trace!("H3 bi queue capacity: {}", ready_bi_h3_streams.capacity());
let h3_slot = ready_bi_h3_streams
.clone()
.reserve_owned()
.await
.expect("Receiver cannot be dropped");
let wt_slot = match ready_bi_wt_streams.clone().reserve_owned().await {
Ok(wt_slot) => wt_slot,
Err(mpsc::error::SendError(_)) => return Err(DriverError::NotConnected),
};
let stream_quic = Stream::accept_bi(quic_connection)
.await
.ok_or(DriverError::NotConnected)?;
let stream_id = stream_quic.id();
debug!("New incoming bi stream ({})", stream_id);
tokio::spawn(
async move {
let mut stream_h3 = stream_quic.upgrade();
let frame = loop {
match stream_h3.read_frame().await {
Ok(frame) => {
debug!("Frame kind: {:?}", frame.kind());
if !matches!(frame.kind(), FrameKind::Exercise(_)) {
break frame;
}
}
Err(ProtoReadError::H3(error_code)) => {
h3_slot.send(Err(DriverError::Proto(error_code)));
return;
}
Err(ProtoReadError::IO(_)) => {
return;
}
}
};
debug!("First frame: {:?}", frame);
match frame.session_id() {
Some(session_id) => {
let stream_wt = stream_h3.upgrade(session_id);
wt_slot.send(stream_wt);
}
None => {
h3_slot.send(Ok((stream_h3, frame)));
}
}
}
.instrument(debug_span!("Stream", "id={}", stream_id)),
);
Ok(())
}
async fn accept_datagram(
quic_connection: &quinn::Connection,
ready_datagrams: &mpsc::Sender<Datagram>,
) -> Result<(), DriverError> {
let slot = match ready_datagrams.reserve().await {
Ok(slot) => slot,
Err(mpsc::error::SendError(_)) => return Err(DriverError::NotConnected),
};
let quic_dgram = quic_connection
.read_datagram()
.await
.map_err(|_| DriverError::NotConnected)?;
let datagram = match Datagram::read(quic_dgram) {
Ok(datagram) => datagram,
Err(error_code) => return Err(DriverError::Proto(error_code)),
};
trace!(
"New incoming datagram (session_id: {})",
datagram.session_id()
);
slot.send(datagram);
Ok(())
}
fn handle_uni_h3_stream(&mut self, stream: StreamUniRemoteH3) -> Result<(), DriverError> {
match stream.kind() {
StreamKind::Control => {
if !self.remote_settings_stream.is_empty() {
return Err(DriverError::Proto(ErrorCode::StreamCreation));
}
self.remote_settings_stream.set_stream(stream);
}
StreamKind::QPackEncoder => {
if !self.remote_qpack_enc_stream.is_empty() {
return Err(DriverError::Proto(ErrorCode::StreamCreation));
}
self.remote_qpack_enc_stream.set_stream(stream);
}
StreamKind::QPackDecoder => {
if !self.remote_qpack_dec_stream.is_empty() {
return Err(DriverError::Proto(ErrorCode::StreamCreation));
}
self.remote_qpack_dec_stream.set_stream(stream);
}
StreamKind::WebTransport => unreachable!(),
StreamKind::Exercise(_) => {}
}
Ok(())
}
#[instrument(skip_all, name = "Stream", fields(id = %stream.id()))]
fn handle_bi_h3_stream(
&mut self,
mut stream: StreamBiRemoteH3,
first_frame: Frame<'static>,
) -> Result<(), DriverError> {
match first_frame.kind() {
FrameKind::Data => {
return Err(DriverError::Proto(ErrorCode::FrameUnexpected));
}
FrameKind::Headers => {
let headers = match Headers::with_frame(&first_frame) {
Ok(headers) => headers,
Err(error_code) => return Err(DriverError::Proto(error_code)),
};
debug!("Headers: {:?}", headers);
let stream_session = match SessionRequest::try_from(headers) {
Ok(session_request) => stream.into_session(session_request),
Err(HeadersParseError::MethodNotConnect) => {
stream
.stop(ErrorCode::RequestRejected.to_code())
.expect("Stream not already stopped");
return Ok(());
}
// TODO(biagio): we might have more granularity with errors
Err(_) => {
stream
.stop(ErrorCode::Message.to_code())
.expect("Stream not already stopped");
return Ok(());
}
};
match self.ready_sessions.try_send(stream_session) {
Ok(()) => {}
Err(TrySendError::Full(mut stream)) => {
debug!("Discarding session request: sessions queue is full");
stream
.stop(ErrorCode::RequestRejected.to_code())
.expect("Stream not already stopped");
}
Err(TrySendError::Closed(_)) => return Err(DriverError::NotConnected),
}
}
FrameKind::Settings => {
return Err(DriverError::Proto(ErrorCode::FrameUnexpected));
}
FrameKind::WebTransport => unreachable!(),
FrameKind::Exercise(_) => {}
}
Ok(())
}
async fn run_control_streams(
local_settings: &mut LocalSettingsStream,
remote_settings: &mut RemoteSettingsStream,
remote_qpack_enc: &mut RemoteQPackEncStream,
remote_qpack_dec: &mut RemoteQPackDecStream,
connect_stream: &mut ConnectStream,
) -> DriverError {
tokio::select! {
error = connect_stream.run() => error,
error = local_settings.run() => error,
error = remote_settings.run() => error,
error = remote_qpack_enc.run() => error,
error = remote_qpack_dec.run() => error,
}
}
fn handle_remote_settings(&mut self, settings: Settings) -> Result<(), DriverError> {
debug!("Received: {:?}", settings);
match self.ready_settings.try_send(settings) {
Ok(()) => Ok(()),
Err(mpsc::error::TrySendError::Closed(_)) => Err(DriverError::NotConnected),
Err(mpsc::error::TrySendError::Full(_)) => {
unreachable!("No more than 1 setting frame can be processed")
}
}
}
}
}
pub(crate) mod streams;
pub(crate) mod utils;

View file

@ -0,0 +1,230 @@
use std::time::Duration;
use tracing::debug;
use wtransport_proto::bytes::BytesWriter;
use wtransport_proto::bytes::IoReadError;
use wtransport_proto::capsule::capsules;
use wtransport_proto::capsule::Capsule;
use wtransport_proto::capsule::{self};
use wtransport_proto::error::ErrorCode;
use wtransport_proto::frame::Frame;
use wtransport_proto::frame::FrameKind;
use wtransport_proto::varint::VarInt;
use super::session::StreamSession;
use super::ProtoReadError;
use crate::driver::DriverError;
use crate::error::ApplicationClose;
use std::future::pending;
pub struct ConnectStream {
stream: Option<StreamSession>,
}
impl ConnectStream {
pub fn empty() -> Self {
Self { stream: None }
}
pub fn is_empty(&self) -> bool {
self.stream.is_none()
}
pub fn set_stream(&mut self, stream: StreamSession) {
self.stream = Some(stream);
}
/// Closes the session from this end (draft-ietf-webtrans-http3-16 §6): one DATA frame
/// carrying a WT_CLOSE_SESSION capsule, then a FIN, on the CONNECT stream, ahead of
/// anything else queued on the connection. Then it waits, at most `linger`, for the
/// peer to close its side of the CONNECT stream, as it must on receiving the capsule:
/// the connection's close, which comes after, cannot then overtake the capsule.
pub async fn close(&mut self, error_code: u32, reason: &str, linger: Duration) {
let Some(mut stream) = self.stream.take() else {
return;
};
let frame = Frame::new_data(close_session_capsule(error_code, reason).into());
let _ = tokio::time::timeout(linger, async move {
stream.set_priority(i32::MAX);
if stream.write_frame(frame).await.is_err() {
return;
}
stream.finish().await;
// Until the peer's FIN or reset, or the connection's end.
while stream.read_frame().await.is_ok() {}
})
.await;
}
pub async fn run(&mut self) -> DriverError {
let stream = match self.stream.as_mut() {
Some(stream) => stream,
None => pending().await,
};
loop {
return match stream.read_frame().await {
Ok(frame) => {
if !matches!(frame.kind(), FrameKind::Data) {
debug!("Skipping non-data frame of kind {:?}", frame.kind());
continue;
}
let capsule = match Capsule::with_frame(&frame) {
Some(capsule)
if matches!(
capsule.kind(),
capsule::CapsuleKind::CloseWebTransportSession
) =>
{
capsule
}
// Unknown capsule, skip it
_ => {
debug!(
"Unknown capsule of kind {:?} of {}B",
frame.kind(),
frame.payload().len()
);
continue;
}
};
let close_session =
match capsules::CloseWebTransportSession::with_capsule(&capsule) {
Ok(close_session) => close_session,
Err(error_code) => return DriverError::Proto(error_code),
};
// reset right away to avoid receiving additional data which requires resetting with ErrorCode::Message.
self.stream
.take()
.unwrap()
.reset(ErrorCode::NoError.to_code());
DriverError::ApplicationClosed(ApplicationClose::new(
close_session.error_code(),
close_session
.reason()
.as_bytes()
.to_vec()
.into_boxed_slice(),
))
}
Err(ProtoReadError::H3(error_code)) => DriverError::Proto(error_code),
Err(ProtoReadError::IO(io_error)) => match io_error {
// Cleanly terminating a CONNECT stream without a CLOSE_WEBTRANSPORT_SESSION
// capsule SHALL be semantically equivalent to terminating it with a
// CLOSE_WEBTRANSPORT_SESSION capsule that has an error code of 0 and an empty error string.
IoReadError::ImmediateFin => DriverError::ApplicationClosed(
ApplicationClose::new(VarInt::from_u32(0), Box::new([])),
),
IoReadError::UnexpectedFin | IoReadError::Reset => {
DriverError::Proto(ErrorCode::ClosedCriticalStream)
}
IoReadError::NotConnected => DriverError::NotConnected,
},
};
}
}
}
/// The longest Application Error Message a WT_CLOSE_SESSION capsule may carry, in bytes
/// (draft-ietf-webtrans-http3-16 §6).
pub const MAX_CLOSE_REASON: usize = 1024;
/// The payload of the DATA frame that carries a WT_CLOSE_SESSION capsule
/// (draft-ietf-webtrans-http3-16 §6, in the capsule format of RFC 9297 §3.2):
///
/// ```text
/// WT_CLOSE_SESSION Capsule {
/// Type (i) = WT_CLOSE_SESSION (0x2843),
/// Length (i),
/// Application Error Code (32),
/// Application Error Message (..8192),
/// }
/// ```
///
/// A reason longer than [`MAX_CLOSE_REASON`] bytes is cut at the last UTF-8 character
/// boundary within them, as the draft requires of a sender that truncates.
pub fn close_session_capsule(error_code: u32, reason: &str) -> Vec<u8> {
let mut end = reason.len().min(MAX_CLOSE_REASON);
while !reason.is_char_boundary(end) {
end -= 1;
}
let reason = &reason.as_bytes()[..end];
let mut capsule = Vec::with_capacity(2 + 2 + 4 + reason.len());
let length = VarInt::from_u32(4 + reason.len() as u32);
capsule
.put_varint(VarInt::from_u32(0x2843))
.and_then(|()| capsule.put_varint(length))
.and_then(|()| capsule.put_bytes(&error_code.to_be_bytes()))
.and_then(|()| capsule.put_bytes(reason))
.expect("a Vec grows to fit");
capsule
}
#[cfg(test)]
mod tests {
use super::*;
use wtransport_proto::capsule::Capsule;
/// The frame's bytes as they go on the CONNECT stream.
fn frame_bytes(error_code: u32, reason: &str) -> Vec<u8> {
let mut out = Vec::new();
Frame::new_data(close_session_capsule(error_code, reason).into())
.write(&mut out)
.unwrap();
out
}
#[test]
fn the_capsule_is_laid_out_as_the_draft_gives() {
// DATA (0x00) of 18 bytes: WT_CLOSE_SESSION, 0x2843 as a two-byte varint (0x68 0x43);
// its length, 4 + 11; the code, 32 bits big-endian; the message.
let mut want = vec![0x00, 0x12, 0x68, 0x43, 0x0f, 0x00, 0x00, 0x00, 0x02];
want.extend_from_slice(b"pop_missing");
assert_eq!(frame_bytes(2, "pop_missing"), want);
// Code 0 and no message: the capsule is the type, a length of 4, and the code.
assert_eq!(
frame_bytes(0, ""),
[0x00, 0x07, 0x68, 0x43, 0x04, 0x00, 0x00, 0x00, 0x00]
);
// The whole 32-bit code space.
assert_eq!(
&frame_bytes(u32::MAX, "")[5..],
[0xff, 0xff, 0xff, 0xff]
);
}
#[test]
fn a_long_message_is_cut_at_a_character_boundary_within_1024_bytes() {
// 1023 bytes of ASCII, then a two-byte character that would end at byte 1025.
let reason = format!("{}é tail", "a".repeat(1023));
let capsule = close_session_capsule(7, &reason);
// The type (2 bytes), then a length of 4 + 1023 = 1027 as a two-byte varint.
assert_eq!(&capsule[..4], [0x68, 0x43, 0x44, 0x03]);
assert_eq!(&capsule[4..8], 7u32.to_be_bytes());
assert_eq!(&capsule[8..], "a".repeat(1023).as_bytes());
let exact = "b".repeat(1024);
assert_eq!(close_session_capsule(7, &exact).len(), 2 + 2 + 4 + 1024);
assert_eq!(close_session_capsule(7, &"c".repeat(5000)).len(), 2 + 2 + 4 + 1024);
}
#[test]
fn the_capsule_reads_back_as_the_code_and_message() {
let frame = Frame::new_data(close_session_capsule(0x10, "GOAWAY timeout").into());
let capsule = Capsule::with_frame(&frame).expect("a capsule");
assert!(matches!(
capsule.kind(),
capsule::CapsuleKind::CloseWebTransportSession
));
let close = capsules::CloseWebTransportSession::with_capsule(&capsule).expect("a close");
assert_eq!(close.error_code(), VarInt::from_u32(0x10));
assert_eq!(close.reason(), "GOAWAY timeout");
}
}

View file

@ -0,0 +1,592 @@
use crate::driver::utils::streamid_q2w;
use crate::driver::utils::varint_q2w;
use crate::driver::utils::varint_w2q;
use crate::error::ClosedStream;
use crate::error::StreamReadError;
use crate::error::StreamReadExactError;
use crate::error::StreamWriteError;
use crate::SessionId;
use crate::StreamId;
use crate::VarInt;
use std::pin::Pin;
use std::task::ready;
use std::task::Context;
use std::task::Poll;
use tokio::io::ReadBuf;
use wtransport_proto::frame::Frame;
use wtransport_proto::session::SessionRequest;
use wtransport_proto::stream as stream_proto;
use wtransport_proto::stream::Stream as StreamProto;
use wtransport_proto::stream_header::StreamHeader;
use wtransport_proto::stream_header::StreamKind;
pub type ProtoReadError = wtransport_proto::stream::IoReadError;
pub type ProtoWriteError = wtransport_proto::stream::IoWriteError;
#[derive(Debug)]
pub struct AlreadyStop;
#[derive(Debug)]
pub struct QuicSendStream(quinn::SendStream);
impl QuicSendStream {
#[inline(always)]
pub async fn write(&mut self, buf: &[u8]) -> Result<usize, StreamWriteError> {
let written = self.0.write(buf).await?;
Ok(written)
}
#[inline(always)]
pub async fn write_all(&mut self, buf: &[u8]) -> Result<(), StreamWriteError> {
self.0.write_all(buf).await?;
Ok(())
}
#[inline(always)]
pub async fn finish(&mut self) -> Result<(), StreamWriteError> {
let _ = self.0.finish();
let result = self.stopped().await;
if matches!(result, StreamWriteError::Closed) {
Ok(())
} else {
Err(result)
}
}
#[inline(always)]
pub fn set_priority(&self, priority: i32) {
let _ = self.0.set_priority(priority);
}
#[inline(always)]
/// # Panics
///
/// If `reset` was called.
pub fn priority(&self) -> i32 {
self.0.priority().expect("Stream has been reset")
}
pub async fn stopped(&mut self) -> StreamWriteError {
match self.0.stopped().await {
Ok(None) => StreamWriteError::Closed,
Ok(Some(code)) => StreamWriteError::Stopped(varint_q2w(code)),
Err(quinn::StoppedError::ConnectionLost(_)) => StreamWriteError::NotConnected,
Err(quinn::StoppedError::ZeroRttRejected) => StreamWriteError::QuicProto,
}
}
#[inline(always)]
pub fn reset(&mut self, error_code: VarInt) -> Result<(), ClosedStream> {
self.0
.reset(varint_w2q(error_code))
.map_err(|_| ClosedStream)
}
#[inline(always)]
pub fn id(&self) -> StreamId {
streamid_q2w(self.0.id())
}
#[cfg(feature = "quinn")]
#[cfg_attr(docsrs, doc(cfg(feature = "quinn")))]
#[inline(always)]
pub fn quic_stream(&self) -> &quinn::SendStream {
&self.0
}
#[cfg(feature = "quinn")]
#[cfg_attr(docsrs, doc(cfg(feature = "quinn")))]
#[inline(always)]
pub fn quic_stream_mut(&mut self) -> &mut quinn::SendStream {
&mut self.0
}
}
impl wtransport_proto::bytes::AsyncWrite for QuicSendStream {
#[inline(always)]
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<std::io::Result<usize>> {
tokio::io::AsyncWrite::poll_write(Pin::new(&mut self.0), cx, buf)
}
}
impl tokio::io::AsyncWrite for QuicSendStream {
#[inline(always)]
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, std::io::Error>> {
tokio::io::AsyncWrite::poll_write(Pin::new(&mut self.0), cx, buf)
}
#[inline(always)]
fn poll_flush(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Result<(), std::io::Error>> {
tokio::io::AsyncWrite::poll_flush(Pin::new(&mut self.0), cx)
}
#[inline(always)]
fn poll_shutdown(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Result<(), std::io::Error>> {
tokio::io::AsyncWrite::poll_shutdown(Pin::new(&mut self.0), cx)
}
#[inline(always)]
fn poll_write_vectored(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
bufs: &[std::io::IoSlice<'_>],
) -> Poll<Result<usize, std::io::Error>> {
tokio::io::AsyncWrite::poll_write_vectored(Pin::new(&mut self.0), cx, bufs)
}
fn is_write_vectored(&self) -> bool {
tokio::io::AsyncWrite::is_write_vectored(&self.0)
}
}
#[derive(Debug)]
pub struct QuicRecvStream(quinn::RecvStream);
impl QuicRecvStream {
#[inline(always)]
pub async fn read(&mut self, buf: &mut [u8]) -> Result<Option<usize>, StreamReadError> {
match self.0.read(buf).await? {
Some(read) => Ok(Some(read)),
None => Ok(None),
}
}
#[inline(always)]
pub async fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), StreamReadExactError> {
self.0
.read_exact(buf)
.await
.map_err(|quic_error| match quic_error {
quinn::ReadExactError::FinishedEarly(read) => {
StreamReadExactError::FinishedEarly(read)
}
quinn::ReadExactError::ReadError(read) => StreamReadExactError::Read(read.into()),
})
}
#[inline(always)]
pub fn stop(&mut self, error_code: VarInt) -> Result<(), AlreadyStop> {
self.0.stop(varint_w2q(error_code)).map_err(|_| AlreadyStop)
}
#[inline(always)]
pub fn id(&self) -> StreamId {
streamid_q2w(self.0.id())
}
#[cfg(feature = "quinn")]
#[cfg_attr(docsrs, doc(cfg(feature = "quinn")))]
#[inline(always)]
pub fn quic_stream(&self) -> &quinn::RecvStream {
&self.0
}
#[cfg(feature = "quinn")]
#[cfg_attr(docsrs, doc(cfg(feature = "quinn")))]
#[inline(always)]
pub fn quic_stream_mut(&mut self) -> &mut quinn::RecvStream {
&mut self.0
}
}
impl wtransport_proto::bytes::AsyncRead for QuicRecvStream {
#[inline(always)]
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<std::io::Result<usize>> {
let mut buffer = ReadBuf::new(buf);
match ready!(tokio::io::AsyncRead::poll_read(
Pin::new(&mut self.0),
cx,
&mut buffer
)) {
Ok(()) => Poll::Ready(Ok(buffer.filled().len())),
Err(io_error) => Poll::Ready(Err(io_error)),
}
}
}
impl tokio::io::AsyncRead for QuicRecvStream {
#[inline(always)]
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
tokio::io::AsyncRead::poll_read(Pin::new(&mut self.0), cx, buf)
}
}
#[derive(Debug)]
pub struct Stream<S, P> {
stream: S,
proto: P,
}
pub mod biremote {
use super::*;
pub type StreamBiRemoteQuic =
Stream<(QuicSendStream, QuicRecvStream), stream_proto::biremote::StreamBiRemoteQuic>;
pub type StreamBiRemoteH3 =
Stream<(QuicSendStream, QuicRecvStream), stream_proto::biremote::StreamBiRemoteH3>;
pub type StreamBiRemoteWT =
Stream<(QuicSendStream, QuicRecvStream), stream_proto::biremote::StreamBiRemoteWT>;
impl StreamBiRemoteQuic {
pub async fn accept_bi(quic_connection: &quinn::Connection) -> Option<Self> {
let stream = quic_connection.accept_bi().await.ok()?;
Some(Self {
stream: (QuicSendStream(stream.0), QuicRecvStream(stream.1)),
proto: StreamProto::accept_bi(),
})
}
pub fn upgrade(self) -> StreamBiRemoteH3 {
StreamBiRemoteH3 {
stream: self.stream,
proto: self.proto.upgrade(),
}
}
#[inline(always)]
pub fn id(&self) -> StreamId {
self.stream.0.id()
}
}
impl StreamBiRemoteH3 {
pub async fn read_frame<'a>(&mut self) -> Result<Frame<'a>, ProtoReadError> {
self.proto.read_frame_async(&mut self.stream.1).await
}
pub fn stop(&mut self, error_code: VarInt) -> Result<(), AlreadyStop> {
self.stream.1.stop(error_code)
}
pub fn upgrade(self, session_id: SessionId) -> StreamBiRemoteWT {
StreamBiRemoteWT {
stream: self.stream,
proto: self.proto.upgrade(session_id),
}
}
pub fn id(&self) -> StreamId {
self.stream.0.id()
}
pub fn into_session(self, session_request: SessionRequest) -> session::StreamSession {
session::StreamSession {
stream: self.stream,
proto: self.proto.into_session(session_request),
}
}
}
impl StreamBiRemoteWT {
#[inline(always)]
pub fn session_id(&self) -> SessionId {
self.proto.session_id()
}
#[inline(always)]
pub fn id(&self) -> StreamId {
self.stream.0.id()
}
#[inline(always)]
pub fn into_stream(self) -> (QuicSendStream, QuicRecvStream) {
self.stream
}
}
}
pub mod bilocal {
use super::*;
pub type StreamBiLocalQuic =
Stream<(QuicSendStream, QuicRecvStream), stream_proto::bilocal::StreamBiLocalQuic>;
pub type StreamBiLocalH3 =
Stream<(QuicSendStream, QuicRecvStream), stream_proto::bilocal::StreamBiLocalH3>;
pub type StreamBiLocalWT =
Stream<(QuicSendStream, QuicRecvStream), stream_proto::bilocal::StreamBiLocalWT>;
impl StreamBiLocalQuic {
pub async fn open_bi(quic_connection: &quinn::Connection) -> Option<Self> {
let stream = quic_connection.open_bi().await.ok()?;
Some(Self {
stream: (QuicSendStream(stream.0), QuicRecvStream(stream.1)),
proto: StreamProto::open_bi(),
})
}
pub fn upgrade(self) -> StreamBiLocalH3 {
StreamBiLocalH3 {
stream: self.stream,
proto: self.proto.upgrade(),
}
}
}
impl StreamBiLocalH3 {
pub async fn upgrade(
mut self,
session_id: SessionId,
) -> Result<StreamBiLocalWT, ProtoWriteError> {
let proto = self
.proto
.upgrade_async(session_id, &mut self.stream.0)
.await?;
Ok(StreamBiLocalWT {
stream: self.stream,
proto,
})
}
pub fn into_session(self, session_request: SessionRequest) -> session::StreamSession {
session::StreamSession {
stream: self.stream,
proto: self.proto.into_session(session_request),
}
}
}
impl StreamBiLocalWT {
pub fn into_stream(self) -> (QuicSendStream, QuicRecvStream) {
self.stream
}
}
}
pub mod uniremote {
use super::*;
pub type StreamUniRemoteQuic =
Stream<QuicRecvStream, stream_proto::uniremote::StreamUniRemoteQuic>;
pub type StreamUniRemoteH3 = Stream<QuicRecvStream, stream_proto::uniremote::StreamUniRemoteH3>;
pub type StreamUniRemoteWT = Stream<QuicRecvStream, stream_proto::uniremote::StreamUniRemoteWT>;
impl StreamUniRemoteQuic {
pub async fn accept_uni(quic_connection: &quinn::Connection) -> Option<Self> {
let stream = quic_connection.accept_uni().await.ok()?;
Some(Self {
stream: QuicRecvStream(stream),
proto: StreamProto::accept_uni(),
})
}
pub async fn upgrade(mut self) -> Result<StreamUniRemoteH3, ProtoReadError> {
let proto = self.proto.upgrade_async(&mut self.stream).await?;
Ok(StreamUniRemoteH3 {
stream: self.stream,
proto,
})
}
#[inline(always)]
pub fn id(&self) -> StreamId {
self.stream.id()
}
}
impl StreamUniRemoteH3 {
pub async fn read_frame<'a>(&mut self) -> Result<Frame<'a>, ProtoReadError> {
self.proto.read_frame_async(&mut self.stream).await
}
pub fn kind(&self) -> StreamKind {
self.proto.kind()
}
pub fn upgrade(self) -> StreamUniRemoteWT {
StreamUniRemoteWT {
stream: self.stream,
proto: self.proto.upgrade(),
}
}
pub fn stream_mut(&mut self) -> &mut QuicRecvStream {
&mut self.stream
}
}
impl StreamUniRemoteWT {
#[inline(always)]
pub fn session_id(&self) -> SessionId {
self.proto.session_id()
}
#[inline(always)]
pub fn id(&self) -> StreamId {
self.stream.id()
}
#[inline(always)]
pub fn into_stream(self) -> QuicRecvStream {
self.stream
}
}
}
pub mod unilocal {
use super::*;
pub type StreamUniLocalQuic =
Stream<QuicSendStream, stream_proto::unilocal::StreamUniLocalQuic>;
pub type StreamUniLocalH3 = Stream<QuicSendStream, stream_proto::unilocal::StreamUniLocalH3>;
pub type StreamUniLocalWT = Stream<QuicSendStream, stream_proto::unilocal::StreamUniLocalWT>;
impl StreamUniLocalQuic {
pub async fn open_uni(quic_connection: &quinn::Connection) -> Option<Self> {
let stream = quic_connection.open_uni().await.ok()?;
Some(Self {
stream: QuicSendStream(stream),
proto: StreamProto::open_uni(),
})
}
pub async fn upgrade(
mut self,
stream_header: StreamHeader,
) -> Result<StreamUniLocalH3, ProtoWriteError> {
let proto = self
.proto
.upgrade_async(stream_header, &mut self.stream)
.await?;
Ok(StreamUniLocalH3 {
stream: self.stream,
proto,
})
}
}
impl StreamUniLocalH3 {
pub async fn write_frame(&mut self, frame: Frame<'_>) -> Result<(), ProtoWriteError> {
self.proto.write_frame_async(frame, &mut self.stream).await
}
pub fn kind(&self) -> StreamKind {
self.proto.kind()
}
pub async fn stopped(&mut self) -> StreamWriteError {
self.stream.stopped().await
}
pub fn upgrade(self) -> StreamUniLocalWT {
StreamUniLocalWT {
stream: self.stream,
proto: self.proto.upgrade(),
}
}
}
impl StreamUniLocalWT {
pub fn into_stream(self) -> QuicSendStream {
self.stream
}
}
}
pub mod session {
use super::*;
pub type StreamSession =
Stream<(QuicSendStream, QuicRecvStream), stream_proto::session::StreamSession>;
impl StreamSession {
pub async fn read_frame<'a>(&mut self) -> Result<Frame<'a>, ProtoReadError> {
self.proto.read_frame_async(&mut self.stream.1).await
}
pub async fn write_frame(&mut self, frame: Frame<'_>) -> Result<(), ProtoWriteError> {
self.proto
.write_frame_async(frame, &mut self.stream.0)
.await
}
pub fn stop(&mut self, error_code: VarInt) -> Result<(), AlreadyStop> {
self.stream.1.stop(error_code)
}
pub fn id(&self) -> StreamId {
self.stream.0.id()
}
pub fn session_id(&self) -> SessionId {
SessionId::try_from_session_stream(self.id()).expect("Session stream must be valid")
}
pub fn request(&self) -> &SessionRequest {
self.proto.request()
}
pub async fn finish(&mut self) {
let _ = self.stream.0.finish().await;
}
pub fn reset(&mut self, error_code: VarInt) {
let _ = self.stream.0.reset(error_code);
}
pub fn set_priority(&self, priority: i32) {
self.stream.0.set_priority(priority);
}
}
}
impl From<quinn::WriteError> for StreamWriteError {
fn from(error: quinn::WriteError) -> Self {
match error {
quinn::WriteError::Stopped(code) => StreamWriteError::Stopped(varint_q2w(code)),
quinn::WriteError::ConnectionLost(_) | quinn::WriteError::ClosedStream => {
StreamWriteError::NotConnected
}
quinn::WriteError::ZeroRttRejected => StreamWriteError::QuicProto,
}
}
}
impl From<quinn::ReadError> for StreamReadError {
fn from(error: quinn::ReadError) -> Self {
match error {
quinn::ReadError::Reset(code) => StreamReadError::Reset(varint_q2w(code)),
quinn::ReadError::ConnectionLost(_) | quinn::ReadError::ClosedStream => {
StreamReadError::NotConnected
}
quinn::ReadError::IllegalOrderedRead => StreamReadError::QuicProto,
quinn::ReadError::ZeroRttRejected => StreamReadError::QuicProto,
}
}
}
pub mod connect;
pub mod qpack;
pub mod settings;

View file

@ -0,0 +1,106 @@
use crate::driver::streams::uniremote::StreamUniRemoteH3;
use crate::driver::DriverError;
use crate::error::StreamReadError;
use crate::error::StreamReadExactError;
use std::future::pending;
use wtransport_proto::error::ErrorCode;
use wtransport_proto::stream_header::StreamKind;
pub struct RemoteQPackEncStream {
stream: Option<StreamUniRemoteH3>,
buffer: Box<[u8]>,
}
impl RemoteQPackEncStream {
pub fn empty() -> Self {
let buffer = vec![0; 64].into_boxed_slice();
Self {
stream: None,
buffer,
}
}
pub fn is_empty(&self) -> bool {
self.stream.is_none()
}
pub fn set_stream(&mut self, stream: StreamUniRemoteH3) {
assert!(matches!(stream.kind(), StreamKind::QPackEncoder));
self.stream = Some(stream);
}
pub async fn run(&mut self) -> DriverError {
let stream = match self.stream.as_mut() {
Some(stream) => stream,
None => pending().await,
};
loop {
match stream.stream_mut().read_exact(&mut self.buffer).await {
Ok(()) => {}
Err(StreamReadExactError::FinishedEarly(_)) => {
return DriverError::Proto(ErrorCode::ClosedCriticalStream);
}
Err(StreamReadExactError::Read(StreamReadError::NotConnected)) => {
return DriverError::NotConnected;
}
Err(StreamReadExactError::Read(StreamReadError::Reset(_))) => {
return DriverError::Proto(ErrorCode::ClosedCriticalStream);
}
Err(StreamReadExactError::Read(StreamReadError::QuicProto)) => {
return DriverError::Proto(ErrorCode::ClosedCriticalStream);
}
}
}
}
}
pub struct RemoteQPackDecStream {
stream: Option<StreamUniRemoteH3>,
buffer: Box<[u8]>,
}
impl RemoteQPackDecStream {
pub fn empty() -> Self {
let buffer = vec![0; 64].into_boxed_slice();
Self {
stream: None,
buffer,
}
}
pub fn is_empty(&self) -> bool {
self.stream.is_none()
}
pub fn set_stream(&mut self, stream: StreamUniRemoteH3) {
assert!(matches!(stream.kind(), StreamKind::QPackDecoder));
self.stream = Some(stream);
}
pub async fn run(&mut self) -> DriverError {
let stream = match self.stream.as_mut() {
Some(stream) => stream,
None => pending().await,
};
loop {
match stream.stream_mut().read_exact(&mut self.buffer).await {
Ok(()) => {}
Err(StreamReadExactError::FinishedEarly(_)) => {
return DriverError::Proto(ErrorCode::ClosedCriticalStream);
}
Err(StreamReadExactError::Read(StreamReadError::NotConnected)) => {
return DriverError::NotConnected;
}
Err(StreamReadExactError::Read(StreamReadError::Reset(_))) => {
return DriverError::Proto(ErrorCode::ClosedCriticalStream);
}
Err(StreamReadExactError::Read(StreamReadError::QuicProto)) => {
return DriverError::Proto(ErrorCode::ClosedCriticalStream);
}
}
}
}
}

View file

@ -0,0 +1,161 @@
use crate::driver::streams::unilocal::StreamUniLocalH3;
use crate::driver::streams::uniremote::StreamUniRemoteH3;
use crate::driver::streams::ProtoReadError;
use crate::driver::streams::ProtoWriteError;
use crate::driver::DriverError;
use crate::error::StreamWriteError;
use crate::VarInt;
use std::future::pending;
use tokio::sync::watch;
use wtransport_proto::bytes;
use wtransport_proto::error::ErrorCode;
use wtransport_proto::frame::Frame;
use wtransport_proto::frame::FrameKind;
use wtransport_proto::settings::Settings;
use wtransport_proto::stream_header::StreamKind;
pub struct LocalSettingsStream {
stream: Option<StreamUniLocalH3>,
settings: Settings,
}
impl LocalSettingsStream {
pub fn empty() -> Self {
let settings = Settings::builder()
.qpack_max_table_capacity(VarInt::from_u32(0))
.qpack_blocked_streams(VarInt::from_u32(0))
.enable_connect_protocol() // TODO(biagio): it would be nice to have this only for server
.enable_webtransport()
.enable_h3_datagrams()
.webtransport_max_sessions(VarInt::from_u32(1))
.build();
Self {
stream: None,
settings,
}
}
pub fn is_empty(&self) -> bool {
self.stream.is_none()
}
pub fn set_stream(&mut self, stream: StreamUniLocalH3) {
assert!(matches!(stream.kind(), StreamKind::Control));
self.stream = Some(stream);
}
pub async fn send_settings(&mut self) -> Result<(), DriverError> {
match self
.stream
.as_mut()
.expect("Cannot send settings on empty stream")
.write_frame(self.settings.generate_frame())
.await
{
Ok(()) => Ok(()),
Err(ProtoWriteError::NotConnected) => Err(DriverError::NotConnected),
Err(ProtoWriteError::Stopped) => {
Err(DriverError::Proto(ErrorCode::ClosedCriticalStream))
}
}
}
pub async fn run(&mut self) -> DriverError {
match self.stream.as_mut() {
Some(stream) => match stream.stopped().await {
StreamWriteError::NotConnected => DriverError::NotConnected,
StreamWriteError::Closed | StreamWriteError::Stopped(_) => {
DriverError::Proto(ErrorCode::ClosedCriticalStream)
}
StreamWriteError::QuicProto => DriverError::Proto(ErrorCode::ClosedCriticalStream),
},
None => pending().await,
}
}
}
pub struct RemoteSettingsStream {
stream: Option<StreamUniRemoteH3>,
settings: watch::Sender<Option<Settings>>,
}
impl RemoteSettingsStream {
pub fn empty() -> Self {
Self {
stream: None,
settings: watch::channel(None).0,
}
}
pub fn is_empty(&self) -> bool {
self.stream.is_none()
}
pub fn set_stream(&mut self, stream: StreamUniRemoteH3) {
assert!(matches!(stream.kind(), StreamKind::Control));
self.stream = Some(stream);
}
pub fn subscribe(&self) -> RemoteSettingsWatcher {
RemoteSettingsWatcher(self.settings.subscribe())
}
pub async fn run(&mut self) -> DriverError {
loop {
let frame = match self.read_frame().await {
Ok(frame) => frame,
Err(driver_error) => return driver_error,
};
if self.settings.borrow().is_none() {
if !matches!(frame.kind(), FrameKind::Settings) {
return DriverError::Proto(ErrorCode::MissingSettings);
}
let settings = match Settings::with_frame(&frame) {
Ok(settings) => settings,
Err(error_code) => return DriverError::Proto(error_code),
};
self.settings.send_replace(Some(settings));
} else if !matches!(frame.kind(), FrameKind::Exercise(_)) {
return DriverError::Proto(ErrorCode::FrameUnexpected);
}
}
}
async fn read_frame<'a>(&mut self) -> Result<Frame<'a>, DriverError> {
let Some(stream) = self.stream.as_mut() else {
return pending().await;
};
match stream.read_frame().await {
Ok(frame) => Ok(frame),
Err(ProtoReadError::H3(error_code)) => Err(DriverError::Proto(error_code)),
Err(ProtoReadError::IO(io_error)) => match io_error {
bytes::IoReadError::ImmediateFin
| bytes::IoReadError::UnexpectedFin
| bytes::IoReadError::Reset => {
Err(DriverError::Proto(ErrorCode::ClosedCriticalStream))
}
bytes::IoReadError::NotConnected => Err(DriverError::NotConnected),
},
}
}
}
pub struct RemoteSettingsWatcher(watch::Receiver<Option<Settings>>);
impl RemoteSettingsWatcher {
pub async fn accept_settings(&mut self) -> Option<Settings> {
self.0.changed().await.ok()?;
Some(
self.0
.borrow()
.clone()
.expect("On change settings must be set"),
)
}
}

View file

@ -0,0 +1,236 @@
use crate::StreamId;
use crate::VarInt;
use std::sync::Arc;
use tokio::sync::mpsc;
use tokio::sync::watch;
use tokio::sync::Mutex;
#[inline(always)]
pub fn varint_q2w(varint: quinn::VarInt) -> VarInt {
// SAFETY: varint conversion
unsafe {
debug_assert!(varint.into_inner() <= VarInt::MAX.into_inner());
VarInt::from_u64_unchecked(varint.into_inner())
}
}
#[inline(always)]
pub fn varint_w2q(varint: VarInt) -> quinn::VarInt {
// SAFETY: varint conversion
unsafe {
debug_assert!(varint.into_inner() <= quinn::VarInt::MAX.into_inner());
quinn::VarInt::from_u64_unchecked(varint.into_inner())
}
}
#[inline(always)]
pub fn streamid_q2w(stream_id: quinn::StreamId) -> StreamId {
let value = quinn::VarInt::from(stream_id).into_inner();
let varint = unsafe {
debug_assert!(value <= VarInt::MAX.into_inner());
VarInt::from_u64_unchecked(value)
};
StreamId::new(varint)
}
pub fn shared_result<T>() -> (SharedResultSet<T>, SharedResultGet<T>)
where
T: Clone,
{
let set = SharedResultSet::new();
let get = set.subscribe();
(set, get)
}
#[derive(Debug, Clone)]
pub struct SharedResultSet<T>(Arc<watch::Sender<Option<T>>>);
impl<T> SharedResultSet<T>
where
T: Clone,
{
#[inline(always)]
pub fn new() -> Self {
Self(Arc::new(watch::channel(None).0))
}
/// Sets the shared result in thread safe manner.
///
/// The first call will be able to actually set the inner value,
/// successive calls end up into being no-op.
///
/// Returns `true` if the inner result is actually set.
pub fn set(&self, result: T) -> bool {
self.0.send_if_modified(|state| {
if state.is_none() {
*state = Some(result);
true
} else {
false
}
})
}
/// Awaits all subscribers are dead.
#[inline(always)]
pub async fn closed(&self) {
self.0.closed().await;
}
/// Generates a new subscriber.
///
/// A subscriber is able to be notified when the shared result
/// will be set.
#[inline(always)]
pub fn subscribe(&self) -> SharedResultGet<T> {
SharedResultGet(Mutex::new(self.0.subscribe()))
}
}
#[derive(Debug)]
pub struct SharedResultGet<T>(Mutex<watch::Receiver<Option<T>>>);
impl<T> SharedResultGet<T>
where
T: Clone,
{
/// Awaits the shared result is set by any setter.
///
/// Once the shared result is set, this method will always
/// return that value (i.e., `Some(T)`).
///
/// If all setters are dead before setting any result, this will
/// return `None`. And all successive calls will return `None`.
pub async fn result(&self) -> Option<T> {
let mut lock = self.0.lock().await;
loop {
if let Some(result) = lock.borrow().clone() {
return Some(result);
}
if lock.changed().await.is_err() {
return None;
}
}
}
}
pub struct SendError;
pub enum TrySendError<T> {
Full(T),
Closed(T),
}
#[derive(Debug)]
pub struct BiChannelEndpoint<T> {
sender: mpsc::Sender<T>,
receiver: Mutex<mpsc::Receiver<T>>,
}
impl<T> BiChannelEndpoint<T> {
#[inline(always)]
pub async fn send(&self, value: T) -> Result<(), SendError> {
self.sender.send(value).await.map_err(|_| SendError)
}
pub fn try_send(&self, value: T) -> Result<(), TrySendError<T>> {
self.sender.try_send(value).map_err(|error| match error {
mpsc::error::TrySendError::Full(value) => TrySendError::Full(value),
mpsc::error::TrySendError::Closed(value) => TrySendError::Closed(value),
})
}
#[inline(always)]
pub async fn recv(&self) -> Option<T> {
self.receiver.lock().await.recv().await
}
}
pub fn bichannel<T>(capacity: usize) -> (BiChannelEndpoint<T>, BiChannelEndpoint<T>) {
let c1 = mpsc::channel(capacity);
let c2 = mpsc::channel(capacity);
(
BiChannelEndpoint {
sender: c1.0,
receiver: Mutex::new(c2.1),
},
BiChannelEndpoint {
sender: c2.0,
receiver: Mutex::new(c1.1),
},
)
}
#[cfg(test)]
mod tests {
use super::*;
use utils::poll_once;
#[tokio::test]
async fn shared_result_double_set() {
let set = SharedResultSet::new();
assert!(set.set(1));
assert!(!set.set(2));
let get = set.subscribe();
assert!(matches!(get.result().await, Some(1)));
}
#[tokio::test]
async fn shared_result_get_drop() {
let set = SharedResultSet::<()>::new();
let get = set.subscribe();
drop(set);
assert!(get.result().await.is_none());
assert!(get.result().await.is_none());
}
#[tokio::test]
async fn shared_result_get() {
let set = SharedResultSet::new();
let get = set.subscribe();
assert!(poll_once(get.result()).await.is_none());
set.set(1);
drop(set);
assert!(matches!(poll_once(get.result()).await.unwrap(), Some(1)));
assert!(matches!(poll_once(get.result()).await.unwrap(), Some(1)));
}
mod utils {
use std::future::Future;
use std::pin::Pin;
use std::task::Context;
use std::task::Poll;
pub async fn poll_once<F, T>(future: F) -> Option<T>
where
F: Future<Output = T>,
{
PollOnce(Box::pin(future)).await
}
struct PollOnce<F>(Pin<Box<F>>);
impl<F, T> Future for PollOnce<F>
where
F: Future<Output = T>,
{
type Output = Option<T>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match Future::poll(self.0.as_mut(), cx) {
Poll::Ready(result) => Poll::Ready(Some(result)),
Poll::Pending => Poll::Ready(None),
}
}
}
}
}

View file

@ -0,0 +1,797 @@
use crate::config::ClientConfig;
use crate::config::DnsResolver;
use crate::config::ServerConfig;
use crate::connection::Connection;
use crate::driver::streams::session::StreamSession;
use crate::driver::streams::ProtoReadError;
use crate::driver::streams::ProtoWriteError;
use crate::driver::utils::varint_w2q;
use crate::driver::Driver;
use crate::error::ConnectingError;
use crate::error::ConnectionError;
use crate::VarInt;
use quinn::TokioRuntime;
use std::collections::HashMap;
use std::future::Future;
use std::future::IntoFuture;
use std::marker::PhantomData;
use std::net::SocketAddr;
use std::net::SocketAddrV4;
use std::net::SocketAddrV6;
use std::pin::Pin;
use std::sync::Arc;
use std::task::Context;
use std::task::Poll;
use tracing::debug;
use url::Host;
use url::Url;
use wtransport_proto::error::ErrorCode;
use wtransport_proto::frame::FrameKind;
use wtransport_proto::headers::Headers;
use wtransport_proto::session::ReservedHeader;
use wtransport_proto::session::SessionRequest as SessionRequestProto;
use wtransport_proto::session::SessionResponse as SessionResponseProto;
/// Helper structure for Endpoint types.
pub mod endpoint_side {
use super::*;
/// Type of endpoint accepting multiple WebTransport connections.
///
/// Use [`Endpoint::server`] to create and server-endpoint.
pub struct Server {
pub(super) _marker: PhantomData<()>,
}
/// Type of endpoint opening a WebTransport connection.
///
/// Use [`Endpoint::client`] to create and client-endpoint.
pub struct Client {
pub(super) dns_resolver: Arc<dyn DnsResolver + Send + Sync>,
}
}
/// Entrypoint for creating client or server connections.
///
/// A single endpoint can be used to accept or connect multiple connections.
/// Each endpoint internally binds an UDP socket.
///
/// # Server
/// Use [`Endpoint::server`] for creating a server-side endpoint.
/// Afterwards use the method [`Endpoint::accept`] for awaiting on incoming session request.
///
/// ```no_run
/// # use anyhow::Result;
/// # use wtransport::ServerConfig;
/// # use wtransport::Identity;
/// use wtransport::Endpoint;
///
/// # async fn run() -> Result<()> {
/// # let config = ServerConfig::builder()
/// # .with_bind_default(4433)
/// # .with_identity(Identity::self_signed(["doc"]).unwrap())
/// # .build();
/// let server = Endpoint::server(config)?;
/// loop {
/// let incoming_session = server.accept().await;
/// // Spawn task that handles client incoming session...
/// }
/// # Ok(())
/// # }
/// ```
///
/// # Client
/// Use [`Endpoint::client`] for creating a client-side endpoint and use [`Endpoint::connect`]
/// to connect to a server specifying the URL.
///
/// ```no_run
/// # use anyhow::Result;
/// use wtransport::ClientConfig;
/// use wtransport::Endpoint;
///
/// # async fn run() -> Result<()> {
/// let connection = Endpoint::client(ClientConfig::default())?
/// .connect("https://localhost:4433")
/// .await?;
/// # Ok(())
/// # }
/// ```
pub struct Endpoint<Side> {
endpoint: quinn::Endpoint,
side: Side,
}
impl<Side> Endpoint<Side> {
/// Closes all of this endpoint's connections immediately and cease accepting new connections.
pub fn close(&self, error_code: VarInt, reason: &[u8]) {
self.endpoint.close(varint_w2q(error_code), reason);
}
/// Waits for all connections on the endpoint to be cleanly shut down.
pub async fn wait_idle(&self) {
self.endpoint.wait_idle().await;
}
/// Gets the local [`SocketAddr`] the underlying socket is bound to.
pub fn local_addr(&self) -> std::io::Result<SocketAddr> {
self.endpoint.local_addr()
}
/// Get the number of connections that are currently open.
pub fn open_connections(&self) -> usize {
self.endpoint.open_connections()
}
}
impl Endpoint<endpoint_side::Server> {
/// Constructs a *server* endpoint.
pub fn server(server_config: ServerConfig) -> std::io::Result<Self> {
let endpoint_config = server_config.endpoint_config;
let quic_config = server_config.quic_config;
let socket = server_config.bind_address_config.bind_socket()?;
let runtime = Arc::new(TokioRuntime);
let endpoint = quinn::Endpoint::new(endpoint_config, Some(quic_config), socket, runtime)?;
Ok(Self {
endpoint,
side: endpoint_side::Server {
_marker: PhantomData,
},
})
}
/// Get the next incoming connection attempt from a client.
pub async fn accept(&self) -> IncomingSession {
let quic_incoming = self
.endpoint
.accept()
.await
.expect("Endpoint cannot be closed");
debug!("New incoming QUIC connection");
IncomingSession(quic_incoming)
}
/// Reloads the server configuration.
///
/// Useful for e.g. refreshing TLS certificates without disrupting existing connections.
///
/// # Arguments
///
/// * `server_config` - The new configuration for the server.
/// * `rebind` - A boolean indicating whether the server should rebind its socket.
/// If `true`, the server will bind to a new socket with the provided configuration.
/// If `false`, the bind address configuration will be ignored.
pub fn reload_config(&self, server_config: ServerConfig, rebind: bool) -> std::io::Result<()> {
if rebind {
let socket = server_config.bind_address_config.bind_socket()?;
self.endpoint.rebind(socket)?;
}
let quic_config = server_config.quic_config;
self.endpoint.set_server_config(Some(quic_config));
Ok(())
}
}
impl Endpoint<endpoint_side::Client> {
/// Constructs a *client* endpoint.
pub fn client(client_config: ClientConfig) -> std::io::Result<Self> {
let endpoint_config = client_config.endpoint_config;
let quic_config = client_config.quic_config;
let socket = client_config.bind_address_config.bind_socket()?;
let runtime = Arc::new(TokioRuntime);
let mut endpoint = quinn::Endpoint::new(endpoint_config, None, socket, runtime)?;
endpoint.set_default_client_config(quic_config);
Ok(Self {
endpoint,
side: endpoint_side::Client {
dns_resolver: client_config.dns_resolver,
},
})
}
/// Establishes a WebTransport connection to a specified URL.
///
/// This method initiates a WebTransport connection to the specified URL.
/// It validates the URL, and performs necessary steps to establish a secure connection.
///
/// # Arguments
///
/// * `options` - Connection options specifying the URL and additional headers.
/// It can be simply an [URL](https://en.wikipedia.org/wiki/URL) string representing
/// the WebTransport endpoint to connect to. It must have an `https` scheme.
/// The URL can specify either an IP address or a hostname.
/// When specifying a hostname, the method will internally perform DNS resolution,
/// configured with
/// [`ClientConfigBuilder::dns_resolver`](crate::config::ClientConfigBuilder::dns_resolver).
///
/// # Examples
///
/// Connect using a URL with a hostname (DNS resolution is performed):
///
/// ```no_run
/// # use anyhow::Result;
/// # use wtransport::endpoint::endpoint_side::Client;
/// # async fn example(endpoint: wtransport::Endpoint<Client>) -> Result<()> {
/// let url = "https://example.com:4433/webtransport";
/// let connection = endpoint.connect(url).await?;
/// # Ok(())
/// # }
/// ```
///
/// Connect using a URL with an IP address:
///
/// ```no_run
/// # use anyhow::Result;
/// # use wtransport::endpoint::endpoint_side::Client;
/// # async fn example(endpoint: wtransport::Endpoint<Client>) -> Result<()> {
/// let url = "https://127.0.0.1:4343/webtransport";
/// let connection = endpoint.connect(url).await?;
/// # Ok(())
/// # }
/// ```
///
/// Connect adding an additional header:
///
/// ```no_run
/// # use anyhow::Result;
/// # use wtransport::endpoint::endpoint_side::Client;
/// # use wtransport::endpoint::ConnectOptions;
/// # async fn example(endpoint: wtransport::Endpoint<Client>) -> Result<()> {
/// let options = ConnectOptions::builder("https://example.com:4433/webtransport")
/// .add_header("Authorization", "AuthToken")
/// .build();
/// let connection = endpoint.connect(options).await?;
/// # Ok(())
/// # }
/// ```
pub async fn connect<O>(&self, options: O) -> Result<Connection, ConnectingError>
where
O: IntoConnectOptions,
{
let options = options.into_options();
let url = Url::parse(&options.url)
.map_err(|parse_error| ConnectingError::InvalidUrl(parse_error.to_string()))?;
if url.scheme() != "https" {
return Err(ConnectingError::InvalidUrl(
"WebTransport URL scheme must be 'https'".to_string(),
));
}
let host = url.host().expect("https scheme must have an host");
let port = url.port().unwrap_or(443);
let (socket_address, server_name) = match host {
Host::Domain(domain) => {
let socket_address = self
.side
.dns_resolver
.resolve(&format!("{domain}:{port}"))
.await
.map_err(ConnectingError::DnsLookup)?
.ok_or(ConnectingError::DnsNotFound)?;
(socket_address, domain.to_string())
}
Host::Ipv4(address) => {
let socket_address = SocketAddr::V4(SocketAddrV4::new(address, port));
(socket_address, address.to_string())
}
Host::Ipv6(address) => {
let socket_address = SocketAddr::V6(SocketAddrV6::new(address, port, 0, 0));
(socket_address, address.to_string())
}
};
let quic_connection = self
.endpoint
.connect(socket_address, &server_name)
.map_err(ConnectingError::with_connect_error)?
.await
.map_err(|connection_error| {
ConnectingError::ConnectionError(connection_error.into())
})?;
let driver = Driver::init(quic_connection.clone());
let _settings = driver.accept_settings().await.map_err(|driver_error| {
ConnectingError::ConnectionError(ConnectionError::with_driver_error(
driver_error,
&quic_connection,
))
})?;
// TODO(biagio): validate settings
let mut session_request_proto =
SessionRequestProto::new(url.as_ref()).expect("Url has been already validate");
for (k, v) in options.additional_headers {
session_request_proto
.insert(k.clone(), v)
.map_err(|ReservedHeader| ConnectingError::ReservedHeader(k))?;
}
let mut stream_session = match driver.open_session(session_request_proto).await {
Ok(stream_session) => stream_session,
Err(driver_error) => {
return Err(ConnectingError::ConnectionError(
ConnectionError::with_driver_error(driver_error, &quic_connection),
))
}
};
let session_id = stream_session.session_id();
match stream_session
.write_frame(stream_session.request().headers().generate_frame())
.await
{
Ok(()) => {}
Err(ProtoWriteError::Stopped) => {
return Err(ConnectingError::SessionRejected);
}
Err(ProtoWriteError::NotConnected) => {
return Err(ConnectingError::with_no_connection(&quic_connection));
}
}
let frame = loop {
let frame = match stream_session.read_frame().await {
Ok(frame) => frame,
Err(ProtoReadError::H3(error_code)) => {
quic_connection.close(varint_w2q(error_code.to_code()), b"");
return Err(ConnectingError::ConnectionError(
ConnectionError::local_h3_error(error_code),
));
}
Err(ProtoReadError::IO(_io_error)) => {
return Err(ConnectingError::with_no_connection(&quic_connection));
}
};
if let FrameKind::Exercise(_) = frame.kind() {
continue;
}
break frame;
};
if !matches!(frame.kind(), FrameKind::Headers) {
quic_connection.close(varint_w2q(ErrorCode::FrameUnexpected.to_code()), b"");
return Err(ConnectingError::ConnectionError(
ConnectionError::local_h3_error(ErrorCode::FrameUnexpected),
));
}
let headers = match Headers::with_frame(&frame) {
Ok(headers) => headers,
Err(error_code) => {
quic_connection.close(varint_w2q(error_code.to_code()), b"");
return Err(ConnectingError::ConnectionError(
ConnectionError::local_h3_error(error_code),
));
}
};
let Ok(session_response) = SessionResponseProto::try_from(headers) else {
quic_connection.close(varint_w2q(ErrorCode::Message.to_code()), b"");
return Err(ConnectingError::ConnectionError(
ConnectionError::local_h3_error(ErrorCode::Message),
));
};
if session_response.code().is_successful() {
match driver.register_session(stream_session).await {
Ok(()) => {}
Err(driver_error) => {
return Err(ConnectingError::ConnectionError(
ConnectionError::with_driver_error(driver_error, &quic_connection),
))
}
}
} else {
return Err(ConnectingError::SessionRejected);
}
Ok(Connection::new(quic_connection, driver, session_id))
}
}
/// Options for establishing a client WebTransport connection.
///
/// Used in [`Endpoint::connect`].
///
/// # Examples
///
/// ```no_run
/// # use anyhow::Result;
/// # use wtransport::endpoint::endpoint_side::Client;
/// # use wtransport::endpoint::ConnectOptions;
/// # async fn example(endpoint: wtransport::Endpoint<Client>) -> Result<()> {
/// let options = ConnectOptions::builder("https://example.com:4433/webtransport")
/// .add_header("Authorization", "AuthToken")
/// .build();
/// let connection = endpoint.connect(options).await?;
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Clone)]
pub struct ConnectOptions {
url: String,
additional_headers: HashMap<String, String>,
}
impl ConnectOptions {
/// Creates a new `ConnectOptions` using a builder pattern.
///
/// # Arguments
///
/// * `url` - A [URL](https://en.wikipedia.org/wiki/URL) string representing the WebTransport
/// endpoint to connect to. It must have an `https` scheme.
/// The URL can specify either an IP address or a hostname.
/// When specifying a hostname, the method will internally perform DNS resolution,
/// configured with
/// [`ClientConfigBuilder::dns_resolver`](crate::config::ClientConfigBuilder::dns_resolver).
pub fn builder<S>(url: S) -> ConnectRequestBuilder
where
S: ToString,
{
ConnectRequestBuilder {
url: url.to_string(),
additional_headers: Default::default(),
}
}
/// Gets the URL which this will connect to.
pub fn url(&self) -> &str {
&self.url
}
/// Gets the additional headers that will be passed when connecting.
pub fn additional_headers(&self) -> &HashMap<String, String> {
&self.additional_headers
}
}
/// A trait for converting types into `ConnectOptions`.
pub trait IntoConnectOptions {
/// Perform value-to-value conversion into [`ConnectOptions`].
fn into_options(self) -> ConnectOptions;
}
/// A builder for [`ConnectOptions`].
///
/// See [`ConnectOptions::builder`].
pub struct ConnectRequestBuilder {
url: String,
additional_headers: HashMap<String, String>,
}
impl ConnectRequestBuilder {
/// Adds a header to the connection options.
///
/// # Examples
///
/// ```rust
/// use wtransport::endpoint::ConnectOptions;
///
/// let options = ConnectOptions::builder("https://example.com:4433/webtransport")
/// .add_header("Authorization", "AuthToken")
/// .build();
/// ```
pub fn add_header<K, V>(mut self, key: K, value: V) -> Self
where
K: ToString,
V: ToString,
{
self.additional_headers
.insert(key.to_string(), value.to_string());
self
}
/// Constructs the [`ConnectOptions`] from the builder configuration.
pub fn build(self) -> ConnectOptions {
ConnectOptions {
url: self.url,
additional_headers: self.additional_headers,
}
}
}
impl IntoConnectOptions for ConnectRequestBuilder {
fn into_options(self) -> ConnectOptions {
self.build()
}
}
impl IntoConnectOptions for ConnectOptions {
fn into_options(self) -> ConnectOptions {
self
}
}
impl<S> IntoConnectOptions for S
where
S: ToString,
{
fn into_options(self) -> ConnectOptions {
ConnectOptions::builder(self).build()
}
}
type DynFutureIncomingSession =
dyn Future<Output = Result<SessionRequest, ConnectionError>> + Send + Sync;
/// [`IntoFuture`] for an in-progress incoming connection attempt.
///
/// Created by [`Endpoint::accept`].
pub struct IncomingSession(quinn::Incoming);
impl IncomingSession {
/// The peer's UDP address.
pub fn remote_address(&self) -> SocketAddr {
self.0.remote_address()
}
/// Whether the socket address that is initiating this connection has been validated.
///
/// This means that the sender of the initial packet has proved that they can receive traffic
/// sent to `self.remote_address()`.
pub fn remote_address_validated(&self) -> bool {
self.0.remote_address_validated()
}
/// Respond with a retry packet, requiring the client to retry with address validation
///
/// # Panics
///
/// If `remote_address_validated()` is true.
pub fn retry(self) {
self.0.retry().expect("remote address already verified");
}
/// Reject this incoming connection attempt.
pub fn refuse(self) {
self.0.refuse();
}
/// Ignore this incoming connection attempt, not sending any packet in response.
pub fn ignore(self) {
self.0.ignore();
}
}
impl IntoFuture for IncomingSession {
type IntoFuture = IncomingSessionFuture;
type Output = Result<SessionRequest, ConnectionError>;
fn into_future(self) -> Self::IntoFuture {
IncomingSessionFuture::new(self.0)
}
}
/// [`Future`] for an in-progress incoming connection attempt.
///
/// Created by awaiting an [`IncomingSession`]
pub struct IncomingSessionFuture(Pin<Box<DynFutureIncomingSession>>);
impl IncomingSessionFuture {
/// Creates a future from [`quinn::Incoming`].
///
/// The returned future resolves to a [`SessionRequest`] once the QUIC connection
/// is established and the WebTransport session has been accepted.
#[cfg(feature = "quinn")]
#[cfg_attr(docsrs, doc(cfg(feature = "quinn")))]
pub fn with_quic_incoming(quic_incoming: quinn::Incoming) -> Self {
Self::new(quic_incoming)
}
/// Creates a future from [`quinn::Connecting`].
///
/// This is useful when a [`quinn::Connecting`] is already available and needs to be
/// driven to completion to accept an incoming WebTransport session.
///
/// The returned future resolves to a [`SessionRequest`] once the QUIC connection
/// is established and the WebTransport session has been accepted.
#[cfg(feature = "quinn")]
#[cfg_attr(docsrs, doc(cfg(feature = "quinn")))]
pub fn with_quic_connecting(quic_connecting: quinn::Connecting) -> Self {
Self(Box::pin(async move {
let quic_connection = quic_connecting.await?;
Self::accept(quic_connection).await
}))
}
fn new(quic_incoming: quinn::Incoming) -> Self {
Self(Box::pin(async move {
let quic_connection = quic_incoming.await?;
Self::accept(quic_connection).await
}))
}
async fn accept(quic_connection: quinn::Connection) -> Result<SessionRequest, ConnectionError> {
let driver = Driver::init(quic_connection.clone());
let _settings = driver.accept_settings().await.map_err(|driver_error| {
ConnectionError::with_driver_error(driver_error, &quic_connection)
})?;
// TODO(biagio): validate settings
let stream_session = driver.accept_session().await.map_err(|driver_error| {
ConnectionError::with_driver_error(driver_error, &quic_connection)
})?;
Ok(SessionRequest::new(quic_connection, driver, stream_session))
}
}
impl Future for IncomingSessionFuture {
type Output = Result<SessionRequest, ConnectionError>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
Future::poll(self.0.as_mut(), cx)
}
}
/// A incoming client session request.
///
/// Server should use methods [`accept`](Self::accept), [`forbidden`](Self::forbidden),
/// or [`not_found`](Self::not_found) in order to validate or reject the client request.
pub struct SessionRequest {
quic_connection: quinn::Connection,
driver: Driver,
stream_session: StreamSession,
}
impl SessionRequest {
pub(crate) fn new(
quic_connection: quinn::Connection,
driver: Driver,
stream_session: StreamSession,
) -> Self {
Self {
quic_connection,
driver,
stream_session,
}
}
/// Returns the peer's UDP address.
///
/// **Note**: as QUIC supports migration, remote address may change
/// during connection. Furthermore, when IPv6 support is enabled, IPv4
/// addresses may be mapped to IPv6.
#[inline(always)]
pub fn remote_address(&self) -> SocketAddr {
self.quic_connection.remote_address()
}
/// Returns the `:authority` field of the request.
pub fn authority(&self) -> &str {
self.stream_session.request().authority()
}
/// Returns the `:path` field of the request.
pub fn path(&self) -> &str {
self.stream_session.request().path()
}
/// Returns the `origin` field of the request if present.
pub fn origin(&self) -> Option<&str> {
self.stream_session.request().origin()
}
/// Returns the `user-agent` field of the request if present.
pub fn user_agent(&self) -> Option<&str> {
self.stream_session.request().user_agent()
}
/// Returns all header fields associated with the request.
pub fn headers(&self) -> &HashMap<String, String> {
self.stream_session.request().headers().as_ref()
}
/// Accepts the client request with headers.
pub async fn accept_with_headers<I, K, V>(
self,
headers: I,
) -> Result<Connection, ConnectionError>
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<String>,
{
self.accept_impl(
headers
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect(),
)
.await
}
/// Accepts the client request and it establishes the WebTransport session.
pub async fn accept(self) -> Result<Connection, ConnectionError> {
self.accept_impl(HashMap::new()).await
}
/// Rejects the client request by replying with `403` status code.
pub async fn forbidden(self) {
self.reject(SessionResponseProto::forbidden()).await;
}
/// Rejects the client request by replying with `404` status code.
pub async fn not_found(self) {
self.reject(SessionResponseProto::not_found()).await;
}
/// Rejects the client request by replying with `429` status code.
pub async fn too_many_requests(self) {
self.reject(SessionResponseProto::too_many_requests()).await;
}
async fn accept_impl(
mut self,
headers: HashMap<String, String>,
) -> Result<Connection, ConnectionError> {
let mut response = SessionResponseProto::ok();
for (key, value) in headers {
response.add(key, value);
}
self.send_response(response).await?;
let session_id = self.stream_session.session_id();
self.driver
.register_session(self.stream_session)
.await
.map_err(|driver_error| {
ConnectionError::with_driver_error(driver_error, &self.quic_connection)
})?;
Ok(Connection::new(
self.quic_connection,
self.driver,
session_id,
))
}
async fn reject(mut self, response: SessionResponseProto) {
let _ = self.send_response(response).await;
self.stream_session.finish().await;
}
async fn send_response(
&mut self,
response: SessionResponseProto,
) -> Result<(), ConnectionError> {
let frame = response.headers().generate_frame();
match self.stream_session.write_frame(frame).await {
Ok(()) => Ok(()),
Err(ProtoWriteError::NotConnected) => {
Err(ConnectionError::no_connect(&self.quic_connection))
}
Err(ProtoWriteError::Stopped) => {
self.quic_connection
.close(varint_w2q(ErrorCode::ClosedCriticalStream.to_code()), b"");
Err(ConnectionError::local_h3_error(
ErrorCode::ClosedCriticalStream,
))
}
}
}
}

View file

@ -0,0 +1,360 @@
use crate::driver::utils::varint_q2w;
use crate::driver::DriverError;
use crate::VarInt;
use std::borrow::Cow;
use std::fmt::Display;
use std::net::SocketAddr;
use wtransport_proto::error::ErrorCode;
/// An enumeration representing various errors that can occur during a WebTransport connection.
#[derive(thiserror::Error, Debug, Clone, Eq, PartialEq)]
pub enum ConnectionError {
/// The connection was aborted by the peer (protocol level).
#[error("connection aborted by peer: {0}")]
ConnectionClosed(ConnectionClose),
/// The peer closed the WebTransport session: with a WT_CLOSE_SESSION capsule, whose
/// application error code and reason this carries, or with a FIN on the CONNECT stream
/// alone, which reads as code 0 and no reason (draft-ietf-webtrans-http3-16 §6).
#[error("session closed by peer: {0}")]
SessionClosed(ApplicationClose),
/// The connection was closed by the peer (application level). Under HTTP/3 the code is
/// an HTTP/3 error code, not the session's.
#[error("connection closed by peer: {0}")]
ApplicationClosed(ApplicationClose),
/// The connection was locally closed.
#[error("connection locally closed")]
LocallyClosed,
/// The connection was locally closed because an HTTP3 protocol violation.
#[error("connection locally aborted: {0}")]
LocalH3Error(H3Error),
/// The connection timed out.
#[error("connection timed out")]
TimedOut,
/// The connection was closed because a QUIC protocol error.
#[error("QUIC protocol error: {0}")]
QuicProto(QuicProtoError),
/// The connection could not be created because not enough of the CID space is available
///
/// Try using longer connection IDs.
#[error("CIDs exhausted")]
CidsExhausted,
}
impl ConnectionError {
pub(crate) fn with_driver_error(
driver_error: DriverError,
quic_connection: &quinn::Connection,
) -> Self {
match driver_error {
DriverError::Proto(error_code) => Self::local_h3_error(error_code),
DriverError::ApplicationClosed(close) => Self::SessionClosed(close),
DriverError::LocallyClosed => Self::LocallyClosed,
DriverError::NotConnected => Self::no_connect(quic_connection),
}
}
pub(crate) fn no_connect(quic_connection: &quinn::Connection) -> Self {
match quic_connection.close_reason() {
Some(reason) => reason.into(),
None => {
// Connection still alive but driver reported NotConnected.
// This race condition can occur when driver threads are processing
// incoming streams while connection cleanup is triggered.
ConnectionError::LocallyClosed
}
}
}
pub(crate) fn local_h3_error(error_code: ErrorCode) -> Self {
ConnectionError::LocalH3Error(H3Error { code: error_code })
}
}
/// An enumeration representing various errors that can occur during a WebTransport client connecting.
#[derive(thiserror::Error, Debug)]
pub enum ConnectingError {
/// URL provided for connection is not valid.
#[error("invalid URL: {0}")]
InvalidUrl(String),
/// Failure during DNS resolution.
#[error("cannot resolve domain: {0}")]
DnsLookup(std::io::Error),
/// Cannot find any DNS.
#[error("cannot resolve domain")]
DnsNotFound,
/// Connection error during handshaking.
#[error(transparent)]
ConnectionError(ConnectionError),
/// Request rejected.
#[error("server rejected WebTransport session request")]
SessionRejected,
/// Cannot use reserved key for additional headers.
#[error("additional header '{0}' is reserved")]
ReservedHeader(String),
/// The endpoint can no longer create new connections
///
/// Indicates that a necessary component of the endpoint has been dropped or otherwise disabled.
#[error("endpoint stopping")]
EndpointStopping,
/// The connection could not be created because not enough of the CID space is available
///
/// Try using longer connection IDs
#[error("CIDs exhausted")]
CidsExhausted,
/// The server name supplied was malformed
#[error("invalid server name: {0}")]
InvalidServerName(String),
/// The remote [`SocketAddr`] supplied was malformed.
///
/// Examples include attempting to connect to port 0, or using an inappropriate address family.
#[error("invalid remote address: {0}")]
InvalidRemoteAddress(SocketAddr),
}
impl ConnectingError {
pub(crate) fn with_no_connection(quic_connection: &quinn::Connection) -> Self {
ConnectingError::ConnectionError(match quic_connection.close_reason() {
Some(reason) => reason.into(),
None => {
// Connection still alive but error reported.
// This race condition can occur when driver threads are processing
// incoming streams while connection cleanup is triggered.
ConnectionError::LocallyClosed
}
})
}
pub(crate) fn with_connect_error(error: quinn::ConnectError) -> Self {
match error {
quinn::ConnectError::EndpointStopping => ConnectingError::EndpointStopping,
quinn::ConnectError::CidsExhausted => ConnectingError::CidsExhausted,
quinn::ConnectError::InvalidServerName(name) => {
ConnectingError::InvalidServerName(name)
}
quinn::ConnectError::InvalidRemoteAddress(socket_addr) => {
ConnectingError::InvalidRemoteAddress(socket_addr)
}
quinn::ConnectError::NoDefaultClientConfig => {
unreachable!("quic client config is internally provided")
}
quinn::ConnectError::UnsupportedVersion => {
unreachable!("quic version is internally configured")
}
}
}
}
/// Error indicating that a stream has been already finished or reset.
#[derive(thiserror::Error, Debug)]
#[error("closed stream")]
pub struct ClosedStream;
/// An error that arise from writing to a stream.
#[derive(thiserror::Error, Debug, Clone, Eq, PartialEq)]
pub enum StreamWriteError {
/// Connection has been dropped.
#[error("not connected")]
NotConnected,
/// The stream was already finished or reset locally.
#[error("stream closed")]
Closed,
/// The peer is no longer accepting data on this stream.
#[error("stream stopped (code: {0})")]
Stopped(VarInt),
/// QUIC protocol error.
#[error("QUIC protocol error")]
QuicProto,
}
/// An error that arise from reading from a stream.
#[derive(thiserror::Error, Debug, Clone, Eq, PartialEq)]
pub enum StreamReadError {
/// Connection has been dropped.
#[error("not connected")]
NotConnected,
/// The peer abandoned transmitting data on this stream
#[error("stream reset (code: {0})")]
Reset(VarInt),
/// QUIC protocol error.
#[error("QUIC protocol error")]
QuicProto,
}
/// An error that arise from reading from a stream.
#[derive(thiserror::Error, Debug, Clone)]
pub enum StreamReadExactError {
/// The stream finished before all bytes were read.
#[error("stream finished too early ({0} bytes read)")]
FinishedEarly(usize),
/// A read error occurred.
#[error(transparent)]
Read(StreamReadError),
}
/// An error that arise from sending a datagram.
#[derive(thiserror::Error, Debug, Clone, Eq, PartialEq)]
pub enum SendDatagramError {
/// Connection has been dropped.
#[error("not connected")]
NotConnected,
/// The peer does not support receiving datagram frames.
#[error("peer does not support datagrams")]
UnsupportedByPeer,
/// The datagram is larger than the connection can currently accommodate.
#[error("datagram payload too large")]
TooLarge,
}
/// An error that arise when opening a new stream.
#[derive(thiserror::Error, Debug, Clone, Eq, PartialEq)]
pub enum StreamOpeningError {
/// Connection has been dropped.
#[error("not connected")]
NotConnected,
/// The peer refused the stream, stopping it during initialization.
#[error("opening stream refused")]
Refused,
}
/// Reason given by an application for closing the connection
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct ApplicationClose {
code: VarInt,
reason: Box<[u8]>,
}
impl Display for ApplicationClose {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.reason.is_empty() {
self.code.fmt(f)?;
} else {
f.write_str(&String::from_utf8_lossy(&self.reason))?;
f.write_str(" (code ")?;
self.code.fmt(f)?;
f.write_str(")")?;
}
Ok(())
}
}
impl ApplicationClose {
/// Creates a new application close reason.
pub(crate) fn new(code: VarInt, reason: Box<[u8]>) -> Self {
Self { code, reason }
}
/// Application-specific code for close operation.
pub fn code(&self) -> VarInt {
self.code
}
/// Data containing the reason for closing operation.
pub fn reason(&self) -> &[u8] {
&self.reason
}
}
/// Reason given by the transport for closing the connection.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConnectionClose(quinn::ConnectionClose);
impl Display for ConnectionClose {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
/// A struct representing an error in the HTTP3 layer.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct H3Error {
code: ErrorCode,
}
impl Display for H3Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.code.fmt(f)
}
}
impl From<quinn::ConnectionError> for ConnectionError {
fn from(error: quinn::ConnectionError) -> Self {
match error {
quinn::ConnectionError::VersionMismatch => ConnectionError::QuicProto(QuicProtoError {
code: None,
reason: Cow::Borrowed("QUIC protocol version mismatched"),
}),
quinn::ConnectionError::TransportError(e) => {
ConnectionError::QuicProto(QuicProtoError {
code: VarInt::try_from_u64(e.code.into()).ok(),
reason: Cow::Owned(e.reason),
})
}
quinn::ConnectionError::ConnectionClosed(close) => {
ConnectionError::ConnectionClosed(ConnectionClose(close))
}
quinn::ConnectionError::ApplicationClosed(close) => {
ConnectionError::ApplicationClosed(ApplicationClose {
code: varint_q2w(close.error_code),
reason: close.reason.to_vec().into_boxed_slice(),
})
}
quinn::ConnectionError::Reset => ConnectionError::QuicProto(QuicProtoError {
code: None,
reason: Cow::Borrowed("connection has been reset"),
}),
quinn::ConnectionError::TimedOut => ConnectionError::TimedOut,
quinn::ConnectionError::LocallyClosed => ConnectionError::LocallyClosed,
quinn::ConnectionError::CidsExhausted => ConnectionError::CidsExhausted,
}
}
}
/// A complete specification of an error over QUIC protocol.
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct QuicProtoError {
code: Option<VarInt>,
reason: Cow<'static, str>,
}
impl Display for QuicProtoError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let code = self
.code
.map(|code| format!(" (code: {code})"))
.unwrap_or_default();
f.write_fmt(format_args!("{}{}", self.reason, code))
}
}
/// Error returned by [`Connection::export_keying_material`](crate::Connection::export_keying_material).
///
/// This error occurs if the requested output length is too large.
#[derive(Debug, thiserror::Error, Clone, Eq, PartialEq)]
#[error("cannot derive keying material as requested output length is too large")]
pub struct ExportKeyingMaterialError;

View file

@ -0,0 +1,148 @@
//! WebTransport protocol implementation in *pure* Rust, *async-friendly*, and *API-simple*.
//!
//! For a quick start with this library, refer to [`Endpoint`].
//!
//! ## About WebTransport
//! WebTransport is a modern protocol built on [QUIC](https://en.wikipedia.org/wiki/QUIC)
//! and [HTTP/3](https://en.wikipedia.org/wiki/HTTP/3), providing an alternative to
//! HTTP and WebSocket.
//!
//! It's designed for efficient client-server communication with *low latency* and
//! bi-directional *multistream* data exchange capabilities, making it suitable for a wide range of
//! applications.
//!
//! WebTransport guarantees *secure* and *reliable* communication by leveraging encryption
//! and authentication to protect your data during transmission.
//!
//! WebTransport offers two key communication channels: *streams* and *datagrams*.
//!
//! ### Streams
//! WebTransport streams are communication channels that provide *ordered* and
//! *reliable* data transfer.
//!
//! WebTransport streams allow sending multiple sets of data at once within a single session.
//! Each stream operates independently, ensuring that the order and reliability
//! of one stream do not affect the others.
//!
//! Streams can be: *uni-directional* or *bi-directional*.
//!
//! *Order Preserved, Guaranteed Delivery, Flow-Controlled, Secure (All Traffic Encrypted),
//! and Multiple Independent Streams*.
//!
//! ## Datagrams
//! WebTransport datagrams are lightweight and *unordered* communication channels,
//! prioritizing quick data exchange without guarantees of reliability or sequence.
//!
//! *Unordered, No Guaranteed Delivery, No Flow-Controlled, Secure (All Traffic Encrypted),
//! Independent Messages*.
//!
//!
//! # Examples
//! Explore operational server and client examples below. The elegantly simple yet potent
//! API empowers you to get started with minimal code.
//!
//! ## Server
//! ```no_run
//! # use anyhow::Result;
//! use wtransport::Endpoint;
//! use wtransport::Identity;
//! use wtransport::ServerConfig;
//!
//! #[tokio::main]
//! async fn main() -> Result<()> {
//! let config = ServerConfig::builder()
//! .with_bind_default(4433)
//! .with_identity(Identity::load_pemfiles("cert.pem", "key.pem").await?)
//! .build();
//!
//! let server = Endpoint::server(config)?;
//!
//! loop {
//! let incoming_session = server.accept().await;
//! let incoming_request = incoming_session.await?;
//! let connection = incoming_request.accept().await?;
//! // ...
//! }
//! }
//! ```
//! See [repository server example](https://github.com/BiagioFesta/wtransport/blob/master/wtransport/examples/server.rs)
//! for the complete code.
//!
//! ## Client
//! ```no_run
//! # use anyhow::Result;
//! use wtransport::ClientConfig;
//! use wtransport::Endpoint;
//!
//! #[tokio::main]
//! async fn main() -> Result<()> {
//! let connection = Endpoint::client(ClientConfig::default())?
//! .connect("https://localhost:4433")
//! .await?;
//! // ...
//! # Ok(())
//! }
//! ```
//! See [repository client example](https://github.com/BiagioFesta/wtransport/blob/master/wtransport/examples/client.rs)
//! for the complete code.
#![cfg_attr(docsrs, feature(doc_cfg))]
#![warn(missing_docs, clippy::doc_markdown)]
/// Client and server configurations.
pub mod config;
/// WebTransport connection.
pub mod connection;
/// Endpoint module.
pub mod endpoint;
/// Errors definitions module.
pub mod error;
/// Interfaces for sending and receiving data.
pub mod stream;
/// TLS specific configurations.
pub mod tls;
/// Datagrams module.
pub mod datagram;
#[doc(inline)]
pub use config::ClientConfig;
#[doc(inline)]
pub use config::ServerConfig;
#[doc(inline)]
pub use tls::Identity;
#[doc(inline)]
pub use endpoint::Endpoint;
#[doc(inline)]
pub use connection::Connection;
#[doc(inline)]
pub use stream::RecvStream;
#[doc(inline)]
pub use stream::SendStream;
#[doc(inline)]
pub use wtransport_proto::varint::VarInt;
#[doc(inline)]
pub use wtransport_proto::ids::StreamId;
#[doc(inline)]
pub use wtransport_proto::ids::SessionId;
pub use wtransport_proto as proto;
#[cfg(feature = "quinn")]
#[cfg_attr(docsrs, doc(cfg(feature = "quinn")))]
pub use quinn;
mod driver;

View file

@ -0,0 +1,392 @@
use crate::driver::streams::bilocal::StreamBiLocalQuic;
use crate::driver::streams::unilocal::StreamUniLocalQuic;
use crate::driver::streams::ProtoWriteError;
use crate::driver::streams::QuicRecvStream;
use crate::driver::streams::QuicSendStream;
use crate::error::ClosedStream;
use crate::error::StreamOpeningError;
use crate::error::StreamReadError;
use crate::error::StreamReadExactError;
use crate::error::StreamWriteError;
use crate::SessionId;
use crate::StreamId;
use crate::VarInt;
use std::future::Future;
use std::pin::Pin;
use std::task::Context;
use std::task::Poll;
use tokio::io::ReadBuf;
use wtransport_proto::stream_header::StreamHeader;
/// A stream that can only be used to send data.
#[derive(Debug)]
pub struct SendStream(QuicSendStream);
impl SendStream {
#[inline(always)]
pub(crate) fn new(stream: QuicSendStream) -> Self {
Self(stream)
}
/// Writes bytes to the stream.
///
/// On success, returns the number of bytes written.
/// Congestion and flow control may cause this to be shorter than `buf.len()`,
/// indicating that only a prefix of `buf` was written.
#[inline(always)]
pub async fn write(&mut self, buf: &[u8]) -> Result<usize, StreamWriteError> {
self.0.write(buf).await
}
/// Convenience method to write an entire buffer to the stream.
#[inline(always)]
pub async fn write_all(&mut self, buf: &[u8]) -> Result<(), StreamWriteError> {
self.0.write_all(buf).await
}
/// Shuts down the send stream gracefully.
///
/// No new data may be written after calling this method. Completes when the peer has
/// acknowledged all sent data, retransmitting data as needed.
#[inline(always)]
pub async fn finish(&mut self) -> Result<(), StreamWriteError> {
self.0.finish().await
}
/// Returns the [`StreamId`] associated.
#[inline(always)]
pub fn id(&self) -> StreamId {
self.0.id()
}
/// Sets the priority of the send stream.
///
/// Every send stream has an initial priority of 0. Locally buffered 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 performance.
#[inline(always)]
pub fn set_priority(&self, priority: i32) {
self.0.set_priority(priority);
}
/// Gets the priority of the send stream.
///
/// # Panics
///
/// If `reset` was called.
#[inline(always)]
pub fn priority(&self) -> i32 {
self.0.priority()
}
/// Closes the send stream immediately.
///
/// No new data can be written after calling this method. Locally buffered data is dropped, and
/// previously transmitted data will no longer be retransmitted if lost. If an attempt has
/// already been made to finish the stream, the peer may still receive all written data.
///
/// If called more than once, subsequent calls will result in [`ClosedStream`] error.
#[inline(always)]
pub fn reset(&mut self, error_code: VarInt) -> Result<(), ClosedStream> {
self.0.reset(error_code)
}
/// Passively waits for the send stream to be stopped for any reason.
///
/// Returns [`StreamWriteError::Closed`] if the stream was already `finish`ed or `reset`.
///
/// Otherwise returns [`StreamWriteError::Stopped`] with an error code from the peer.
#[inline(always)]
pub async fn stopped(&mut self) -> StreamWriteError {
self.0.stopped().await
}
/// Returns a reference to the underlying QUIC stream.
#[cfg(feature = "quinn")]
#[cfg_attr(docsrs, doc(cfg(feature = "quinn")))]
#[inline(always)]
pub fn quic_stream(&self) -> &quinn::SendStream {
self.0.quic_stream()
}
/// Returns a mutable reference to the underlying QUIC stream.
#[cfg(feature = "quinn")]
#[cfg_attr(docsrs, doc(cfg(feature = "quinn")))]
#[inline(always)]
pub fn quic_stream_mut(&mut self) -> &mut quinn::SendStream {
self.0.quic_stream_mut()
}
}
/// A stream that can only be used to receive data.
#[derive(Debug)]
pub struct RecvStream(QuicRecvStream);
impl RecvStream {
#[inline(always)]
pub(crate) fn new(stream: QuicRecvStream) -> Self {
Self(stream)
}
/// Read data contiguously from the stream.
///
/// On success, returns the number of bytes read into `buf`.
#[inline(always)]
pub async fn read(&mut self, buf: &mut [u8]) -> Result<Option<usize>, StreamReadError> {
self.0.read(buf).await
}
/// Reads an exact number of bytes contiguously from the stream.
///
/// If the stream terminates before the entire length has been read, it
/// returns [`StreamReadExactError::FinishedEarly`].
pub async fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), StreamReadExactError> {
self.0.read_exact(buf).await
}
/// Stops accepting data on the stream.
///
/// Discards unread data and notifies the peer to stop transmitting.
pub fn stop(mut self, error_code: VarInt) {
let _ = self.0.stop(error_code);
}
/// Returns the [`StreamId`] associated.
#[inline(always)]
pub fn id(&self) -> StreamId {
self.0.id()
}
/// Returns a reference to the underlying QUIC stream.
#[cfg(feature = "quinn")]
#[cfg_attr(docsrs, doc(cfg(feature = "quinn")))]
#[inline(always)]
pub fn quic_stream(&self) -> &quinn::RecvStream {
self.0.quic_stream()
}
/// Returns a mutable reference to the underlying QUIC stream.
#[cfg(feature = "quinn")]
#[cfg_attr(docsrs, doc(cfg(feature = "quinn")))]
#[inline(always)]
pub fn quic_stream_mut(&mut self) -> &mut quinn::RecvStream {
self.0.quic_stream_mut()
}
}
/// A bidirectional stream composed of [`SendStream`] and [`RecvStream`].
///
/// `BiStream` is a utility particularly useful in situations where a generic
/// function or method expects a single object that must implement both
/// [`AsyncRead`](tokio::io::AsyncRead) and [`AsyncWrite`](tokio::io::AsyncWrite).
///
/// # Examples
///
/// ```
/// use tokio::io::AsyncRead;
/// use tokio::io::AsyncWrite;
/// use wtransport::stream::BiStream;
///
/// async fn do_operation<T>(io: T)
/// where
/// T: AsyncRead + AsyncWrite,
/// {
/// // ...
/// }
///
/// # use wtransport::Connection;
/// # async fn run(connection: Connection) {
/// let bi_stream = BiStream::join(connection.accept_bi().await.unwrap());
/// do_operation(bi_stream).await;
/// # }
/// ```
#[derive(Debug)]
pub struct BiStream((SendStream, RecvStream));
impl BiStream {
/// Joins a sending stream and a receiving stream into a single `BiStream` object.
pub fn join(s: (SendStream, RecvStream)) -> Self {
Self(s)
}
/// Splits the bidirectional stream into its sending and receiving stream handles.
pub fn split(self) -> (SendStream, RecvStream) {
self.0
}
/// Returns a reference to the inner [`SendStream`].
pub fn send(&self) -> &SendStream {
&self.0 .0
}
/// Returns a mutable reference to the inner [`SendStream`].
pub fn send_mut(&mut self) -> &mut SendStream {
&mut self.0 .0
}
/// Returns a reference to the inner [`RecvStream`].
pub fn recv(&self) -> &RecvStream {
&self.0 .1
}
/// Returns a mutable reference to the inner [`RecvStream`].
pub fn recv_mut(&mut self) -> &mut RecvStream {
&mut self.0 .1
}
}
impl From<(SendStream, RecvStream)> for BiStream {
fn from(value: (SendStream, RecvStream)) -> Self {
Self::join(value)
}
}
impl From<BiStream> for (SendStream, RecvStream) {
fn from(value: BiStream) -> Self {
value.split()
}
}
impl tokio::io::AsyncWrite for SendStream {
#[inline(always)]
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<std::io::Result<usize>> {
tokio::io::AsyncWrite::poll_write(Pin::new(&mut self.0), cx, buf)
}
#[inline(always)]
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
tokio::io::AsyncWrite::poll_flush(Pin::new(&mut self.0), cx)
}
#[inline(always)]
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
tokio::io::AsyncWrite::poll_shutdown(Pin::new(&mut self.0), cx)
}
#[inline(always)]
fn poll_write_vectored(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
bufs: &[std::io::IoSlice<'_>],
) -> Poll<Result<usize, std::io::Error>> {
tokio::io::AsyncWrite::poll_write_vectored(Pin::new(&mut self.0), cx, bufs)
}
#[inline(always)]
fn is_write_vectored(&self) -> bool {
tokio::io::AsyncWrite::is_write_vectored(&self.0)
}
}
impl tokio::io::AsyncRead for RecvStream {
#[inline(always)]
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
tokio::io::AsyncRead::poll_read(Pin::new(&mut self.0), cx, buf)
}
}
impl tokio::io::AsyncWrite for BiStream {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, std::io::Error>> {
tokio::io::AsyncWrite::poll_write(Pin::new(&mut self.0 .0), cx, buf)
}
fn poll_flush(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Result<(), std::io::Error>> {
tokio::io::AsyncWrite::poll_flush(Pin::new(&mut self.0 .0), cx)
}
fn poll_shutdown(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Result<(), std::io::Error>> {
tokio::io::AsyncWrite::poll_shutdown(Pin::new(&mut self.0 .0), cx)
}
}
impl tokio::io::AsyncRead for BiStream {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
tokio::io::AsyncRead::poll_read(Pin::new(&mut self.0 .1), cx, buf)
}
}
type DynFutureUniStream = dyn Future<Output = Result<SendStream, StreamOpeningError>> + Send + Sync;
/// [`Future`] for an in-progress opening unidirectional stream.
///
/// See [`Connection::open_uni`](crate::Connection::open_uni).
pub struct OpeningUniStream(Pin<Box<DynFutureUniStream>>);
impl OpeningUniStream {
pub(crate) fn new(session_id: SessionId, quic_stream: StreamUniLocalQuic) -> Self {
Self(Box::pin(async move {
match quic_stream
.upgrade(StreamHeader::new_webtransport(session_id))
.await
{
Ok(stream) => Ok(SendStream(stream.upgrade().into_stream())),
Err(ProtoWriteError::NotConnected) => Err(StreamOpeningError::NotConnected),
Err(ProtoWriteError::Stopped) => Err(StreamOpeningError::Refused),
}
}))
}
}
impl Future for OpeningUniStream {
type Output = Result<SendStream, StreamOpeningError>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
Future::poll(self.0.as_mut(), cx)
}
}
type DynFutureBiStream =
dyn Future<Output = Result<(SendStream, RecvStream), StreamOpeningError>> + Send + Sync;
/// [`Future`] for an in-progress opening bidirectional stream.
///
/// See [`Connection::open_bi`](crate::Connection::open_bi).
pub struct OpeningBiStream(Pin<Box<DynFutureBiStream>>);
impl OpeningBiStream {
pub(crate) fn new(session_id: SessionId, quic_stream: StreamBiLocalQuic) -> Self {
Self(Box::pin(async move {
match quic_stream.upgrade().upgrade(session_id).await {
Ok(stream) => {
let stream = stream.into_stream();
Ok((SendStream::new(stream.0), RecvStream::new(stream.1)))
}
Err(ProtoWriteError::NotConnected) => Err(StreamOpeningError::NotConnected),
Err(ProtoWriteError::Stopped) => Err(StreamOpeningError::Refused),
}
}))
}
}
impl Future for OpeningBiStream {
type Output = Result<(SendStream, RecvStream), StreamOpeningError>;
#[inline(always)]
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
Future::poll(self.0.as_mut(), cx)
}
}

File diff suppressed because it is too large Load diff

View file

@ -33,6 +33,10 @@ SKIP=(
# that workspace, against that workspace's lock; its own lock is the
# upstream's, kept with the vendored source.
"jouledesk/jouledesk-rs/vendor/libwebrtc"
# A vendored crate, patched into the joule-code workspace and into joule-moq-room.
# Cargo resolves it as part of each, against that workspace's lock; its own lock is
# the upstream's, kept with the vendored source.
"joule-code/joule-code-rs/vendor/wtransport"
)
skipped() { local d; for d in "${SKIP[@]}"; do [ "$d" = "$1" ] && return 0; done; return 1; }