use web_transport_proto::{ConnectRequest, ConnectResponse, VarInt}; use thiserror::Error; use url::Url; #[derive(Error, Debug, Clone)] pub enum ConnectError { #[error("quic stream was closed early")] UnexpectedEnd, #[error("protocol error: {0}")] ProtoError(#[from] web_transport_proto::ConnectError), #[error("connection error")] ConnectionError(#[from] iroh::endpoint::ConnectionError), #[error("read error")] ReadError(#[from] quinn::ReadError), #[error("write error")] WriteError(#[from] quinn::WriteError), #[error("http error status: {0}")] ErrorStatus(http::StatusCode), } pub struct Connect { // The request that was sent by the client. request: ConnectRequest, // A reference to the send/recv stream, so we don't close it until dropped. send: quinn::SendStream, #[allow(dead_code)] recv: quinn::RecvStream, } impl Connect { pub async fn accept(conn: &iroh::endpoint::Connection) -> Result { // Accept the stream that will be used to send the HTTP CONNECT request. // If they try to send any other type of HTTP request, we will error out. let (send, mut recv) = conn.accept_bi().await?; let request = web_transport_proto::ConnectRequest::read(&mut recv).await?; tracing::debug!("received CONNECT request: {request:?}"); // The request was successfully decoded, so we can send a response. Ok(Self { request, send, recv, }) } // Called by the server to send a response to the client. pub async fn respond(&mut self, status: http::StatusCode) -> Result<(), ConnectError> { let resp = ConnectResponse { status }; tracing::debug!("sending CONNECT response: {resp:?}"); resp.write(&mut self.send).await?; Ok(()) } pub async fn open(conn: &iroh::endpoint::Connection, url: Url) -> Result { // Create a new stream that will be used to send the CONNECT frame. let (mut send, mut recv) = conn.open_bi().await?; // Create a new CONNECT request that we'll send using HTTP/3 let request = ConnectRequest { url }; tracing::debug!("sending CONNECT request: {request:?}"); request.write(&mut send).await?; let response = web_transport_proto::ConnectResponse::read(&mut recv).await?; tracing::debug!("received CONNECT response: {response:?}"); // Throw an error if we didn't get a 200 OK. if response.status != http::StatusCode::OK { return Err(ConnectError::ErrorStatus(response.status)); } Ok(Self { request, send, recv, }) } // The session ID is the stream ID of the CONNECT request. pub fn session_id(&self) -> VarInt { // We gotta convert from the Quinn VarInt to the (forked) WebTransport VarInt. // We don't use the quinn::VarInt because that would mean a quinn dependency in web-transport-proto let stream_id = quinn::VarInt::from(self.send.id()); VarInt::try_from(stream_id.into_inner()).unwrap() } // The URL in the CONNECT request. pub fn url(&self) -> &Url { &self.request.url } pub(super) fn into_inner(self) -> (quinn::SendStream, quinn::RecvStream) { (self.send, self.recv) } // Keep reading from the control stream until it's closed. pub(crate) async fn run_closed(self) -> (u32, String) { let (_send, mut recv) = self.into_inner(); loop { match web_transport_proto::Capsule::read(&mut recv).await { Ok(web_transport_proto::Capsule::CloseWebTransportSession { code, reason }) => { return (code, reason); } Ok(web_transport_proto::Capsule::Unknown { typ, payload }) => { tracing::warn!("unknown capsule: type={typ} size={}", payload.len()); } Err(_) => { return (1, "capsule error".to_string()); } } } } }