diff --git a/.github/workflows/joule-code-conformance.yml b/.github/workflows/joule-code-conformance.yml index 129b18f02..aad2f6bbb 100644 --- a/.github/workflows/joule-code-conformance.yml +++ b/.github/workflows/joule-code-conformance.yml @@ -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 diff --git a/.github/workflows/joule-moq-room.yml b/.github/workflows/joule-moq-room.yml index d97393977..faa3f5e13 100644 --- a/.github/workflows/joule-moq-room.yml +++ b/.github/workflows/joule-moq-room.yml @@ -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' diff --git a/.github/workflows/joule-moq-web.yml b/.github/workflows/joule-moq-web.yml index 85f079ccb..fe48ff55c 100644 --- a/.github/workflows/joule-moq-web.yml +++ b/.github/workflows/joule-moq-web.yml @@ -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/**' diff --git a/joule-code/CHANGELOG.md b/joule-code/CHANGELOG.md index 317a29b4e..036c69fb4 100644 --- a/joule-code/CHANGELOG.md +++ b/joule-code/CHANGELOG.md @@ -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 diff --git a/joule-code/joule-code-rs/Cargo.lock b/joule-code/joule-code-rs/Cargo.lock index 81fce2b67..541e4dfff 100644 --- a/joule-code/joule-code-rs/Cargo.lock +++ b/joule-code/joule-code-rs/Cargo.lock @@ -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", diff --git a/joule-code/joule-code-rs/Cargo.toml b/joule-code/joule-code-rs/Cargo.toml index af6d8f1f3..3add5c1dc 100644 --- a/joule-code/joule-code-rs/Cargo.toml +++ b/joule-code/joule-code-rs/Cargo.toml @@ -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" } diff --git a/joule-code/joule-code-rs/crates/joule-moq-relay/README.md b/joule-code/joule-code-rs/crates/joule-moq-relay/README.md index c7c43410d..855868d1b 100644 --- a/joule-code/joule-code-rs/crates/joule-moq-relay/README.md +++ b/joule-code/joule-code-rs/crates/joule-moq-relay/README.md @@ -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 diff --git a/joule-code/joule-code-rs/crates/joule-moq-relay/src/session.rs b/joule-code/joule-code-rs/crates/joule-moq-relay/src/session.rs index 9256bb804..815910865 100644 --- a/joule-code/joule-code-rs/crates/joule-moq-relay/src/session.rs +++ b/joule-code/joule-code-rs/crates/joule-moq-relay/src/session.rs @@ -120,24 +120,33 @@ pub fn fetch_range(f: Option, 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) -> 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) + } + }; } } diff --git a/joule-code/joule-code-rs/crates/joule-moq-relay/src/transport/webtransport.rs b/joule-code/joule-code-rs/crates/joule-moq-relay/src/transport/webtransport.rs index 926947add..288c20d5e 100644 --- a/joule-code/joule-code-rs/crates/joule-moq-relay/src/transport/webtransport.rs +++ b/joule-code/joule-code-rs/crates/joule-moq-relay/src/transport/webtransport.rs @@ -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, + closing: Arc, +} + +/// 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>>, +} + +impl Closing { + /// Keep a stream while the session is closing; otherwise it is dropped here, as usual. + fn drop_or_hold(&self, h: Box) { + 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, Arc); +pub struct WtRecv(Option, Arc); + +impl WtSend { + fn new(s: wtransport::SendStream, closing: &Arc) -> 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) -> 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> { - 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> { - 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 { 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 { 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) diff --git a/joule-code/joule-code-rs/crates/joule-moq-relay/tests/webtransport_close.rs b/joule-code/joule-code-rs/crates/joule-moq-relay/tests/webtransport_close.rs new file mode 100644 index 000000000..6691e4600 --- /dev/null +++ b/joule-code/joule-code-rs/crates/joule-moq-relay/tests/webtransport_close.rs @@ -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 { + 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; +} diff --git a/joule-code/joule-code-rs/crates/joule-moq-room/Cargo.lock b/joule-code/joule-code-rs/crates/joule-moq-room/Cargo.lock index e6a1552ef..254932ac3 100644 --- a/joule-code/joule-code-rs/crates/joule-moq-room/Cargo.lock +++ b/joule-code/joule-code-rs/crates/joule-moq-room/Cargo.lock @@ -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", diff --git a/joule-code/joule-code-rs/crates/joule-moq-room/Cargo.toml b/joule-code/joule-code-rs/crates/joule-moq-room/Cargo.toml index fc70ad04e..705d8e909 100644 --- a/joule-code/joule-code-rs/crates/joule-moq-room/Cargo.toml +++ b/joule-code/joule-code-rs/crates/joule-moq-room/Cargo.toml @@ -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" diff --git a/joule-code/joule-code-rs/crates/joule-moq-web/e2e/room.e2e.mjs b/joule-code/joule-code-rs/crates/joule-moq-web/e2e/room.e2e.mjs index d34386fb2..56bc37b87 100644 --- a/joule-code/joule-code-rs/crates/joule-moq-web/e2e/room.e2e.mjs +++ b/joule-code/joule-code-rs/crates/joule-moq-web/e2e/room.e2e.mjs @@ -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`); diff --git a/joule-code/joule-code-rs/vendor/wtransport/.cargo_vcs_info.json b/joule-code/joule-code-rs/vendor/wtransport/.cargo_vcs_info.json new file mode 100644 index 000000000..f4bbaaed1 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "d022a6ad5f6260cb526e06d8ca42d09516ce6440" + }, + "path_in_vcs": "wtransport" +} \ No newline at end of file diff --git a/joule-code/joule-code-rs/vendor/wtransport/Cargo.lock b/joule-code/joule-code-rs/vendor/wtransport/Cargo.lock new file mode 100644 index 000000000..c74b349ab --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/Cargo.lock @@ -0,0 +1,1941 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-trait" +version = "0.1.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "aws-lc-fips-sys" +version = "0.13.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b00953a69b2cfb471d13d72538d2e66930832340b0f31deadd404b48c573c5" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", + "regex", +] + +[[package]] +name = "aws-lc-rs" +version = "1.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" +dependencies = [ + "aws-lc-fips-sys", + "aws-lc-sys", + "untrusted 0.7.1", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex 1.3.0", + "syn 2.0.119", +] + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex 2.0.1", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "rand_core", + "wasm-bindgen", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "httlib-huffman" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a9fcbcc408c5526c3ab80d534e5c86e7967c1fb7aa0a8c76abd1edc27deb877" + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "octets" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "866cb5af6f3aa3c1b44c3c2d79d22165fbb1b102e1b3fb499864bfe34736ec4b" + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rcgen" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57f6d249aad744e274e682777a50283a225a32705394ee6d5fcc01efa25e4055" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustls" +version = "0.23.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" +dependencies = [ + "aws-lc-rs", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wtransport" +version = "0.7.2" +dependencies = [ + "anyhow", + "axum", + "bytes", + "hyper", + "pem", + "quinn", + "rcgen", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "sha2", + "socket2", + "thiserror", + "time", + "tokio", + "tracing", + "tracing-subscriber", + "url", + "wtransport-proto", + "x509-parser", +] + +[[package]] +name = "wtransport-proto" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aad9059572c7dbd6901ccef37f3b7321678cd708dcf58a64b1921dbeab7bfede" +dependencies = [ + "httlib-huffman", + "octets", + "thiserror", + "url", +] + +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "aws-lc-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "ring", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "yasna" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" +dependencies = [ + "bit-vec", + "time", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/joule-code/joule-code-rs/vendor/wtransport/Cargo.toml b/joule-code/joule-code-rs/vendor/wtransport/Cargo.toml new file mode 100644 index 000000000..45151cf7b --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/Cargo.toml @@ -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" diff --git a/joule-code/joule-code-rs/vendor/wtransport/Cargo.toml.orig b/joule-code/joule-code-rs/vendor/wtransport/Cargo.toml.orig new file mode 100644 index 000000000..7c7db7aa4 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/Cargo.toml.orig @@ -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", +] diff --git a/joule-code/joule-code-rs/vendor/wtransport/LICENSE-APACHE b/joule-code/joule-code-rs/vendor/wtransport/LICENSE-APACHE new file mode 100644 index 000000000..d9a10c0d8 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/LICENSE-APACHE @@ -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 diff --git a/joule-code/joule-code-rs/vendor/wtransport/LICENSE-MIT b/joule-code/joule-code-rs/vendor/wtransport/LICENSE-MIT new file mode 100644 index 000000000..2070cd3d0 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/LICENSE-MIT @@ -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. diff --git a/joule-code/joule-code-rs/vendor/wtransport/README.md b/joule-code/joule-code-rs/vendor/wtransport/README.md new file mode 100644 index 000000000..5db50f419 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/README.md @@ -0,0 +1,139 @@ +

+ WTransport Logo +

+ +[![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. + +

+ Check Library Documentation +

+ +### 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 + + + + + + + + + +
Server Client
+ +```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?; + // ... +} +``` + + + +```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?; + // ... +} +``` + +
+ +## 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) diff --git a/joule-code/joule-code-rs/vendor/wtransport/VENDORED.md b/joule-code/joule-code-rs/vendor/wtransport/VENDORED.md new file mode 100644 index 000000000..2fd6db1d2 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/VENDORED.md @@ -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. diff --git a/joule-code/joule-code-rs/vendor/wtransport/src/config.rs b/joule-code/joule-code-rs/vendor/wtransport/src/config.rs new file mode 100644 index 000000000..12112d195 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/src/config.rs @@ -0,0 +1,1287 @@ +//! +//! This module defines configurations for the WebTransport server and client. +//! +//! It provides builders for creating server and client configurations with various options. +//! +//! The module includes: +//! - [`ServerConfig`]: Configuration for the WebTransport server. +//! - [`ClientConfig`]: Configuration for the WebTransport client. +//! +//! Example for creating a server configuration: +//! +//! ```no_run +//! # async fn run() -> anyhow::Result<()> { +//! use wtransport::Identity; +//! use wtransport::ServerConfig; +//! +//! let server_config = ServerConfig::builder() +//! .with_bind_default(443) +//! .with_identity(Identity::load_pemfiles("cert.pem", "key.pem").await?) +//! .build(); +//! +//! # Ok(()) +//! # } +//! ``` +//! +//! Example for creating a client configuration: +//! +//! ```no_run +//! use wtransport::ClientConfig; +//! +//! let client_config = ClientConfig::builder() +//! .with_bind_default() +//! .with_native_certs() +//! .build(); +//! ``` + +use crate::tls::build_native_cert_store; +use crate::tls::Identity; +use socket2::Domain as SocketDomain; +use socket2::Protocol as SocketProtocol; +use socket2::Socket; +use socket2::Type as SocketType; +use std::fmt::Debug; +use std::fmt::Display; +use std::future::Future; +use std::net::IpAddr; +use std::net::Ipv4Addr; +use std::net::Ipv6Addr; +use std::net::SocketAddr; +use std::net::SocketAddrV4; +use std::net::SocketAddrV6; +use std::net::UdpSocket; +use std::pin::Pin; +use std::sync::Arc; +use std::time::Duration; + +/// Alias of [`crate::tls::rustls::ServerConfig`]. +pub type TlsServerConfig = rustls::ServerConfig; + +/// Alias of [`crate::tls::rustls::ClientConfig`]. +pub type TlsClientConfig = rustls::ClientConfig; + +/// Alias of [`crate::quinn::TransportConfig`]. +#[cfg(feature = "quinn")] +#[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] +pub type QuicTransportConfig = quinn::TransportConfig; + +/// Alias of [`crate::quinn::ServerConfig`]. +#[cfg(feature = "quinn")] +#[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] +pub type QuicServerConfig = quinn::ServerConfig; + +/// Alias of [`crate::quinn::ClientConfig`]. +#[cfg(feature = "quinn")] +#[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] +pub type QuicClientConfig = quinn::ClientConfig; + +/// Configuration for IP address socket bind. +#[derive(Debug, Copy, Clone)] +pub enum IpBindConfig { + /// Bind to LOCALHOST IPv4 address (no IPv6). + LocalV4, + + /// Bind to LOCALHOST IPv6 address (no IPv4). + LocalV6, + + /// Bind to LOCALHOST both IPv4 and IPv6 address (dual stack, if supported). + LocalDual, + + /// Bind to `INADDR_ANY` IPv4 address (no IPv6). + InAddrAnyV4, + + /// Bind to `INADDR_ANY` IPv6 address (no IPv4). + InAddrAnyV6, + + /// Bind to `INADDR_ANY` both IPv4 and IPv6 address (dual stack, if supported). + InAddrAnyDual, +} + +impl IpBindConfig { + fn into_ip(self) -> IpAddr { + match self { + IpBindConfig::LocalV4 => Ipv4Addr::LOCALHOST.into(), + IpBindConfig::LocalV6 => Ipv6Addr::LOCALHOST.into(), + IpBindConfig::LocalDual => Ipv6Addr::LOCALHOST.into(), + IpBindConfig::InAddrAnyV4 => Ipv4Addr::UNSPECIFIED.into(), + IpBindConfig::InAddrAnyV6 => Ipv6Addr::UNSPECIFIED.into(), + IpBindConfig::InAddrAnyDual => Ipv6Addr::UNSPECIFIED.into(), + } + } + + fn into_dual_stack_config(self) -> Ipv6DualStackConfig { + match self { + IpBindConfig::LocalV4 | IpBindConfig::InAddrAnyV4 => Ipv6DualStackConfig::OsDefault, + IpBindConfig::LocalV6 | IpBindConfig::InAddrAnyV6 => Ipv6DualStackConfig::Deny, + IpBindConfig::LocalDual | IpBindConfig::InAddrAnyDual => Ipv6DualStackConfig::Allow, + } + } +} + +/// Configuration for IPv6 dual stack. +#[derive(Debug, Copy, Clone)] +pub enum Ipv6DualStackConfig { + /// Do not configure dual stack. Use OS's default. + OsDefault, + + /// Deny dual stack. This is equivalent to `IPV6_V6ONLY`. + /// + /// Socket will only bind for IPv6 (IPv4 port will still be available). + Deny, + + /// Allow dual stack. + /// + /// Please note that not all configurations/platforms support dual stack. + Allow, +} + +/// Invalid idle timeout. +pub struct InvalidIdleTimeout; + +/// Server configuration. +/// +/// You can create an instance of `ServerConfig` using its builder pattern by calling +/// the [`builder()`](Self::builder) method. +/// Once you have an instance, you can further customize it by chaining method calls +/// to set various configuration options. +/// +/// ## Configuration Builder States +/// +/// The configuration process follows a *state-based builder pattern*, where the server +/// configuration progresses through *3* states. +/// +/// ### 1. `WantsBindAddress` +/// +/// The caller must supply a binding address for the server. This is where to specify +/// the listening port of the server. +/// The following options are mutually exclusive: +/// +/// - [`with_bind_default`](ServerConfigBuilder::with_bind_default): the simplest +/// configuration where only the port will be specified. +/// - [`with_bind_config`](ServerConfigBuilder::with_bind_config): configures +/// binding to an address determined by a configuration preset. +/// - [`with_bind_address`](ServerConfigBuilder::with_bind_address): configures +/// binding to a custom-specified socket address. +/// - [`with_bind_address_v6`](ServerConfigBuilder::with_bind_address_v6): configures +/// binding to a custom-specified socket address for *IPv6*, along with the [dual stack +/// configuration](Ipv6DualStackConfig). +/// - [`with_bind_socket`](ServerConfigBuilder::with_bind_socket): configures +/// binding directly to a custom-specified socket. +/// +/// Only one of these options can be selected during the client configuration process. +/// +/// #### Examples: +/// +/// ``` +/// use wtransport::ServerConfig; +/// +/// // Configuration for accepting incoming connection on port 443 +/// ServerConfig::builder().with_bind_default(443); +/// ``` +/// +/// ### 2. `WantsIdentity` +/// +/// The caller must supply a TLS identity for the server. +/// +/// - [`with_identity`](ServerConfigBuilder::with_identity): configures +/// a TLS [`Identity`] for the server. +/// - [`with_custom_tls`](ServerConfigBuilder::with_custom_tls): sets the TLS +/// server configuration manually. +/// - [`with_custom_transport`](ServerConfigBuilder::with_custom_transport): sets the QUIC +/// transport configuration manually (using default TLS). +/// - [`with_custom_tls_and_transport`](ServerConfigBuilder::with_custom_tls_and_transport): sets both +/// a custom TLS and QUIC transport configuration. +/// - [`build_with_quic_config`](ServerConfigBuilder::build_with_quic_config): directly builds +/// [`ServerConfig`] providing both TLS and QUIC transport configuration given by +/// [`quic_config`](QuicServerConfig). +/// +/// #### Examples: +/// ``` +/// # use anyhow::Result; +/// use wtransport::Identity; +/// use wtransport::ServerConfig; +/// +/// # async fn run() -> Result<()> { +/// ServerConfig::builder() +/// .with_bind_default(443) +/// .with_identity(Identity::load_pemfiles("cert.pem", "key.pem").await?); +/// # Ok(()) +/// # } +/// ``` +/// +/// ### 3. `WantsTransportConfigServer` +/// +/// The caller can supply *additional* transport configurations. +/// Multiple options can be given at this stage. Once the configuration is completed, it is possible +/// to finalize with the method [`build()`](ServerConfigBuilder::build). +/// +/// All these options can be omitted in the configuration; default values will be used. +/// +/// - [`max_idle_timeout`](ServerConfigBuilder::max_idle_timeout) +/// - [`keep_alive_interval`](ServerConfigBuilder::keep_alive_interval) +/// - [`allow_migration`](ServerConfigBuilder::allow_migration) +/// +/// #### Examples: +/// ``` +/// # use anyhow::Result; +/// use wtransport::ServerConfig; +/// use wtransport::Identity; +/// use std::time::Duration; +/// +/// # async fn run() -> Result<()> { +/// let server_config = ServerConfig::builder() +/// .with_bind_default(443) +/// .with_identity(Identity::load_pemfiles("cert.pem", "key.pem").await?) +/// .keep_alive_interval(Some(Duration::from_secs(3))) +/// .build(); +/// # Ok(()) +/// # } +#[derive(Debug)] +pub struct ServerConfig { + pub(crate) bind_address_config: BindAddressConfig, + pub(crate) endpoint_config: quinn::EndpointConfig, + pub(crate) quic_config: quinn::ServerConfig, +} + +impl ServerConfig { + /// Creates a builder to build up the server configuration. + /// + /// For more information, see the [`ServerConfigBuilder`] documentation. + pub fn builder() -> ServerConfigBuilder { + ServerConfigBuilder::default() + } + + /// Returns a reference to the inner QUIC endpoint configuration. + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + pub fn quic_endpoint_config(&self) -> &quinn::EndpointConfig { + &self.endpoint_config + } + + /// Returns a mutable reference to the inner QUIC endpoint configuration. + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + pub fn quic_endpoint_config_mut(&mut self) -> &mut quinn::EndpointConfig { + &mut self.endpoint_config + } + + /// Returns a reference to the inner QUIC configuration. + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + pub fn quic_config(&self) -> &quinn::ServerConfig { + &self.quic_config + } + + /// Returns a mutable reference to the inner QUIC configuration. + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + pub fn quic_config_mut(&mut self) -> &mut quinn::ServerConfig { + &mut self.quic_config + } +} + +/// Server builder configuration. +/// +/// The builder might have different state at compile time. +/// +/// # Examples: +/// ```no_run +/// # async fn run() -> anyhow::Result<()> { +/// # use std::net::Ipv4Addr; +/// # use std::net::SocketAddr; +/// # use wtransport::Identity; +/// # use wtransport::ServerConfig; +/// let config = ServerConfig::builder() +/// .with_bind_default(4433) +/// .with_identity(Identity::load_pemfiles("cert.pem", "key.pem").await?); +/// # Ok(()) +/// # } +/// ``` +#[must_use] +pub struct ServerConfigBuilder(State); + +impl ServerConfigBuilder { + /// Configures for accepting incoming connections binding ANY IP (allowing IP dual-stack). + /// + /// `listening_port` is the port where the server will accept incoming connections. + /// + /// This is equivalent to: [`Self::with_bind_config`] with [`IpBindConfig::InAddrAnyDual`]. + pub fn with_bind_default( + self, + listening_port: u16, + ) -> ServerConfigBuilder { + self.with_bind_config(IpBindConfig::InAddrAnyDual, listening_port) + } + + /// Sets the binding (local) socket address with a specific [`IpBindConfig`]. + /// + /// `listening_port` is the port where the server will accept incoming connections. + pub fn with_bind_config( + self, + ip_bind_config: IpBindConfig, + listening_port: u16, + ) -> ServerConfigBuilder { + let ip_address: IpAddr = ip_bind_config.into_ip(); + + match ip_address { + IpAddr::V4(ip) => self.with_bind_address(SocketAddr::new(ip.into(), listening_port)), + IpAddr::V6(ip) => self.with_bind_address_v6( + SocketAddrV6::new(ip, listening_port, 0, 0), + ip_bind_config.into_dual_stack_config(), + ), + } + } + + /// Sets the binding (local) socket address for the endpoint. + pub fn with_bind_address( + self, + address: SocketAddr, + ) -> ServerConfigBuilder { + ServerConfigBuilder(states::WantsIdentity { + bind_address_config: BindAddressConfig::from(address), + }) + } + + /// Sets the binding (local) socket address for the endpoint with Ipv6 address. + /// + /// `dual_stack_config` allows/denies dual stack port binding. + pub fn with_bind_address_v6( + self, + address: SocketAddrV6, + dual_stack_config: Ipv6DualStackConfig, + ) -> ServerConfigBuilder { + ServerConfigBuilder(states::WantsIdentity { + bind_address_config: BindAddressConfig::AddressV6(address, dual_stack_config), + }) + } + + /// Configures the server to bind to a pre-existing [`UdpSocket`]. + /// + /// This allows the server to use an already created socket, which may be beneficial + /// for scenarios where socket reuse or specific socket configuration is needed. + pub fn with_bind_socket(self, socket: UdpSocket) -> ServerConfigBuilder { + ServerConfigBuilder(states::WantsIdentity { + bind_address_config: BindAddressConfig::Socket(socket), + }) + } +} + +impl ServerConfigBuilder { + /// Configures TLS with safe defaults and a TLS [`Identity`]. + /// + /// # Example + /// ```no_run + /// use wtransport::Identity; + /// use wtransport::ServerConfig; + /// # use anyhow::Result; + /// + /// # async fn run() -> Result<()> { + /// let identity = Identity::load_pemfiles("cert.pem", "key.pem").await?; + /// + /// let server_config = ServerConfig::builder() + /// .with_bind_default(4433) + /// .with_identity(identity) + /// .build(); + /// # Ok(()) + /// # } + /// ``` + pub fn with_identity( + self, + identity: Identity, + ) -> ServerConfigBuilder { + use crate::tls::server::build_default_tls_config; + + let tls_config = build_default_tls_config(identity); + let quic_endpoint_config = quinn::EndpointConfig::default(); + let quic_transport_config = quinn::TransportConfig::default(); + + self.with(tls_config, quic_endpoint_config, quic_transport_config) + } + + /// Allows for manual configuration of a custom TLS setup using a provided + /// [`rustls::ServerConfig`], which must support + /// [`rustls::CipherSuite::TLS13_AES_128_GCM_SHA256`]. A suitable configuration + /// can be obtained using the `ring` crypto provider with a set of versions containing + /// [`rustls::version::TLS13`]. + /// + /// This method is provided for advanced users who need fine-grained control over the + /// TLS configuration. It allows you to pass a preconfigured [`rustls::ServerConfig`] + /// instance to customize the TLS settings according to your specific requirements. + /// + /// Generally, it is recommended to use the [`with_identity`](Self::with_identity) method + /// to configure TLS with safe defaults and an TLS [`Identity`]. + /// + /// # Example + /// + /// ```no_run + /// use wtransport::tls::rustls; + /// use wtransport::ServerConfig; + /// + /// // Create a custom rustls::ServerConfig with specific TLS settings + /// let custom_tls_config = rustls::ServerConfig::builder(); + /// // Customize TLS settings here... + /// # let custom_tls_config = custom_tls_config + /// # .with_no_client_auth() + /// # .with_single_cert(todo!(), todo!()).unwrap(); + /// + /// // Create a ServerConfigBuilder with the custom TLS configuration + /// let server_config = ServerConfig::builder() + /// .with_bind_default(4433) + /// .with_custom_tls(custom_tls_config) + /// .build(); + /// ``` + pub fn with_custom_tls( + self, + tls_config: TlsServerConfig, + ) -> ServerConfigBuilder { + let quic_endpoint_config = quinn::EndpointConfig::default(); + let quic_transport_config = quinn::TransportConfig::default(); + + self.with(tls_config, quic_endpoint_config, quic_transport_config) + } + + /// Configures the server with a custom QUIC transport configuration and a default TLS setup + /// using the provided [`Identity`]. + /// + /// This method is useful for scenarios where you need to customize the transport settings + /// while relying on a default TLS configuration built from an [`Identity`]. It gives you + /// control over the transport layer while maintaining safe and standard TLS settings. + /// + /// **See**: [`with_identity`](Self::with_identity) + /// for a simpler configuration option that does not require custom transport settings. + /// + /// # Parameters + /// + /// - `identity`: A reference to an [`Identity`] that contains the server's certificate and + /// private key. This will be used to generate the default TLS configuration. + /// - `quic_transport_config`: A custom [`QuicTransportConfig`] instance that allows you to specify + /// various QUIC transport-layer settings according to your requirements. + /// + /// # Example + /// + /// ``` + /// use wtransport::config::QuicTransportConfig; + /// use wtransport::Identity; + /// use wtransport::ServerConfig; + /// + /// // Generate a server identity (self signed certificate and private key) + /// let identity = Identity::self_signed(["localhost", "127.0.0.1", "::1"]).unwrap(); + /// + /// // Create a custom QuicTransportConfig with specific settings + /// let mut custom_transport_config = QuicTransportConfig::default(); + /// custom_transport_config.datagram_send_buffer_size(1024); + /// + /// // Create a ServerConfigBuilder with the custom transport configuration and default TLS settings + /// let server_config = ServerConfig::builder() + /// .with_bind_default(4433) + /// .with_custom_transport(identity, custom_transport_config) + /// .build(); + /// ``` + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + pub fn with_custom_transport( + self, + identity: Identity, + quic_transport_config: QuicTransportConfig, + ) -> ServerConfigBuilder { + use crate::tls::server::build_default_tls_config; + + let tls_config = build_default_tls_config(identity); + let quic_endpoint_config = quinn::EndpointConfig::default(); + + self.with(tls_config, quic_endpoint_config, quic_transport_config) + } + + /// Configures the server with both a custom TLS configuration and a custom QUIC transport + /// configuration. + /// + /// This method is designed for advanced users who require full control over both the TLS + /// and transport settings. It allows you to pass a preconfigured [`TlsServerConfig`] and + /// a custom [`QuicTransportConfig`] to fine-tune both layers of the server configuration. + /// + /// # Parameters + /// + /// - `tls_config`: A custom [`TlsServerConfig`] instance that allows you to specify + /// detailed TLS settings, such as ciphersuites, certificate verification, and more. It must + /// support TLS 1.3 (see the documentation of [`Self::with_custom_tls`]). + /// - `quic_transport_config`: A custom [`QuicTransportConfig`] instance that allows you to specify + /// various QUIC transport-layer settings according to your requirements. + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + pub fn with_custom_tls_and_transport( + self, + tls_config: TlsServerConfig, + quic_transport_config: QuicTransportConfig, + ) -> ServerConfigBuilder { + let quic_endpoint_config = quinn::EndpointConfig::default(); + self.with(tls_config, quic_endpoint_config, quic_transport_config) + } + + /// Directly builds [`ServerConfig`] skipping TLS and transport configuration. + /// + /// Both TLS and transport configuration is given by [`quic_config`](QuicServerConfig). + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + pub fn build_with_quic_config(self, quic_config: QuicServerConfig) -> ServerConfig { + ServerConfig { + bind_address_config: self.0.bind_address_config, + endpoint_config: quinn::EndpointConfig::default(), + quic_config, + } + } + + fn with( + self, + tls_config: TlsServerConfig, + endpoint_config: quinn::EndpointConfig, + transport_config: quinn::TransportConfig, + ) -> ServerConfigBuilder { + ServerConfigBuilder(states::WantsTransportConfigServer { + bind_address_config: self.0.bind_address_config, + tls_config, + endpoint_config, + transport_config, + migration: true, + }) + } +} + +impl ServerConfigBuilder { + /// Completes configuration process. + /// + /// # Panics + /// + /// See the documentation of [`Self::with_custom_tls`] for the TLS 1.3 requirement. + #[must_use] + pub fn build(self) -> ServerConfig { + let crypto: Arc = Arc::new( + quinn::crypto::rustls::QuicServerConfig::try_from(self.0.tls_config) + .expect("CipherSuite::TLS13_AES_128_GCM_SHA256 missing"), + ); + + let mut quic_config = quinn::ServerConfig::with_crypto(crypto); + + quic_config.transport_config(Arc::new(self.0.transport_config)); + quic_config.migration(self.0.migration); + + ServerConfig { + bind_address_config: self.0.bind_address_config, + endpoint_config: self.0.endpoint_config, + quic_config, + } + } + + /// Maximum duration of inactivity to accept before timing out the connection. + /// + /// The true idle timeout is the minimum of this and the peer's own max idle timeout. `None` + /// represents an infinite timeout. + /// + /// **WARNING**: If a peer or its network path malfunctions or acts maliciously, an infinite + /// idle timeout can result in permanently hung futures! + pub fn max_idle_timeout( + mut self, + idle_timeout: Option, + ) -> Result { + let idle_timeout = idle_timeout + .map(quinn::IdleTimeout::try_from) + .transpose() + .map_err(|_| InvalidIdleTimeout)?; + + self.0.transport_config.max_idle_timeout(idle_timeout); + + Ok(self) + } + + /// Period of inactivity before sending a keep-alive packet + /// + /// Keep-alive packets prevent an inactive but otherwise healthy connection from timing out. + /// + /// `None` to disable, which is the default. Only one side of any given connection needs keep-alive + /// enabled for the connection to be preserved. Must be set lower than the + /// [`max_idle_timeout`](Self::max_idle_timeout) of both peers to be effective. + pub fn keep_alive_interval(mut self, interval: Option) -> Self { + self.0.transport_config.keep_alive_interval(interval); + self + } + + /// Whether to allow clients to migrate to new addresses. + /// + /// Improves behavior for clients that move between different internet connections or suffer NAT + /// rebinding. Enabled by default. + pub fn allow_migration(mut self, value: bool) -> Self { + self.0.migration = value; + self + } +} + +/// Client configuration. +/// +/// +/// You can create an instance of `ClientConfig` using its builder pattern by calling +/// the [`builder()`](Self::builder) method. +/// Once you have an instance, you can further customize it by chaining method calls +/// to set various configuration options. +/// +/// ## Configuration Builder States +/// +/// The configuration process follows a *state-based builder pattern*, where the client +/// configuration progresses through *3* states. +/// +/// ### 1. `WantsBindAddress` +/// +/// The caller must supply a binding address for the client. +/// The following options are mutually exclusive: +/// +/// - [`with_bind_default`](ClientConfigBuilder::with_bind_default): configures to use +/// the default bind address. This is generally the *default* choice for a client. +/// - [`with_bind_config`](ClientConfigBuilder::with_bind_config): configures +/// binding to an address determined by a configuration preset. +/// - [`with_bind_address`](ClientConfigBuilder::with_bind_address): configures +/// binding to a custom-specified socket address. +/// - [`with_bind_address_v6`](ClientConfigBuilder::with_bind_address_v6): configures +/// binding to a custom-specified socket address for *IPv6*, along with the [dual stack +/// configuration](Ipv6DualStackConfig). +/// - [`with_bind_socket`](ClientConfigBuilder::with_bind_socket): configures +/// binding directly to a custom-specified socket. +/// +/// Only one of these options can be selected during the client configuration process. +/// +/// #### Examples: +/// +/// ``` +/// use wtransport::ClientConfig; +/// +/// ClientConfig::builder().with_bind_default(); +/// ``` +/// +/// ### 2. `WantsRootStore` +/// +/// The caller must supply a TLS root store configuration for server certificate validation. +/// The following options are mutually exclusive: +/// +/// - [`with_native_certs`](ClientConfigBuilder::with_native_certs): configures to use +/// root certificates found in the platform's native certificate store. This is the *default* +/// configuration as it uses root store installed on the current machine. +/// - [`with_server_certificate_hashes`][cert_hashes]: configures the client to accept +/// *some* certificates mapped to hashes. This can be used to connect to self signed +/// certificates securely, where the hash of the certificate is shared in advance +/// through some other mechanism (such as an invite link). +/// - (**insecure**) [`with_no_cert_validation`](ClientConfigBuilder::with_no_cert_validation): +/// configure to skip server certificate validation. This might be handy for testing purpose +/// to accept *self-signed* certificate, but you should almost always prefer +/// [`with_server_certificate_hashes`][cert_hashes] for that use case. +/// - [`with_custom_tls`](ClientConfigBuilder::with_custom_tls): sets the TLS client +/// configuration manually. +/// - [`with_custom_transport`](ClientConfigBuilder::with_custom_transport): sets the QUIC +/// transport configuration manually (using default TLS). +/// - [`with_custom_tls_and_transport`](ClientConfigBuilder::with_custom_tls_and_transport): sets both +/// a custom TLS and QUIC transport configuration. +/// - [`build_with_quic_config`](ClientConfigBuilder::build_with_quic_config): directly builds +/// [`ClientConfig`] providing both TLS and QUIC transport configuration given by +/// [`quic_config`](QuicClientConfig). +/// +/// Only one of these options can be selected during the client configuration process. +/// +/// [cert_hashes]: ClientConfigBuilder::with_server_certificate_hashes +/// +/// #### Examples: +/// ``` +/// use wtransport::ClientConfig; +/// +/// ClientConfig::builder() +/// .with_bind_default() +/// .with_native_certs(); +/// ``` +/// +/// ### 3. `WantsTransportConfigClient` +/// +/// The caller can supply *additional* transport configurations. +/// Multiple options can be given at this stage. Once the configuration is completed, it is possible +/// to finalize with the method [`build()`](ClientConfigBuilder::build). +/// +/// All these options can be omitted in the configuration; default values will be used. +/// +/// - [`max_idle_timeout`](ClientConfigBuilder::max_idle_timeout) +/// - [`keep_alive_interval`](ClientConfigBuilder::keep_alive_interval) +/// - [`dns_resolver`](ClientConfigBuilder::dns_resolver) +/// +/// #### Examples: +/// ``` +/// use std::time::Duration; +/// use wtransport::ClientConfig; +/// +/// let client_config = ClientConfig::builder() +/// .with_bind_default() +/// .with_native_certs() +/// .max_idle_timeout(Some(Duration::from_secs(30))) +/// .unwrap() +/// .keep_alive_interval(Some(Duration::from_secs(3))) +/// .build(); +/// ``` +#[derive(Debug)] +pub struct ClientConfig { + pub(crate) bind_address_config: BindAddressConfig, + pub(crate) endpoint_config: quinn::EndpointConfig, + pub(crate) quic_config: quinn::ClientConfig, + pub(crate) dns_resolver: Arc, +} + +impl ClientConfig { + /// Creates a builder to build up the client configuration. + /// + /// For more information, see the [`ClientConfigBuilder`] documentation. + pub fn builder() -> ClientConfigBuilder { + ClientConfigBuilder::default() + } + + /// Allows setting a custom [`DnsResolver`] for this configuration. + /// + /// Default resolver is [`TokioDnsResolver`]. + pub fn set_dns_resolver(&mut self, dns_resolver: R) + where + R: DnsResolver + Send + Sync + 'static, + { + self.dns_resolver = Arc::new(dns_resolver); + } + + /// Returns a reference to the inner QUIC endpoint configuration. + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + pub fn quic_endpoint_config(&self) -> &quinn::EndpointConfig { + &self.endpoint_config + } + + /// Returns a mutable reference to the inner QUIC endpoint configuration. + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + pub fn quic_endpoint_config_mut(&mut self) -> &mut quinn::EndpointConfig { + &mut self.endpoint_config + } + + /// Returns a reference to the inner QUIC configuration. + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + pub fn quic_config(&self) -> &quinn::ClientConfig { + &self.quic_config + } + + /// Returns a mutable reference to the inner QUIC configuration. + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + pub fn quic_config_mut(&mut self) -> &mut quinn::ClientConfig { + &mut self.quic_config + } +} + +impl Default for ClientConfig { + fn default() -> Self { + ClientConfig::builder() + .with_bind_default() + .with_native_certs() + .build() + } +} + +/// Client builder configuration. +/// +/// The builder might have different state at compile time. +/// +/// # Example +/// ```no_run +/// # use std::net::Ipv4Addr; +/// # use std::net::SocketAddr; +/// # use wtransport::ClientConfig; +/// let config = ClientConfig::builder().with_bind_default(); +/// ``` +#[must_use] +pub struct ClientConfigBuilder(State); + +impl ClientConfigBuilder { + /// Configures for connecting binding ANY IP (allowing IP dual-stack). + /// + /// Bind port will be randomly picked. + /// + /// This is equivalent to: [`Self::with_bind_config`] with [`IpBindConfig::InAddrAnyDual`]. + pub fn with_bind_default(self) -> ClientConfigBuilder { + self.with_bind_config(IpBindConfig::InAddrAnyDual) + } + + /// Sets the binding (local) socket address with a specific [`IpBindConfig`]. + /// + /// Bind port will be randomly picked. + pub fn with_bind_config( + self, + ip_bind_config: IpBindConfig, + ) -> ClientConfigBuilder { + let ip_address: IpAddr = ip_bind_config.into_ip(); + + match ip_address { + IpAddr::V4(ip) => self.with_bind_address(SocketAddr::new(ip.into(), 0)), + IpAddr::V6(ip) => self.with_bind_address_v6( + SocketAddrV6::new(ip, 0, 0, 0), + ip_bind_config.into_dual_stack_config(), + ), + } + } + + /// Sets the binding (local) socket address for the endpoint. + pub fn with_bind_address( + self, + address: SocketAddr, + ) -> ClientConfigBuilder { + ClientConfigBuilder(states::WantsRootStore { + bind_address_config: BindAddressConfig::from(address), + }) + } + + /// Sets the binding (local) socket address for the endpoint. + /// + /// `dual_stack_config` allows/denies dual stack port binding. + pub fn with_bind_address_v6( + self, + address: SocketAddrV6, + dual_stack_config: Ipv6DualStackConfig, + ) -> ClientConfigBuilder { + ClientConfigBuilder(states::WantsRootStore { + bind_address_config: BindAddressConfig::AddressV6(address, dual_stack_config), + }) + } + + /// Configures the client to bind to a pre-existing [`UdpSocket`]. + /// + /// This allows the client to use an already created socket, which can be useful in cases + /// where socket reuse or specific socket configurations are necessary. + pub fn with_bind_socket( + self, + socket: UdpSocket, + ) -> ClientConfigBuilder { + ClientConfigBuilder(states::WantsRootStore { + bind_address_config: BindAddressConfig::Socket(socket), + }) + } +} + +impl ClientConfigBuilder { + /// Configures the client to use native (local) root certificates for server validation. + /// + /// This method loads trusted root certificates from the system's certificate store, + /// ensuring that your client can trust certificates signed by well-known authorities. + /// + /// It configures safe default TLS configuration. + pub fn with_native_certs(self) -> ClientConfigBuilder { + use crate::tls::client::build_default_tls_config; + + let tls_config = build_default_tls_config(Arc::new(build_native_cert_store()), None); + let endpoint_config = quinn::EndpointConfig::default(); + let transport_config = quinn::TransportConfig::default(); + + self.with(tls_config, endpoint_config, transport_config) + } + + /// Configures the client to skip server certificate validation, potentially + /// compromising security. + /// + /// This method is intended for advanced users and should be used with caution. It + /// configures the client to bypass server certificate validation during the TLS + /// handshake, effectively trusting any server certificate presented, even if it is + /// not signed by a trusted certificate authority (CA). Using this method can expose + /// your application to security risks. + /// + /// # Safety Note + /// + /// Using [`with_no_cert_validation`] should only be considered when you have a + /// specific need to disable certificate validation. In most cases, it is strongly + /// recommended to validate server certificates using trusted root certificates + /// (e.g., [`with_native_certs`]) to ensure secure communication. + /// + /// However, this method can be useful in testing environments or situations where + /// you intentionally want to skip certificate validation for specific use cases. + /// + /// [`with_native_certs`]: #method.with_native_certs + /// [`with_no_cert_validation`]: #method.with_no_cert_validation + #[cfg(feature = "dangerous-configuration")] + #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-configuration")))] + pub fn with_no_cert_validation( + self, + ) -> ClientConfigBuilder { + use crate::tls::client::build_default_tls_config; + use crate::tls::client::NoServerVerification; + use rustls::RootCertStore; + + let tls_config = build_default_tls_config( + Arc::new(RootCertStore::empty()), + Some(Arc::new(NoServerVerification::new())), + ); + + let endpoint_config = quinn::EndpointConfig::default(); + let transport_config = quinn::TransportConfig::default(); + + self.with(tls_config, endpoint_config, transport_config) + } + + /// Configures the client to skip *some* server certificates validation. + /// + /// This method configures the client to accept server certificates + /// whose digests match the specified *SHA-256* hashes and fulfill + /// some additional constraints (*see notes below*). + /// + /// This is useful for scenarios where clients need to accept known + /// self-signed certificates or certificates from non-standard authorities. + /// + /// This method configuration is similar to the + /// [browser W3C WebTransport API](https://www.w3.org/TR/webtransport/#dom-webtransportoptions-servercertificatehashes). + /// + /// # Notes + /// + /// - The current time MUST be within the validity period of the certificate. + /// - The total length of the validity period MUST NOT exceed *two* weeks. + /// - Only certificates for which the public key algorithm is *ECDSA* with the *secp256r1* are accepted. + pub fn with_server_certificate_hashes( + self, + hashes: I, + ) -> ClientConfigBuilder + where + I: IntoIterator, + { + use crate::tls::client::build_default_tls_config; + use crate::tls::client::ServerHashVerification; + use rustls::RootCertStore; + + let tls_config = build_default_tls_config( + Arc::new(RootCertStore::empty()), + Some(Arc::new(ServerHashVerification::new(hashes))), + ); + + let endpoint_config = quinn::EndpointConfig::default(); + let transport_config = quinn::TransportConfig::default(); + + self.with(tls_config, endpoint_config, transport_config) + } + + /// Allows for manual configuration of a custom TLS setup using a provided + /// [`rustls::ClientConfig`], which must support + /// [`rustls::CipherSuite::TLS13_AES_128_GCM_SHA256`]. A suitable configuration + /// can be obtained using the `ring` crypto provider with a set of versions containing + /// [`rustls::version::TLS13`]. + /// + /// This method is provided for advanced users who need fine-grained control over the + /// TLS configuration. It allows you to pass a preconfigured [`rustls::ClientConfig`] + /// instance to customize the TLS settings according to your specific requirements. + /// + /// For most use cases, it is recommended to use the [`with_native_certs`](Self::with_native_certs) + /// method to configure TLS with safe defaults. + pub fn with_custom_tls( + self, + tls_config: TlsClientConfig, + ) -> ClientConfigBuilder { + let endpoint_config = quinn::EndpointConfig::default(); + let transport_config = quinn::TransportConfig::default(); + + self.with(tls_config, endpoint_config, transport_config) + } + + /// Similar to [`with_native_certs`](Self::with_native_certs), but it allows specifying a custom + /// QUIC transport configuration. + /// + /// # Parameters + /// + /// - `quic_transport_config`: A custom [`QuicTransportConfig`] instance that allows you to specify + /// various QUIC transport-layer settings according to your requirements. + /// + /// # Example + /// + /// ``` + /// use wtransport::config::QuicTransportConfig; + /// use wtransport::ClientConfig; + /// + /// // Create a custom QuicTransportConfig with specific settings + /// let mut custom_transport_config = QuicTransportConfig::default(); + /// custom_transport_config.datagram_send_buffer_size(1024); + /// + /// // Create a ClientConfigBuilder with the custom transport configuration and default TLS settings + /// let client_config = ClientConfig::builder() + /// .with_bind_default() + /// .with_custom_transport(custom_transport_config) + /// .build(); + /// ``` + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + pub fn with_custom_transport( + self, + quic_transport_config: QuicTransportConfig, + ) -> ClientConfigBuilder { + use crate::tls::client::build_default_tls_config; + + let tls_config = build_default_tls_config(Arc::new(build_native_cert_store()), None); + let quic_endpoint_config = quinn::EndpointConfig::default(); + + self.with(tls_config, quic_endpoint_config, quic_transport_config) + } + + /// Configures the client with both a custom TLS configuration and a custom QUIC transport + /// configuration. + /// + /// This method is designed for advanced users who require full control over both the TLS + /// and transport settings. It allows you to pass a preconfigured [`TlsClientConfig`] and + /// a custom [`QuicTransportConfig`] to fine-tune both layers of the server configuration. + /// + /// # Parameters + /// + /// - `tls_config`: A custom [`TlsClientConfig`] instance that allows you to specify + /// detailed TLS settings, such as ciphersuites, certificate verification, and more. It must + /// support TLS 1.3 (see the documentation of [`Self::with_custom_tls`]). + /// - `quic_transport_config`: A custom [`QuicTransportConfig`] instance that allows you to specify + /// various QUIC transport-layer settings according to your requirements. + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + pub fn with_custom_tls_and_transport( + self, + tls_config: TlsClientConfig, + quic_transport_config: QuicTransportConfig, + ) -> ClientConfigBuilder { + let quic_endpoint_config = quinn::EndpointConfig::default(); + self.with(tls_config, quic_endpoint_config, quic_transport_config) + } + + /// Directly builds [`ClientConfig`] skipping TLS and transport configuration. + /// + /// Both TLS and transport configuration is given by [`quic_config`](QuicClientConfig). + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + pub fn build_with_quic_config(self, quic_config: QuicClientConfig) -> ClientConfig { + ClientConfig { + bind_address_config: self.0.bind_address_config, + endpoint_config: quinn::EndpointConfig::default(), + quic_config, + dns_resolver: Arc::::default(), + } + } + + fn with( + self, + tls_config: TlsClientConfig, + endpoint_config: quinn::EndpointConfig, + transport_config: quinn::TransportConfig, + ) -> ClientConfigBuilder { + ClientConfigBuilder(states::WantsTransportConfigClient { + bind_address_config: self.0.bind_address_config, + tls_config, + endpoint_config, + transport_config, + dns_resolver: Arc::::default(), + }) + } +} + +impl ClientConfigBuilder { + /// Completes configuration process. + /// + /// # Panics + /// + /// See the documentation of [`Self::with_custom_tls`] for the TLS 1.3 requirement. + #[must_use] + pub fn build(self) -> ClientConfig { + let crypto = quinn::crypto::rustls::QuicClientConfig::try_from(self.0.tls_config) + .expect("CipherSuite::TLS13_AES_128_GCM_SHA256 missing"); + + let mut quic_config = quinn::ClientConfig::new(Arc::new(crypto)); + quic_config.transport_config(Arc::new(self.0.transport_config)); + + ClientConfig { + bind_address_config: self.0.bind_address_config, + endpoint_config: self.0.endpoint_config, + quic_config, + dns_resolver: self.0.dns_resolver, + } + } + + /// Maximum duration of inactivity to accept before timing out the connection. + /// + /// The true idle timeout is the minimum of this and the peer's own max idle timeout. `None` + /// represents an infinite timeout. + /// + /// **WARNING**: If a peer or its network path malfunctions or acts maliciously, an infinite + /// idle timeout can result in permanently hung futures! + pub fn max_idle_timeout( + mut self, + idle_timeout: Option, + ) -> Result { + let idle_timeout = idle_timeout + .map(quinn::IdleTimeout::try_from) + .transpose() + .map_err(|_| InvalidIdleTimeout)?; + + self.0.transport_config.max_idle_timeout(idle_timeout); + + Ok(self) + } + + /// Period of inactivity before sending a keep-alive packet + /// + /// Keep-alive packets prevent an inactive but otherwise healthy connection from timing out. + /// + /// `None` to disable, which is the default. Only one side of any given connection needs keep-alive + /// enabled for the connection to be preserved. Must be set lower than the + /// [`max_idle_timeout`](Self::max_idle_timeout) of both peers to be effective. + pub fn keep_alive_interval(mut self, interval: Option) -> Self { + self.0.transport_config.keep_alive_interval(interval); + self + } + + /// Sets the *DNS* resolver used during [`Endpoint::connect`](crate::Endpoint::connect). + /// + /// Default configuration uses [`TokioDnsResolver`]. + pub fn dns_resolver(mut self, dns_resolver: R) -> Self + where + R: DnsResolver + Send + Sync + 'static, + { + self.0.dns_resolver = Arc::new(dns_resolver); + self + } +} + +impl Default for ServerConfigBuilder { + fn default() -> Self { + Self(states::WantsBindAddress {}) + } +} + +impl Default for ClientConfigBuilder { + fn default() -> Self { + Self(states::WantsBindAddress {}) + } +} + +#[derive(Debug)] +pub(crate) enum BindAddressConfig { + AddressV4(SocketAddrV4), + AddressV6(SocketAddrV6, Ipv6DualStackConfig), + Socket(UdpSocket), +} + +impl BindAddressConfig { + pub(crate) fn bind_socket(self) -> std::io::Result { + let (bind_address, dual_stack_config) = match self { + BindAddressConfig::AddressV4(address) => { + (SocketAddr::from(address), Ipv6DualStackConfig::OsDefault) + } + BindAddressConfig::AddressV6(address, ipv6_dual_stack_config) => { + (SocketAddr::from(address), ipv6_dual_stack_config) + } + BindAddressConfig::Socket(socket) => { + return Ok(socket); + } + }; + + let domain = match bind_address { + SocketAddr::V4(_) => SocketDomain::IPV4, + SocketAddr::V6(_) => SocketDomain::IPV6, + }; + + let socket = Socket::new(domain, SocketType::DGRAM, Some(SocketProtocol::UDP))?; + + match dual_stack_config { + Ipv6DualStackConfig::OsDefault => {} + Ipv6DualStackConfig::Deny => socket.set_only_v6(true)?, + Ipv6DualStackConfig::Allow => socket.set_only_v6(false)?, + } + + socket.bind(&bind_address.into())?; + + Ok(UdpSocket::from(socket)) + } +} + +impl From for BindAddressConfig { + fn from(value: SocketAddr) -> Self { + match value { + SocketAddr::V4(address) => BindAddressConfig::AddressV4(address), + SocketAddr::V6(address) => { + BindAddressConfig::AddressV6(address, Ipv6DualStackConfig::OsDefault) + } + } + } +} + +/// State-types for client/server builder. +pub mod states { + use super::*; + + /// Config builder state where the caller must supply binding address. + pub struct WantsBindAddress {} + + /// Config builder state where the caller must supply TLS certificate. + pub struct WantsIdentity { + pub(super) bind_address_config: BindAddressConfig, + } + + /// Config builder state where the caller must supply TLS root store. + pub struct WantsRootStore { + pub(super) bind_address_config: BindAddressConfig, + } + + /// Config builder state where transport properties can be set. + pub struct WantsTransportConfigServer { + pub(super) bind_address_config: BindAddressConfig, + pub(super) tls_config: TlsServerConfig, + pub(super) endpoint_config: quinn::EndpointConfig, + pub(super) transport_config: quinn::TransportConfig, + pub(super) migration: bool, + } + + /// Config builder state where transport properties can be set. + pub struct WantsTransportConfigClient { + pub(super) bind_address_config: BindAddressConfig, + pub(super) tls_config: TlsClientConfig, + pub(super) endpoint_config: quinn::EndpointConfig, + pub(super) transport_config: quinn::TransportConfig, + pub(super) dns_resolver: Arc, + } +} + +/// Future resolving domain name. +/// +/// See [`DnsResolver::resolve`]. +pub trait DnsLookupFuture: Future>> + Send {} + +impl DnsLookupFuture for F where F: Future>> + Send {} + +/// A trait for asynchronously resolving domain names to IP addresses using DNS. +pub trait DnsResolver: Debug { + /// Resolves a domain name to one IP address. + fn resolve(&self, host: &str) -> Pin>; +} + +/// A DNS resolver implementation using the *Tokio* asynchronous runtime. +/// +/// Internally, it uses [`tokio::net::lookup_host`]. +#[derive(Default)] +pub struct TokioDnsResolver; + +impl DnsResolver for TokioDnsResolver { + fn resolve(&self, host: &str) -> Pin> { + let host = host.to_string(); + + Box::pin(async move { Ok(tokio::net::lookup_host(host).await?.next()) }) + } +} + +impl Debug for TokioDnsResolver { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TokioDnsResolver").finish() + } +} + +impl std::error::Error for InvalidIdleTimeout {} + +impl Debug for InvalidIdleTimeout { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("idle timeout value configuration is invalid") + } +} + +impl Display for InvalidIdleTimeout { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Debug::fmt(self, f) + } +} diff --git a/joule-code/joule-code-rs/vendor/wtransport/src/connection.rs b/joule-code/joule-code-rs/vendor/wtransport/src/connection.rs new file mode 100644 index 000000000..6cdd9ab7e --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/src/connection.rs @@ -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, + 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 { + 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 { + 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 { + 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 { + 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(&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 { + 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 { + self.quic_connection.peer_identity().map(|any| { + any.downcast::>>() + .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::() + .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 + } +} diff --git a/joule-code/joule-code-rs/vendor/wtransport/src/datagram.rs b/joule-code/joule-code-rs/vendor/wtransport/src/datagram.rs new file mode 100644 index 000000000..f9c87d442 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/src/datagram.rs @@ -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 { + 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..] + } +} diff --git a/joule-code/joule-code-rs/vendor/wtransport/src/driver/mod.rs b/joule-code/joule-code-rs/vendor/wtransport/src/driver/mod.rs new file mode 100644 index 000000000..0c16359ee --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/src/driver/mod.rs @@ -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>, + ready_sessions: BiChannelEndpoint, + ready_uni_wt_streams: Mutex>, + ready_bi_wt_streams: Mutex>, + ready_datagrams: Mutex>, + driver_result: SharedResultGet, + 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 { + self.driver_result.result().await + } + + fn closing(&self) -> bool { + self.session_closing.load(Ordering::Acquire) + } + + pub async fn accept_settings(&self) -> Result { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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, + ready_sessions: BiChannelEndpoint, + ready_uni_wt_streams: mpsc::Sender, + ready_bi_wt_streams: mpsc::Sender, + ready_datagrams: mpsc::Sender, + driver_result: SharedResultSet, + 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, + ready_sessions: BiChannelEndpoint, + ready_uni_wt_streams: mpsc::Sender, + ready_bi_wt_streams: mpsc::Sender, + ready_datagrams: mpsc::Sender, + driver_result: SharedResultSet, + 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>, + ready_uni_wt_streams: &mpsc::Sender, + ) -> 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, + ) -> 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, + ) -> 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; diff --git a/joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/connect.rs b/joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/connect.rs new file mode 100644 index 000000000..520c2e280 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/connect.rs @@ -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, +} + +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 { + 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 { + 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"); + } +} diff --git a/joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/mod.rs b/joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/mod.rs new file mode 100644 index 000000000..57e035667 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/mod.rs @@ -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 { + 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> { + 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> { + 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> { + 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> { + 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> { + 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, 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> { + 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> { + tokio::io::AsyncRead::poll_read(Pin::new(&mut self.0), cx, buf) + } +} + +#[derive(Debug)] +pub struct Stream { + 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 { + 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, 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 { + 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 { + 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; + + pub type StreamUniRemoteH3 = Stream; + + pub type StreamUniRemoteWT = Stream; + + impl StreamUniRemoteQuic { + pub async fn accept_uni(quic_connection: &quinn::Connection) -> Option { + let stream = quic_connection.accept_uni().await.ok()?; + Some(Self { + stream: QuicRecvStream(stream), + proto: StreamProto::accept_uni(), + }) + } + + pub async fn upgrade(mut self) -> Result { + 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, 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; + + pub type StreamUniLocalH3 = Stream; + + pub type StreamUniLocalWT = Stream; + + impl StreamUniLocalQuic { + pub async fn open_uni(quic_connection: &quinn::Connection) -> Option { + 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 { + 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, 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 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 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; diff --git a/joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/qpack.rs b/joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/qpack.rs new file mode 100644 index 000000000..3589837ca --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/qpack.rs @@ -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, + 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, + 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); + } + } + } + } +} diff --git a/joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/settings.rs b/joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/settings.rs new file mode 100644 index 000000000..8b02b6e71 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/settings.rs @@ -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, + 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, + settings: watch::Sender>, +} + +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, 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>); + +impl RemoteSettingsWatcher { + pub async fn accept_settings(&mut self) -> Option { + self.0.changed().await.ok()?; + + Some( + self.0 + .borrow() + .clone() + .expect("On change settings must be set"), + ) + } +} diff --git a/joule-code/joule-code-rs/vendor/wtransport/src/driver/utils.rs b/joule-code/joule-code-rs/vendor/wtransport/src/driver/utils.rs new file mode 100644 index 000000000..50e854aa7 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/src/driver/utils.rs @@ -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() -> (SharedResultSet, SharedResultGet) +where + T: Clone, +{ + let set = SharedResultSet::new(); + let get = set.subscribe(); + (set, get) +} + +#[derive(Debug, Clone)] +pub struct SharedResultSet(Arc>>); + +impl SharedResultSet +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 { + SharedResultGet(Mutex::new(self.0.subscribe())) + } +} + +#[derive(Debug)] +pub struct SharedResultGet(Mutex>>); + +impl SharedResultGet +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 { + 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 { + Full(T), + Closed(T), +} + +#[derive(Debug)] +pub struct BiChannelEndpoint { + sender: mpsc::Sender, + receiver: Mutex>, +} + +impl BiChannelEndpoint { + #[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> { + 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 { + self.receiver.lock().await.recv().await + } +} + +pub fn bichannel(capacity: usize) -> (BiChannelEndpoint, BiChannelEndpoint) { + 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(future: F) -> Option + where + F: Future, + { + PollOnce(Box::pin(future)).await + } + + struct PollOnce(Pin>); + + impl Future for PollOnce + where + F: Future, + { + type Output = Option; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + match Future::poll(self.0.as_mut(), cx) { + Poll::Ready(result) => Poll::Ready(Some(result)), + Poll::Pending => Poll::Ready(None), + } + } + } + } +} diff --git a/joule-code/joule-code-rs/vendor/wtransport/src/endpoint.rs b/joule-code/joule-code-rs/vendor/wtransport/src/endpoint.rs new file mode 100644 index 000000000..0e7d9e672 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/src/endpoint.rs @@ -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, + } +} + +/// 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 { + endpoint: quinn::Endpoint, + side: Side, +} + +impl Endpoint { + /// 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 { + 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 { + /// Constructs a *server* endpoint. + pub fn server(server_config: ServerConfig) -> std::io::Result { + 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 { + /// Constructs a *client* endpoint. + pub fn client(client_config: ClientConfig) -> std::io::Result { + 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) -> 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) -> 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) -> 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(&self, options: O) -> Result + 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) -> 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, +} + +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(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 { + &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, +} + +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(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 IntoConnectOptions for S +where + S: ToString, +{ + fn into_options(self) -> ConnectOptions { + ConnectOptions::builder(self).build() + } +} + +type DynFutureIncomingSession = + dyn Future> + 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; + + 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>); + +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 { + 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; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + 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 { + self.stream_session.request().headers().as_ref() + } + + /// Accepts the client request with headers. + pub async fn accept_with_headers( + self, + headers: I, + ) -> Result + where + I: IntoIterator, + K: Into, + V: Into, + { + 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 { + 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, + ) -> Result { + 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, + )) + } + } + } +} diff --git a/joule-code/joule-code-rs/vendor/wtransport/src/error.rs b/joule-code/joule-code-rs/vendor/wtransport/src/error.rs new file mode 100644 index 000000000..86002fd1c --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/src/error.rs @@ -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 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, + 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; diff --git a/joule-code/joule-code-rs/vendor/wtransport/src/lib.rs b/joule-code/joule-code-rs/vendor/wtransport/src/lib.rs new file mode 100644 index 000000000..a1915c1f1 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/src/lib.rs @@ -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; diff --git a/joule-code/joule-code-rs/vendor/wtransport/src/stream.rs b/joule-code/joule-code-rs/vendor/wtransport/src/stream.rs new file mode 100644 index 000000000..4528157df --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/src/stream.rs @@ -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 { + 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, 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(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 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> { + 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> { + 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> { + 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> { + 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> { + 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> { + 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> { + tokio::io::AsyncWrite::poll_flush(Pin::new(&mut self.0 .0), cx) + } + + fn poll_shutdown( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + 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> { + tokio::io::AsyncRead::poll_read(Pin::new(&mut self.0 .1), cx, buf) + } +} + +type DynFutureUniStream = dyn Future> + Send + Sync; + +/// [`Future`] for an in-progress opening unidirectional stream. +/// +/// See [`Connection::open_uni`](crate::Connection::open_uni). +pub struct OpeningUniStream(Pin>); + +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; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + Future::poll(self.0.as_mut(), cx) + } +} + +type DynFutureBiStream = + dyn Future> + Send + Sync; + +/// [`Future`] for an in-progress opening bidirectional stream. +/// +/// See [`Connection::open_bi`](crate::Connection::open_bi). +pub struct OpeningBiStream(Pin>); + +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 { + Future::poll(self.0.as_mut(), cx) + } +} diff --git a/joule-code/joule-code-rs/vendor/wtransport/src/tls.rs b/joule-code/joule-code-rs/vendor/wtransport/src/tls.rs new file mode 100644 index 000000000..dd2baec57 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/src/tls.rs @@ -0,0 +1,1373 @@ +use error::InvalidCertificate; +use error::InvalidDigest; +use error::PemLoadError; +use pem::encode as pem_encode; +use pem::Pem; +use rustls::RootCertStore; +use rustls_pki_types::pem::PemObject; +use rustls_pki_types::CertificateDer; +use rustls_pki_types::PrivateKeyDer; +use rustls_pki_types::PrivatePkcs8KeyDer; +use sha2::Digest; +use sha2::Sha256; +use std::fmt::Debug; +use std::fmt::Display; +use std::path::Path; +use std::str::FromStr; +use std::sync::Arc; +use x509_parser::certificate::X509Certificate; +use x509_parser::prelude::FromDer; + +pub use wtransport_proto::WEBTRANSPORT_ALPN; + +/// Represents an X.509 certificate. +#[derive(Clone)] +pub struct Certificate(CertificateDer<'static>); + +impl Certificate { + pub(crate) fn from_rustls_pki(cert: CertificateDer<'static>) -> Self { + Self(cert) + } + + /// Constructs a new `Certificate` from DER-encoded binary data. + pub fn from_der(der: Vec) -> Result { + X509Certificate::from_der(&der).map_err(|error| InvalidCertificate(error.to_string()))?; + Ok(Self(CertificateDer::from(der))) + } + + /// Loads the *first* certificate found in a PEM-encoded file. + /// + /// Filters out any PEM sections that are not certificate. + /// + /// Returns a [`PemLoadError::NoCertificateSection`] if no certificate is found in the file. + pub async fn load_pemfile(filepath: impl AsRef) -> Result { + let file_data = tokio::fs::read(filepath.as_ref()) + .await + .map_err(|io_error| PemLoadError::FileError { + file: filepath.as_ref().to_path_buf(), + error: io_error, + })?; + + let der = CertificateDer::from_pem_slice(&file_data).map_err(|error| { + if let rustls_pki_types::pem::Error::NoItemsFound = error { + PemLoadError::NoCertificateSection + } else { + PemLoadError::FileError { + file: filepath.as_ref().to_path_buf(), + error: std::io::Error::other(error), + } + } + })?; + + Self::from_der(der.to_vec()).map_err(|invalid_certificate| { + PemLoadError::InvalidCertificateChain { + index: 0, + error: invalid_certificate, + } + }) + } + + /// Stores the certificate in PEM format into a file asynchronously. + /// + /// If the file does not exist, it will be created. If the file exists, its contents + /// will be truncated before writing. + pub async fn store_pemfile(&self, filepath: impl AsRef) -> std::io::Result<()> { + use tokio::io::AsyncWriteExt; + + let mut file = tokio::fs::File::create(filepath).await?; + file.write_all(self.to_pem().as_bytes()).await?; + + Ok(()) + } + + /// Returns a reference to the DER-encoded binary data of the certificate. + pub fn der(&self) -> &[u8] { + &self.0 + } + + /// Retrieves the serial number of the certificate as a string. + pub fn serial(&self) -> String { + X509Certificate::from_der(&self.0) + .expect("valid der") + .1 + .raw_serial_as_string() + } + + /// Converts the X.509 certificate to the PEM (Privacy-Enhanced Mail) format. + /// + /// # Returns + /// A `String` containing the PEM-encoded representation of the certificate. + pub fn to_pem(&self) -> String { + pem_encode(&Pem::new("CERTIFICATE", self.der())) + } + + /// Computes certificate's *hash*. + /// + /// The hash is the *SHA-256* of the DER encoding of the certificate. + /// + /// This function can be used to make a *web* client accept a self-signed + /// certificate by using the [`WebTransportOptions.serverCertificateHashes`] W3C API. + /// + /// [`WebTransportOptions.serverCertificateHashes`]: https://www.w3.org/TR/webtransport/#dom-webtransportoptions-servercertificatehashes + pub fn hash(&self) -> Sha256Digest { + // TODO(biagio): you might consider use crypto provider from new rustls version + Sha256Digest(Sha256::digest(self.der()).into()) + } +} + +impl Debug for Certificate { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Certificate") + .field("serial", &self.serial()) + .finish() + } +} + +/// Represents a private key. +#[derive(Debug)] +pub struct PrivateKey(PrivateKeyDer<'static>); + +impl PrivateKey { + /// Constructs a new `PrivateKey` from DER-encoded PKCS#8 binary data. + pub fn from_der_pkcs8(der: Vec) -> Self { + PrivateKey(PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(der))) + } + + /// Loads the *first* private key found in a PEM-encoded file. + /// + /// Filters out any PEM sections that are not private key. + /// + /// Returns a [`PemLoadError::NoPrivateKeySection`] if no private key is found in the file. + pub async fn load_pemfile(filepath: impl AsRef) -> Result { + let file_data = tokio::fs::read(filepath.as_ref()) + .await + .map_err(|io_error| PemLoadError::FileError { + file: filepath.as_ref().to_path_buf(), + error: io_error, + })?; + + let private_key = PrivateKeyDer::from_pem_slice(&file_data).map_err(|error| { + if let rustls_pki_types::pem::Error::NoItemsFound = error { + PemLoadError::NoPrivateKeySection + } else { + PemLoadError::FileError { + file: filepath.as_ref().to_path_buf(), + error: std::io::Error::other(error), + } + } + })?; + + Ok(Self(private_key)) + } + + /// Stores the private key in PEM format into a file asynchronously. + /// + /// If the file does not exist, it will be created. If the file exists, + /// its contents will be truncated before writing. + pub async fn store_secret_pemfile(&self, filepath: impl AsRef) -> std::io::Result<()> { + tokio::fs::write(filepath, self.to_secret_pem()).await + } + + /// Returns a reference to the DER-encoded binary data of the private key. + pub fn secret_der(&self) -> &[u8] { + self.0.secret_der() + } + + /// Converts the private key to PEM format. + pub fn to_secret_pem(&self) -> String { + pem_encode(&Pem::new("PRIVATE KEY", self.secret_der())) + } + + /// Clones this private key. + /// + /// # Note + /// `PrivateKey` does not implement `Clone` directly to ensure that sensitive information + /// is not cloned inadvertently. + /// Implementing `Clone` directly could potentially lead to unintended cloning of sensitive data. + pub fn clone_key(&self) -> Self { + Self(self.0.clone_key()) + } +} + +/// A collection of [`Certificate`]. +#[derive(Clone, Debug)] +pub struct CertificateChain(Vec); + +impl CertificateChain { + /// Constructs a new `CertificateChain` from a vector of certificates. + pub fn new(certificates: Vec) -> Self { + Self(certificates) + } + + /// Constructs a new `CertificateChain` with a single certificate. + pub fn single(certificate: Certificate) -> Self { + Self::new(vec![certificate]) + } + + /// Loads a certificate chain from a PEM-encoded file. + /// + /// Filters out any PEM sections that are not certificates and yields error + /// if a problem occurs while trying to parse any certificate. + /// + /// *Note*: if the PEM file does not contain any certificate section this + /// will return an empty chain. + pub async fn load_pemfile(filepath: impl AsRef) -> Result { + let file_data = tokio::fs::read(filepath.as_ref()) + .await + .map_err(|io_error| PemLoadError::FileError { + file: filepath.as_ref().to_path_buf(), + error: io_error, + })?; + + let certificates = CertificateDer::pem_slice_iter(&file_data) + .enumerate() + .map(|(index, maybe_der)| match maybe_der { + Ok(der) => Certificate::from_der(der.to_vec()).map_err(|invalid_certificate| { + PemLoadError::InvalidCertificateChain { + index, + error: invalid_certificate, + } + }), + Err(error) => Err(PemLoadError::FileError { + file: filepath.as_ref().to_path_buf(), + error: std::io::Error::other(error), + }), + }) + .collect::, _>>()?; + + Ok(Self(certificates)) + } + + /// Stores the certificate chain in PEM format into a file asynchronously. + /// + /// If the file does not exist, it will be created. If the file exists, its contents + /// will be truncated before writing. + pub async fn store_pemfile(&self, filepath: impl AsRef) -> std::io::Result<()> { + use tokio::io::AsyncWriteExt; + + let mut file = tokio::fs::File::create(filepath).await?; + + for cert in self.0.iter() { + file.write_all(cert.to_pem().as_bytes()).await?; + } + + Ok(()) + } + + /// Returns a slice containing references to the certificates in the chain. + pub fn as_slice(&self) -> &[Certificate] { + &self.0 + } +} + +impl AsRef<[Certificate]> for CertificateChain { + fn as_ref(&self) -> &[Certificate] { + self.as_slice() + } +} + +impl FromIterator for CertificateChain { + fn from_iter>(iter: T) -> Self { + Self(iter.into_iter().collect()) + } +} + +/// Represents an TLS identity consisting of a certificate chain and a private key. +#[derive(Debug)] +pub struct Identity { + certificate_chain: CertificateChain, + private_key: PrivateKey, +} + +impl Identity { + /// Constructs a new `Identity` with the given certificate chain and private key. + pub fn new(certificate_chain: CertificateChain, private_key: PrivateKey) -> Self { + Self { + certificate_chain, + private_key, + } + } + + /// Loads an identity from PEM-encoded certificate and private key files. + pub async fn load_pemfiles( + cert_pemfile: impl AsRef, + private_key_pemfile: impl AsRef, + ) -> Result { + let certificate_chain = CertificateChain::load_pemfile(cert_pemfile).await?; + let private_key = PrivateKey::load_pemfile(private_key_pemfile).await?; + + Ok(Self::new(certificate_chain, private_key)) + } + + /// Generates a self-signed certificate and private key for new identity. + /// + /// The certificate conforms to the W3C WebTransport specifications as follows: + /// + /// - The certificate MUST be an *X.509v3* certificate as defined in *RFC5280*. + /// - The key used in the Subject Public Key field MUST be one of the allowed public key algorithms. + /// This function uses the `ECDSA P-256` algorithm. + /// - The current time MUST be within the validity period of the certificate as defined + /// in Section 4.1.2.5 of *RFC5280*. + /// - The total length of the validity period MUST NOT exceed two weeks. + /// + /// # Arguments + /// + /// * `subject_alt_names` - An iterator of strings representing subject alternative names (SANs). + /// They can be both hostnames or IP addresses. An error is returned + /// if DNS are not valid ASN.1 strings. + /// + /// # Examples + /// + /// ``` + /// use wtransport::Identity; + /// + /// let identity = Identity::self_signed(&["localhost", "127.0.0.1", "::1"]).unwrap(); + /// ``` + /// + /// The following example will return an error as *subject alternative name* is an invalid + /// string. + /// ```should_panic + /// use wtransport::Identity; + /// + /// Identity::self_signed(&["❤️"]).unwrap(); + /// ``` + /// + /// # Features + /// + /// This function is only available when the `self-signed` feature is enabled. + #[cfg(feature = "self-signed")] + #[cfg_attr(docsrs, doc(cfg(feature = "self-signed")))] + pub fn self_signed(subject_alt_names: I) -> Result + where + I: IntoIterator, + S: AsRef, + { + self_signed::SelfSignedIdentityBuilder::new() + .subject_alt_names(subject_alt_names) + .from_now_utc() + .validity_days(14) + .build() + } + + /// Creates a new [`SelfSignedIdentityBuilder`][1] instance. + /// + /// This function provides a convenient way to create a self-signed identity + /// builder, and thus generate a custom self-signed identity. + /// + /// # Example + /// + /// ``` + /// use wtransport::Identity; + /// + /// let identity = Identity::self_signed_builder() + /// .subject_alt_names(&["localhost", "127.0.0.1", "::1"]) + /// .from_now_utc() + /// .validity_days(14) + /// .build() + /// .unwrap(); + /// ``` + /// + /// # Features + /// + /// This function is only available when the `self-signed` feature is enabled. + /// + /// [1]: self_signed::SelfSignedIdentityBuilder + #[cfg(feature = "self-signed")] + #[cfg_attr(docsrs, doc(cfg(feature = "self-signed")))] + pub fn self_signed_builder( + ) -> self_signed::SelfSignedIdentityBuilder { + self_signed::SelfSignedIdentityBuilder::new() + } + + /// Returns a reference to the certificate chain associated with the identity. + pub fn certificate_chain(&self) -> &CertificateChain { + &self.certificate_chain + } + + /// Returns a reference to the private key associated with the identity. + pub fn private_key(&self) -> &PrivateKey { + &self.private_key + } + + /// Clones this identity. + /// + /// # Note + /// `Identity` does not implement `Clone` directly to ensure that sensitive information, + /// specifically the inner *private key*, is not cloned inadvertently. + /// Implementing `Clone` directly could potentially lead to unintended cloning of sensitive data. + pub fn clone_identity(&self) -> Self { + Self::new(self.certificate_chain.clone(), self.private_key.clone_key()) + } +} + +/// Represents the formatting options for displaying a SHA-256 digest. +#[derive(Debug, Copy, Clone)] +pub enum Sha256DigestFmt { + /// Represents the SHA-256 digest as an array of bytes. + /// + /// The string-format is as follows: `"[b0, b1, b2, ..., b31]"`, + /// where `b` is the byte represented as a *decimal* integer. + BytesArray, + + /// Represents the SHA-256 digest as a dotted hexadecimal string. + /// + /// The string-format is as follows: `"x0:x1:x2:...:x31"` where `x` + /// is the byte represented as a *hexadecimal* integer. + DottedHex, +} + +/// Represents a *SHA-256* digest, which is a fixed-size array of 32 bytes. +/// +/// For example, you can obtain the certificate digest with [`Certificate::hash`]. +#[derive(Debug, Clone, Eq, Hash, PartialEq, PartialOrd, Ord)] +pub struct Sha256Digest([u8; 32]); + +impl Sha256Digest { + /// Creates a new instance from a given array of 32 bytes. + /// + /// # Examples + /// + /// ``` + /// use wtransport::tls::Sha256Digest; + /// + /// // Create a new SHA-256 digest instance. + /// let digest = Sha256Digest::new([97; 32]); + /// ``` + pub fn new(bytes: [u8; 32]) -> Self { + Self(bytes) + } + + /// Attempts to create a new instance from a string representation. + /// + /// This method parses the string representation of the digest according to the provided + /// format (`fmt`) and constructs a new `Sha256Digest` instance if the parsing is successful. + /// + /// # Examples + /// + /// ``` + /// use wtransport::tls::Sha256Digest; + /// use wtransport::tls::Sha256DigestFmt; + /// + /// const HASH_ARRAY: &str = "[234, 204, 110, 153, 82, 177, 87, 232, 180, 125, \ + /// 234, 158, 66, 129, 212, 250, 217, 48, 47, 32, 83, \ + /// 133, 23, 44, 79, 198, 70, 118, 25, 153, 146, 142]"; + /// + /// let digest_bytes_array = Sha256Digest::from_str_fmt(HASH_ARRAY, Sha256DigestFmt::BytesArray); + /// assert!(digest_bytes_array.is_ok()); + /// + /// const HASH_HEX: &str = "e3:4e:c7:de:b8:da:2d:b8:3c:86:a0:11:76:40:75:b3:\ + /// b9:ba:9d:00:e0:04:b3:38:72:cd:a1:af:4e:e3:11:26"; + /// + /// let digest_dotted_hex = Sha256Digest::from_str_fmt(HASH_HEX, Sha256DigestFmt::DottedHex); + /// assert!(digest_dotted_hex.is_ok()); + /// + /// let invalid_digest = Sha256Digest::from_str_fmt("invalid_digest", Sha256DigestFmt::BytesArray); + /// assert!(invalid_digest.is_err()); + /// ``` + pub fn from_str_fmt(s: S, fmt: Sha256DigestFmt) -> Result + where + S: AsRef, + { + let bytes = match fmt { + Sha256DigestFmt::BytesArray => s + .as_ref() + .trim_start_matches('[') + .trim_end_matches(']') + .split(',') + .map(|byte| byte.trim().parse::().map_err(|_| InvalidDigest)) + .collect::, _>>()?, + + Sha256DigestFmt::DottedHex => s + .as_ref() + .split(':') + .map(|hex| u8::from_str_radix(hex.trim(), 16).map_err(|_| InvalidDigest)) + .collect::, _>>()?, + }; + + Ok(Self(bytes.try_into().map_err(|_| InvalidDigest)?)) + } + + /// Formats the digest into a human-readable representation based on the specified format. + /// + /// # Examples + /// + /// ``` + /// use wtransport::tls::Sha256Digest; + /// use wtransport::tls::Sha256DigestFmt; + /// + /// let digest = Sha256Digest::new([ + /// 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, + /// 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 64, + /// ]); + /// + /// // Represent the digest as a byte array string. + /// let bytes_array_fmt = digest.fmt(Sha256DigestFmt::BytesArray); + /// assert_eq!( + /// bytes_array_fmt, + /// "[97, 98, 99, 100, 101, 102, 103, 104, 105, 106, \ + /// 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, \ + /// 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, \ + /// 127, 64]" + /// ); + /// + /// // Represent the digest as a dotted hexadecimal string. + /// let dotted_hex_fmt = digest.fmt(Sha256DigestFmt::DottedHex); + /// assert_eq!( + /// dotted_hex_fmt, + /// "61:62:63:64:65:66:67:68:69:6a:6b:6c:6d:6e:6f:70:\ + /// 71:72:73:74:75:76:77:78:79:7a:7b:7c:7d:7e:7f:40" + /// ); + /// ``` + pub fn fmt(&self, fmt: Sha256DigestFmt) -> String { + match fmt { + Sha256DigestFmt::BytesArray => { + format!("{:?}", self.0) + } + Sha256DigestFmt::DottedHex => self + .0 + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::>() + .join(":"), + } + } +} + +impl From<[u8; 32]> for Sha256Digest { + fn from(value: [u8; 32]) -> Self { + Self::new(value) + } +} + +impl AsRef<[u8; 32]> for Sha256Digest { + fn as_ref(&self) -> &[u8; 32] { + &self.0 + } +} + +impl Display for Sha256Digest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.fmt(Sha256DigestFmt::DottedHex), f) + } +} + +/// Represents data related to a TLS handshake process. +#[derive(Clone, Debug)] +pub struct HandshakeData { + pub(crate) alpn: Option>, + pub(crate) server_name: Option, +} + +impl HandshakeData { + /// Application-Layer Protocol Negotiation (ALPN) data. + pub fn alpn(&self) -> Option<&[u8]> { + self.alpn.as_deref() + } + + /// The server name associated with the handshake data. + pub fn server_name(&self) -> Option<&str> { + self.server_name.as_deref() + } +} + +impl FromStr for Sha256Digest { + type Err = InvalidDigest; + + fn from_str(s: &str) -> Result { + Sha256Digest::from_str_fmt(s, Sha256DigestFmt::BytesArray) + .or_else(|_| Sha256Digest::from_str_fmt(s, Sha256DigestFmt::DottedHex)) + } +} + +/// Builds [`rustls::RootCertStore`] by using platform’s native certificate store. +/// +/// This function works on a *best-effort* basis, attempting to load every possible +/// root certificate found on the platform. It skips anchors that produce errors during loading. +/// Therefore, it might produce an *empty* root store. +/// +/// +/// It's important to note that this function can be expensive, as it might involve loading +/// all root certificates from the filesystem platform. Therefore, it's advisable to use it +/// sporadically and judiciously. +pub fn build_native_cert_store() -> RootCertStore { + let _vars_restore_guard = utils::remove_vars_tmp(["SSL_CERT_FILE", "SSL_CERT_DIR"]); + + let mut root_store = RootCertStore::empty(); + + let rustls_native_certs::CertificateResult { certs, .. } = + rustls_native_certs::load_native_certs(); + + for c in certs { + let _ = root_store.add(c); + } + + root_store +} + +fn default_crypto_provider() -> rustls::crypto::CryptoProvider { + #[cfg(feature = "ring")] + { + rustls::crypto::ring::default_provider() + } + + #[cfg(not(feature = "ring"))] + { + rustls::crypto::aws_lc_rs::default_provider() + } +} + +/// TLS configurations and utilities server-side. +pub mod server { + use super::*; + use rustls::ServerConfig as TlsServerConfig; + + /// Builds a default TLS server configuration with safe defaults. + /// + /// This function constructs a TLS server configuration with safe defaults. + /// The configuration utilizes the identity (certificate and private key) specified + /// in the input argument of the function. + /// + /// Client authentication is not required in this configuration. + /// + /// # Arguments + /// + /// - `identity`: A reference to the identity containing the certificate chain and private key. + pub fn build_default_tls_config(identity: Identity) -> TlsServerConfig { + let provider = Arc::new(default_crypto_provider()); + + let certificates = identity + .certificate_chain + .0 + .into_iter() + .map(|cert| cert.0) + .collect(); + + let private_key = identity.private_key.0; + + let mut tls_config = TlsServerConfig::builder_with_provider(provider) + .with_protocol_versions(&[&rustls::version::TLS13]) + .expect("valid version") + .with_no_client_auth() + .with_single_cert(certificates, private_key) + .expect("Certificate and private key should be already validated"); + + tls_config.alpn_protocols = [WEBTRANSPORT_ALPN.to_vec()].to_vec(); + + tls_config + } +} + +/// TLS configurations and utilities client-side. +pub mod client { + use super::*; + use rustls::client::danger::ServerCertVerified; + use rustls::client::danger::ServerCertVerifier; + use rustls::crypto::WebPkiSupportedAlgorithms; + use rustls::ClientConfig as TlsClientConfig; + + /// Builds a default TLS client configuration with safe defaults. + /// + /// This function constructs a TLS client configuration with safe defaults. + /// It utilizes the provided `RootCertStore` to validate server certificates during the TLS handshake. + /// + /// If a custom [`ServerCertVerifier`] is provided, it will be used for certificate validation; otherwise, + /// it will use the standard safe default mechanism (using the Web PKI mechanism). + /// + /// Client authentication is not required in this configuration. + /// + /// # Arguments + /// + /// - `root_store`: An `Arc` containing the [`RootCertStore`] with trusted root certificates. + /// To obtain a `RootCertStore`, one can use the [`build_native_cert_store`] + /// function, which loads the platform's certificate authorities (CAs). + /// - `custom_verifier`: An optional `Arc` containing a custom implementation of the [`ServerCertVerifier`] + /// trait for custom certificate verification. If `None` is provided, the default + /// Web PKI mechanism will be used. + /// + /// # Note + /// + /// If a custom `ServerCertVerifier` is provided, exercise caution as it could potentially compromise the + /// certificate validation process if not implemented correctly. + pub fn build_default_tls_config( + root_store: Arc, + custom_verifier: Option>, + ) -> TlsClientConfig { + let provider = Arc::new(default_crypto_provider()); + + let mut config = TlsClientConfig::builder_with_provider(provider) + .with_protocol_versions(&[&rustls::version::TLS13]) + .expect("valid version") + .with_root_certificates(root_store) + .with_no_client_auth(); + + if let Some(custom_verifier) = custom_verifier { + config.dangerous().set_certificate_verifier(custom_verifier); + } + + config.alpn_protocols = [WEBTRANSPORT_ALPN.to_vec()].to_vec(); + config + } + + /// A custom **insecure** [`ServerCertVerifier`] implementation. + /// + /// This verifier is configured to skip all server's certificate validation. + /// + /// Therefore, it's advisable to use it judiciously, and avoid using it in + /// production environments. + #[derive(Debug)] + pub struct NoServerVerification { + supported_algorithms: WebPkiSupportedAlgorithms, + } + + impl Default for NoServerVerification { + fn default() -> Self { + Self::new() + } + } + + impl NoServerVerification { + /// Creates a new instance of `NoServerVerification`. + pub fn new() -> NoServerVerification { + NoServerVerification { + supported_algorithms: default_crypto_provider().signature_verification_algorithms, + } + } + } + + impl ServerCertVerifier for NoServerVerification { + fn verify_server_cert( + &self, + _end_entity: &CertificateDer, + _intermediates: &[CertificateDer], + _server_name: &rustls_pki_types::ServerName, + _ocsp_response: &[u8], + _now: rustls_pki_types::UnixTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> Result { + rustls::crypto::verify_tls12_signature(message, cert, dss, &self.supported_algorithms) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> Result { + rustls::crypto::verify_tls13_signature(message, cert, dss, &self.supported_algorithms) + } + + fn supported_verify_schemes(&self) -> Vec { + self.supported_algorithms.supported_schemes() + } + } + + /// A custom [`ServerCertVerifier`] implementation. + /// + /// Configures the client to skip *some* server certificates validation. + /// + /// This verifier is configured to accept server certificates + /// whose digests match the specified *SHA-256* hashes and fulfill + /// some additional constraints (*see notes below*). + /// + /// This is useful for scenarios where clients need to accept known + /// self-signed certificates or certificates from non-standard authorities. + /// + /// # Notes + /// + /// - The current time MUST be within the validity period of the certificate. + /// - The total length of the validity period MUST NOT exceed *two* weeks. + /// - Only certificates for which the public key algorithm is *ECDSA* with the *secp256r1* are accepted. + #[derive(Debug)] + pub struct ServerHashVerification { + hashes: std::collections::BTreeSet, + supported_algorithms: WebPkiSupportedAlgorithms, + } + + impl ServerHashVerification { + const SELF_MAX_VALIDITY: time::Duration = time::Duration::days(14); + + /// Creates a new instance of `ServerHashVerification`. + /// + /// # Arguments + /// + /// - `hashes`: An iterator yielding `Sha256Digest` instances representing the + /// accepted certificate hashes. + pub fn new(hashes: H) -> Self + where + H: IntoIterator, + { + use std::collections::BTreeSet; + + Self { + hashes: BTreeSet::from_iter(hashes), + supported_algorithms: default_crypto_provider().signature_verification_algorithms, + } + } + + /// Adds a `digest` to the list of accepted certificates. + pub fn add(&mut self, digest: Sha256Digest) { + self.hashes.insert(digest); + } + } + + impl FromIterator for ServerHashVerification { + fn from_iter>(iter: T) -> Self { + Self::new(iter) + } + } + + impl ServerCertVerifier for ServerHashVerification { + fn verify_server_cert( + &self, + end_entity: &CertificateDer, + _intermediates: &[CertificateDer], + _server_name: &rustls_pki_types::ServerName, + _ocsp_response: &[u8], + now: rustls_pki_types::UnixTime, + ) -> Result { + use time::OffsetDateTime; + use x509_parser::oid_registry::OID_EC_P256; + use x509_parser::oid_registry::OID_KEY_TYPE_EC_PUBLIC_KEY; + use x509_parser::time::ASN1Time; + + let now = ASN1Time::new( + now.as_secs() + .try_into() + .ok() + .and_then(|time| OffsetDateTime::from_unix_timestamp(time).ok()) + .expect("time overflow"), + ); + + let x509 = X509Certificate::from_der(end_entity.as_ref()) + .map_err(|_| rustls::CertificateError::BadEncoding)? + .1; + + match x509.validity() { + x if now < x.not_before => { + return Err(rustls::CertificateError::NotValidYet.into()); + } + x if now > x.not_after => { + return Err(rustls::CertificateError::Expired.into()); + } + _ => {} + } + + let validity_period = x509.validity().not_after - x509.validity.not_before; + if !matches!(validity_period, Some(x) if x <= Self::SELF_MAX_VALIDITY) { + return Err(rustls::CertificateError::UnknownIssuer.into()); + } + + if x509.public_key().algorithm.algorithm != OID_KEY_TYPE_EC_PUBLIC_KEY { + return Err(rustls::CertificateError::UnknownIssuer.into()); + } + + if !matches!(x509.public_key().algorithm.parameters.as_ref().map(|any| any.as_oid()), + Some(Ok(oid)) if oid == OID_EC_P256) + { + return Err(rustls::CertificateError::UnknownIssuer.into()); + } + + // TODO: Duplicates logic in `Certificate::from_der`, to avoid allocating + X509Certificate::from_der(end_entity.as_ref()) + .map_err(|_| rustls::CertificateError::BadEncoding)?; + // TODO: Duplicates logic in `Certificate::hash`, to avoid allocating + let end_entity_hash = Sha256Digest(Sha256::digest(end_entity.as_ref()).into()); + + if self.hashes.contains(&end_entity_hash) { + Ok(ServerCertVerified::assertion()) + } else { + Err(rustls::CertificateError::UnknownIssuer.into()) + } + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> Result { + rustls::crypto::verify_tls12_signature(message, cert, dss, &self.supported_algorithms) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> Result { + rustls::crypto::verify_tls13_signature(message, cert, dss, &self.supported_algorithms) + } + + fn supported_verify_schemes(&self) -> Vec { + self.supported_algorithms.supported_schemes() + } + } +} + +/// TLS errors definitions module. +pub mod error { + use std::path::PathBuf; + + /// Represents an error indicating an invalid certificate parsing. + #[derive(Debug, thiserror::Error)] + #[error("invalid certificate: {0}")] + pub struct InvalidCertificate(pub(super) String); + + /// Represents an error failure to parse a string as a [`Sha256Digest`](super::Sha256Digest). + /// + /// See [`Sha256Digest::from_str_fmt`](super::Sha256Digest::from_str_fmt). + #[derive(Debug, thiserror::Error)] + #[error("cannot parse string as sha256 digest")] + pub struct InvalidDigest; + + /// Error during PEM load operation. + #[derive(Debug, thiserror::Error)] + pub enum PemLoadError { + /// Invalid certificate during chain load. + #[error("invalid certificate in the PEM chain (index: {}): {}", .index, .error)] + InvalidCertificateChain { + /// The index of the certificate in the PEM file. + index: usize, + /// Additional error information. + error: InvalidCertificate, + }, + + /// Cannot load the private key as the PEM file does not contain it. + #[error("no private key section found in PEM file")] + NoPrivateKeySection, + + /// Cannot load the certificate as the PEM file does not contain it. + #[error("no certificate section found in PEM file")] + NoCertificateSection, + + /// I/O operation encoding/decoding PEM file failed. + #[error("error on file '{}': {}", .file.display(), error)] + FileError { + /// Filename of the operation. + file: PathBuf, + + /// io error details. + error: std::io::Error, + }, + } + + /// Certificate SANs are not valid DNS. + /// + /// This error might happen during self signed certificate generation + /// [`Identity::self_signed`](super::Identity::self_signed). + /// In particular, *Subject Alternative Names* passed for the generation of the + /// certificate are not valid DNS *IA5* strings. + /// + /// DNS strings support the [International Alphabet No. 5 (IA5)] character encoding, i.e. + /// the 128 characters of the ASCII alphabet. + /// + /// [International Alphabet No. 5 (IA5)]: https://en.wikipedia.org/wiki/T.50_(standard) + #[cfg(feature = "self-signed")] + #[cfg_attr(docsrs, doc(cfg(feature = "self-signed")))] + #[derive(Debug, thiserror::Error)] + #[error("invalid SANs for the self certificate")] + pub struct InvalidSan; +} + +/// Module for generating self-signed [`Identity`]. +/// +/// This module provides a builder pattern for constructing self-signed +/// identities. These certificates are primarily used for local testing or +/// development where a full certificate authority (CA) infrastructure isn't +/// necessary. +/// +/// The feature is enabled when the `self-signed` feature flag is active. +#[cfg(feature = "self-signed")] +#[cfg_attr(docsrs, doc(cfg(feature = "self-signed")))] +pub mod self_signed { + use super::*; + use error::InvalidSan; + use rcgen::CertificateParams; + use rcgen::DistinguishedName; + use rcgen::DnType; + use rcgen::KeyPair; + use rcgen::PKCS_ECDSA_P256_SHA256; + use time::OffsetDateTime; + + /// The builder for creating self-signed [`Identity`]. + /// + /// This struct uses state-based typing to enforce that the appropriate methods + /// are called in the right order. + pub struct SelfSignedIdentityBuilder(State); + + impl SelfSignedIdentityBuilder { + /// Creates a new `SelfSignedIdentityBuilder` instance. + /// + /// The builder starts in the `WantsSans` state, meaning it requires subject + /// alternative names (SANs) to be specified before continuing. + /// + /// # Example + /// + /// ``` + /// use wtransport::tls::self_signed::SelfSignedIdentityBuilder; + /// + /// let builder = SelfSignedIdentityBuilder::new(); + /// ``` + /// + /// **Note**: You can conveniently create a new builder with [`Identity::self_signed_builder()`]. + /// + /// # Example + /// + /// ``` + /// use wtransport::Identity; + /// + /// let builder = Identity::self_signed_builder(); + /// ``` + pub fn new() -> Self { + Self(states::WantsSans {}) + } + + /// Specifies the subject alternative names (SANs) for the certificate. + /// + /// The SANs can be provided as a collection of strings, such as hostnames or IP addresses. + /// + /// # Arguments + /// + /// * `subject_alt_names` - An iterator of strings representing subject alternative names (SANs). + /// They can be both hostnames or IP addresses. + /// + /// # Example + /// + /// ``` + /// use wtransport::tls::self_signed::SelfSignedIdentityBuilder; + /// + /// let builder = + /// SelfSignedIdentityBuilder::new().subject_alt_names(&["localhost", "127.0.0.1", "::1"]); + /// ``` + pub fn subject_alt_names( + self, + subject_alt_names: I, + ) -> SelfSignedIdentityBuilder + where + I: IntoIterator, + S: AsRef, + { + let sans = subject_alt_names + .into_iter() + .map(|s| s.as_ref().to_string()) + .collect(); + + SelfSignedIdentityBuilder(states::WantsValidityPeriod { sans }) + } + } + + impl SelfSignedIdentityBuilder { + /// Sets the certificate's `not_before` time to the current UTC time. + /// + /// After this, the builder is in the `WantsNotAfter` state, requiring a `not_after` time to be set. + #[allow(clippy::wrong_self_convention)] + pub fn from_now_utc(self) -> SelfSignedIdentityBuilder { + let not_before = OffsetDateTime::now_utc(); + self.not_before(not_before) + } + + /// Sets the `not_before` time of the certificate. + /// + /// # Parameters + /// + /// - `not_before`: The starting time of the certificate's validity period. + /// + /// After this, the builder is in the `WantsNotAfter` state, requiring a `not_after` time to be set. + pub fn not_before( + self, + not_before: OffsetDateTime, + ) -> SelfSignedIdentityBuilder { + SelfSignedIdentityBuilder(states::WantsNotAfter { + sans: self.0.sans, + not_before, + }) + } + + /// Specifies the validity period for the certificate. + /// + /// # Parameters + /// + /// - `not_before`: The starting time of the certificate's validity period. + /// - `not_after`: The ending time of the certificate's validity period. + /// + /// # Example + /// + /// ``` + /// use wtransport::tls::self_signed::time::OffsetDateTime; + /// use wtransport::tls::self_signed::SelfSignedIdentityBuilder; + /// + /// let builder = SelfSignedIdentityBuilder::new() + /// .subject_alt_names(&["localhost"]) + /// .validity_period( + /// OffsetDateTime::now_utc(), + /// OffsetDateTime::now_utc() + time::Duration::days(7), + /// ); + /// ``` + pub fn validity_period( + self, + not_before: OffsetDateTime, + not_after: OffsetDateTime, + ) -> SelfSignedIdentityBuilder { + self.not_before(not_before).not_after(not_after) + } + } + + impl SelfSignedIdentityBuilder { + /// Specifies the `not_after` time of the certificate. + /// + /// # Parameters + /// + /// - `not_after`: The ending time of the certificate's validity. + pub fn not_after( + self, + not_after: OffsetDateTime, + ) -> SelfSignedIdentityBuilder { + SelfSignedIdentityBuilder(states::ReadyToBuild { + sans: self.0.sans, + not_before: self.0.not_before, + not_after, + }) + } + + /// Sets the `not_after` time of the certificate to an offset from the `not_before` time. + /// + /// # Parameters + /// + /// - `offset`: A time duration that specifies how far `not_after` should be from `not_before`. + /// + /// # Example + /// + /// ``` + /// use wtransport::tls::self_signed::time::OffsetDateTime; + /// use wtransport::tls::self_signed::SelfSignedIdentityBuilder; + /// + /// let builder = SelfSignedIdentityBuilder::new() + /// .subject_alt_names(&["localhost"]) + /// .not_before(OffsetDateTime::now_utc()) + /// .offset_from_not_before(time::Duration::days(7)); // now + 7 days + /// ``` + pub fn offset_from_not_before( + self, + offset: time::Duration, + ) -> SelfSignedIdentityBuilder { + let not_after = self.0.not_before + offset; + self.not_after(not_after) + } + + /// Sets the certificate's validity to a specified number of days from `not_before`. + /// + /// # Parameters + /// + /// - `days`: The number of days for which the certificate should be valid. + /// + /// # Example + /// + /// ``` + /// use wtransport::tls::self_signed::SelfSignedIdentityBuilder; + /// + /// let builder = SelfSignedIdentityBuilder::new() + /// .subject_alt_names(&["localhost"]) + /// .from_now_utc() + /// .validity_days(14); + /// ``` + pub fn validity_days(self, days: u32) -> SelfSignedIdentityBuilder { + self.offset_from_not_before(time::Duration::days(days as i64)) + } + } + + impl SelfSignedIdentityBuilder { + /// Generates a self-signed certificate and private key for new identity. + /// + /// # Returns + /// + /// Returns an [`Identity`] containing the certificate and private key, + /// or an error if the SANs are invalid (they are not valid *ASN.1* strings). + /// + /// # Specifications + /// + /// The certificate will be conforms to the following specifications: + /// + /// - The certificate is an *X.509v3* certificate as defined in *RFC5280*. + /// - The key used in the Subject Public Key field uses `ECDSA P-256` algorithm. + /// + /// # Example + /// + /// ``` + /// use wtransport::tls::self_signed::SelfSignedIdentityBuilder; + /// + /// let identity = SelfSignedIdentityBuilder::new() + /// .subject_alt_names(&["localhost", "127.0.0.1"]) + /// .from_now_utc() + /// .validity_days(7) + /// .build() + /// .unwrap(); + /// ``` + pub fn build(self) -> Result { + let mut dname = DistinguishedName::new(); + dname.push(DnType::CommonName, "wtransport self-signed"); + + let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256) + .expect("algorithm for key pair is supported"); + + let mut cert_params = CertificateParams::new(self.0.sans).map_err(|_| InvalidSan)?; + cert_params.distinguished_name = dname; + cert_params.not_before = self.0.not_before; + cert_params.not_after = self.0.not_after; + let cert = cert_params + .self_signed(&key_pair) + .expect("inner params are valid"); + + Ok(Identity::new( + CertificateChain::single(Certificate(cert.der().clone())), + PrivateKey::from_der_pkcs8(key_pair.serialize_der()), + )) + } + } + + impl Default for SelfSignedIdentityBuilder { + fn default() -> Self { + Self::new() + } + } + + /// State-types for [`SelfSignedIdentityBuilder`] builder. + pub mod states { + use super::*; + + /// Initial state, requiring subject alternative names (SANs). + pub struct WantsSans {} + + /// State after SANs have been set, requiring a validity period. + pub struct WantsValidityPeriod { + pub(super) sans: Vec, + } + + /// State after `not_before` is set, requiring `not_after` to be specified. + pub struct WantsNotAfter { + pub(super) sans: Vec, + pub(super) not_before: OffsetDateTime, + } + + /// Final state where all data is ready and the certificate can be built. + pub struct ReadyToBuild { + pub(super) sans: Vec, + pub(super) not_before: OffsetDateTime, + pub(super) not_after: OffsetDateTime, + } + } + + pub use time; +} + +pub use rustls; + +mod utils { + use std::env; + use std::ffi::OsStr; + use std::ffi::OsString; + + pub struct VarsRestoreGuard(Vec<(OsString, Option)>); + + impl Drop for VarsRestoreGuard { + fn drop(&mut self) { + for (k, v) in std::mem::take(&mut self.0) { + if let Some(v) = v { + env::set_var(k, v); + } + } + } + } + + pub fn remove_vars_tmp(keys: I) -> VarsRestoreGuard + where + I: IntoIterator, + K: AsRef, + { + let table = keys + .into_iter() + .map(|k| { + let k = k.as_ref().to_os_string(); + let v = env::var_os(&k); + env::remove_var(&k); + (k, v) + }) + .collect(); + + VarsRestoreGuard(table) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn invalid_certificate() { + assert!(matches!( + Certificate::from_der(b"invalid-certificate".to_vec()), + Err(InvalidCertificate(_)) + )); + } + + #[test] + fn idempotence_digest() { + let digest = Sha256Digest::new([ + 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, + 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 64, + ]); + + let d = Sha256Digest::from_str_fmt( + digest.fmt(Sha256DigestFmt::BytesArray), + Sha256DigestFmt::BytesArray, + ) + .unwrap(); + + assert_eq!(d, digest); + + let d = Sha256Digest::from_str_fmt( + digest.fmt(Sha256DigestFmt::DottedHex), + Sha256DigestFmt::DottedHex, + ) + .unwrap(); + + assert_eq!(d, digest); + } + + #[test] + fn digest_from_str() { + assert!(matches!( + "invalid".parse::(), + Err(InvalidDigest) + )); + + assert!(matches!( + "[97, 98, 99, 100, 101, 102, 103, 104, 105, 106, \ + 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, \ + 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, \ + 127, 64]" + .parse::(), + + Ok(digest) if digest == Sha256Digest::new([ + 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, + 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 64 + ]))); + + assert!(matches!( + "61:62:63:64:65:66:67:68:69:6a:6b:6c:6d:6e:6f:70: \ + 71:72:73:74:75:76:77:78:79:7a:7b:7c:7d:7e:7f:40" + .parse::(), + + Ok(digest) if digest == Sha256Digest::new([ + 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, + 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 64 + ]))); + } +} diff --git a/scripts/check-lockfiles.sh b/scripts/check-lockfiles.sh index 30dfb8403..a116cc94c 100755 --- a/scripts/check-lockfiles.sh +++ b/scripts/check-lockfiles.sh @@ -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; }