every.channel/third_party/iroh-live/web-transport-iroh/src/connect.rs
2026-02-15 16:17:27 -05:00

125 lines
4.1 KiB
Rust

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<Self, ConnectError> {
// 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<Self, ConnectError> {
// 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());
}
}
}
}
}