diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1a7331f --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +/devcert/** -linguist-detectable diff --git a/Cargo.toml b/Cargo.toml index 7fefac0..e38eb20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,15 +10,15 @@ members = [ ] [workspace.package] -version = "0.1.0" +version = "0.2.0" edition = "2024" authors = ["shellrow "] [workspace.dependencies] -webtrans-proto = { path = "webtrans-proto", version = "0.1.0" } -webtrans-trait = { path = "webtrans-trait", version = "0.1.0" } -webtrans-quinn = { path = "webtrans-quinn", version = "0.1.0" } -webtrans-wasm = { path = "webtrans-wasm", version = "0.1.0" } +webtrans-proto = { path = "webtrans-proto", version = "0.2.0" } +webtrans-trait = { path = "webtrans-trait", version = "0.2.0" } +webtrans-quinn = { path = "webtrans-quinn", version = "0.2.0" } +webtrans-wasm = { path = "webtrans-wasm", version = "0.2.0" } bytes = "1" thiserror = "2" http = "1" diff --git a/webtrans-proto/src/frame.rs b/webtrans-proto/src/frame.rs index e7731c4..e08984b 100644 --- a/webtrans-proto/src/frame.rs +++ b/webtrans-proto/src/frame.rs @@ -48,20 +48,14 @@ impl Frame { Ok((typ, limit)) } -} -macro_rules! frames { - {$($name:ident = $val:expr,)*} => { - impl Frame { - $(pub const $name: Frame = Frame(VarInt::from_u32($val));)* - } + pub const fn from_u32(value: u32) -> Self { + Self(VarInt::from_u32(value)) } -} -// Frames sent at the start of a bidirectional stream. -frames! { - DATA = 0x00, - HEADERS = 0x01, - SETTINGS = 0x04, - WEBTRANSPORT = 0x41, + // Frames sent at the start of a bidirectional stream. + pub const DATA: Frame = Frame::from_u32(0x00); + pub const HEADERS: Frame = Frame::from_u32(0x01); + pub const SETTINGS: Frame = Frame::from_u32(0x04); + pub const WEBTRANSPORT: Frame = Frame::from_u32(0x41); } diff --git a/webtrans-proto/src/settings.rs b/webtrans-proto/src/settings.rs index d581fa0..28df2b0 100644 --- a/webtrans-proto/src/settings.rs +++ b/webtrans-proto/src/settings.rs @@ -64,31 +64,27 @@ impl Debug for Setting { } } -macro_rules! settings { - {$($name:ident = $val:expr,)*} => { - impl Setting { - $(pub const $name: Setting = Setting(VarInt::from_u32($val));)* - } +impl Setting { + pub const fn from_u32(value: u32) -> Self { + Self(VarInt::from_u32(value)) } -} -settings! { // HTTP/3 settings that WebTransport ignores. - QPACK_MAX_TABLE_CAPACITY = 0x1, // Default is 0, which disables the dynamic table. - MAX_FIELD_SECTION_SIZE = 0x6, - QPACK_BLOCKED_STREAMS = 0x7, + pub const QPACK_MAX_TABLE_CAPACITY: Setting = Setting::from_u32(0x1); // Default is 0, which disables the dynamic table. + pub const MAX_FIELD_SECTION_SIZE: Setting = Setting::from_u32(0x6); + pub const QPACK_BLOCKED_STREAMS: Setting = Setting::from_u32(0x7); // Both values are required for WebTransport. - ENABLE_CONNECT_PROTOCOL = 0x8, - ENABLE_DATAGRAM = 0x33, - ENABLE_DATAGRAM_DEPRECATED = 0xFFD277, // Still used by some Chrome versions. + pub const ENABLE_CONNECT_PROTOCOL: Setting = Setting::from_u32(0x8); + pub const ENABLE_DATAGRAM: Setting = Setting::from_u32(0x33); + pub const ENABLE_DATAGRAM_DEPRECATED: Setting = Setting::from_u32(0xFFD277); // Still used by some Chrome versions. // Removed in draft-06. - WEBTRANSPORT_ENABLE_DEPRECATED = 0x2b603742, - WEBTRANSPORT_MAX_SESSIONS_DEPRECATED = 0x2b603743, + pub const WEBTRANSPORT_ENABLE_DEPRECATED: Setting = Setting::from_u32(0x2b603742); + pub const WEBTRANSPORT_MAX_SESSIONS_DEPRECATED: Setting = Setting::from_u32(0x2b603743); // Current way to enable WebTransport. - WEBTRANSPORT_MAX_SESSIONS = 0xc671706a, + pub const WEBTRANSPORT_MAX_SESSIONS: Setting = Setting::from_u32(0xc671706a); } #[derive(Error, Debug, Clone)] diff --git a/webtrans-proto/src/stream.rs b/webtrans-proto/src/stream.rs index c8cd3c4..cc209de 100644 --- a/webtrans-proto/src/stream.rs +++ b/webtrans-proto/src/stream.rs @@ -28,20 +28,14 @@ impl UniStream { (val - 0x21) % 0x1f == 0 } } -} -macro_rules! streams_uni { - {$($name:ident = $val:expr,)*} => { - impl UniStream { - $(pub const $name: UniStream = UniStream(VarInt::from_u32($val));)* - } + pub const fn from_u32(value: u32) -> Self { + Self(VarInt::from_u32(value)) } -} -streams_uni! { - CONTROL = 0x00, - PUSH = 0x01, - QPACK_ENCODER = 0x02, - QPACK_DECODER = 0x03, - WEBTRANSPORT = 0x54, + pub const CONTROL: UniStream = UniStream::from_u32(0x00); + pub const PUSH: UniStream = UniStream::from_u32(0x01); + pub const QPACK_ENCODER: UniStream = UniStream::from_u32(0x02); + pub const QPACK_DECODER: UniStream = UniStream::from_u32(0x03); + pub const WEBTRANSPORT: UniStream = UniStream::from_u32(0x54); } diff --git a/webtrans-quinn/src/lib.rs b/webtrans-quinn/src/lib.rs index 0ddaadc..a9af093 100644 --- a/webtrans-quinn/src/lib.rs +++ b/webtrans-quinn/src/lib.rs @@ -1,7 +1,7 @@ //! Native WebTransport implementation built on top of QUIC using Quinn. //! -//! This crate provides a low-level, QUIC WebTransport API for native environments. -//! +//! This crate provides a low-level, QUIC WebTransport API for native environments. +//! //! The implementation is powered by [`quinn`], and most transport-level //! behavior (congestion control, flow control, crypto, etc.) is delegated //! directly to Quinn. diff --git a/webtrans-quinn/src/session.rs b/webtrans-quinn/src/session.rs index a5375d9..627a1f6 100644 --- a/webtrans-quinn/src/session.rs +++ b/webtrans-quinn/src/session.rs @@ -20,6 +20,28 @@ use crate::{ use webtrans_proto::{Frame, UniStream, VarInt}; +fn is_graceful_close(e: &webtrans_proto::CapsuleError) -> bool { + use std::io::ErrorKind; + + match e { + webtrans_proto::CapsuleError::Io(ioe) => { + matches!( + ioe.kind(), + ErrorKind::UnexpectedEof + | ErrorKind::BrokenPipe + | ErrorKind::ConnectionReset + | ErrorKind::ConnectionAborted + | ErrorKind::NotConnected + ) || ioe + .to_string() + .to_ascii_lowercase() + .contains("connection lost") + } + webtrans_proto::CapsuleError::UnexpectedEnd => true, + _ => false, + } +} + /// An established WebTransport session, acting like a full QUIC connection. See [`quinn::Connection`]. /// /// Remember that WebTransport is layered on top of QUIC: @@ -84,14 +106,39 @@ impl Session { }; // Run a background task to detect CONNECT stream closure. - let mut this2 = this.clone(); + let this2 = this.clone(); tokio::spawn(async move { - let (code, reason) = this2.run_closed(connect).await; - tracing::debug!( - "closing QUIC connection after WebTransport close: code={code} reason={reason}" - ); - if this2.conn.close_reason().is_none() { - this2.conn.close(0u32.into(), reason.as_bytes()); + match this2.run_closed(connect).await { + Ok(Some((code, reason))) => { + tracing::debug!("WebTransport close received: code={code} reason={reason}"); + if this2.conn.close_reason().is_none() { + this2.close(code, reason.as_bytes()); + } + } + Ok(None) => { + if let Some(reason) = this2.conn.close_reason() { + let se: crate::SessionError = reason.into(); + tracing::debug!("CONNECT stream ended: {se}"); + } else { + tracing::debug!("CONNECT stream ended without CloseWebTransportSession"); + } + } + Err(e) if is_graceful_close(&e) => { + if let Some(reason) = this2.conn.close_reason() { + let se: crate::SessionError = reason.into(); + tracing::debug!( + "CONNECT stream closed after QUIC close: {se} (capsule={e})" + ); + } else { + tracing::debug!("CONNECT stream closed: {e}"); + } + } + Err(e) => { + tracing::debug!("CONNECT stream error: {e}"); + if this2.conn.close_reason().is_none() { + this2.close(1, b"capsule error"); + } + } } }); @@ -99,20 +146,22 @@ impl Session { } // Keep reading from the control stream until it closes. - async fn run_closed(&mut self, connect: Connect) -> (u32, String) { + async fn run_closed( + &self, + connect: Connect, + ) -> Result, webtrans_proto::CapsuleError> { let (_send, mut recv) = connect.into_inner(); loop { match webtrans_proto::Capsule::read(&mut recv).await { Ok(webtrans_proto::Capsule::CloseWebTransportSession { code, reason }) => { - return (code, reason); + return Ok(Some((code, reason))); } Ok(webtrans_proto::Capsule::Unknown { typ, payload }) => { tracing::warn!("unknown capsule: type={typ} size={}", payload.len()); } - Err(_) => { - return (1, "capsule error".to_string()); - } + Err(e) if is_graceful_close(&e) => return Ok(None), + Err(e) => return Err(e), } } } @@ -250,10 +299,11 @@ impl Session { /// Immediately close the connection with an error code and reason. See [`quinn::Connection::close`]. pub fn close(&self, code: u32, reason: &[u8]) { - let code = if self.session_id.is_some() { - webtrans_proto::error_to_http3(code).try_into().unwrap() + let code: quinn::VarInt = if self.session_id.is_some() { + let mapped = webtrans_proto::error_to_http3(code); + quinn::VarInt::from_u64(mapped).unwrap_or(quinn::VarInt::from_u32(1)) } else { - code.into() + quinn::VarInt::from_u32(code) }; self.conn.close(code, reason) diff --git a/webtrans/Cargo.toml b/webtrans/Cargo.toml index 7b3985c..a7b392b 100644 --- a/webtrans/Cargo.toml +++ b/webtrans/Cargo.toml @@ -14,10 +14,10 @@ license = "MIT" webtrans-proto = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -webtrans-quinn = { version = "0.1.0", path = "../webtrans-quinn" } +webtrans-quinn = { workspace = true } [target.'cfg(target_arch = "wasm32")'.dependencies] -webtrans-wasm = { version = "0.1.0", path = "../webtrans-wasm" } +webtrans-wasm = { workspace = true } [dev-dependencies] anyhow = "1" diff --git a/webtrans/examples/echo-client.rs b/webtrans/examples/echo-client.rs index 17dc4d7..6dc9531 100644 --- a/webtrans/examples/echo-client.rs +++ b/webtrans/examples/echo-client.rs @@ -80,6 +80,10 @@ async fn main() -> anyhow::Result<()> { session.close(42069, b"bye"); session.closed().await; + tracing::info!("session closed"); + tracing::info!("waiting a moment to ensure close"); + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + Ok(()) }