every.channel: sanitized baseline
This commit is contained in:
commit
897e556bea
258 changed files with 74298 additions and 0 deletions
71
third_party/iroh-live/web-transport-iroh/src/client.rs
vendored
Normal file
71
third_party/iroh-live/web-transport-iroh/src/client.rs
vendored
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use iroh::{EndpointAddr, endpoint::{ConnectOptions, QuicTransportConfig}};
|
||||
use url::Url;
|
||||
|
||||
use crate::{ALPN_H3, ClientError, Session};
|
||||
|
||||
/// A client for connecting to a WebTransport server.
|
||||
pub struct Client {
|
||||
endpoint: iroh::Endpoint,
|
||||
config: QuicTransportConfig,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(endpoint: iroh::Endpoint) -> Self {
|
||||
Self::with_transport_config(endpoint, Default::default())
|
||||
}
|
||||
|
||||
/// Creates a client from an endpoint and a transport config.
|
||||
pub fn with_transport_config(
|
||||
endpoint: iroh::Endpoint,
|
||||
config: QuicTransportConfig,
|
||||
) -> Self {
|
||||
Self { endpoint, config }
|
||||
}
|
||||
|
||||
/// Connect to a server over QUIC without HTTP/3.
|
||||
pub async fn connect_quic(
|
||||
&self,
|
||||
addr: impl Into<EndpointAddr>,
|
||||
alpn: &[u8],
|
||||
) -> Result<Session, ClientError> {
|
||||
let conn = self.connect(addr, alpn).await?;
|
||||
Ok(Session::raw(conn))
|
||||
}
|
||||
|
||||
/// Connect with a full HTTP/3 handshake and WebTransport semantics.
|
||||
///
|
||||
/// Note that the url needs to have a `https:` scheme, otherwise the accepting side will
|
||||
/// fail to accept the connection.
|
||||
pub async fn connect_h3(
|
||||
&self,
|
||||
addr: impl Into<EndpointAddr>,
|
||||
url: Url,
|
||||
) -> Result<Session, ClientError> {
|
||||
let conn = self.connect(addr, ALPN_H3.as_bytes()).await?;
|
||||
// Connect with the connection we established.
|
||||
Session::connect_h3(conn, url).await
|
||||
}
|
||||
|
||||
async fn connect(
|
||||
&self,
|
||||
addr: impl Into<EndpointAddr>,
|
||||
alpn: &[u8],
|
||||
) -> Result<iroh::endpoint::Connection, ClientError> {
|
||||
let opts = ConnectOptions::new().with_transport_config(self.config.clone());
|
||||
let conn = self
|
||||
.endpoint
|
||||
.connect_with_opts(addr, alpn, opts)
|
||||
.await
|
||||
.map_err(|err| ClientError::Connect(Arc::new(err.into())))?;
|
||||
let conn = conn
|
||||
.await
|
||||
.map_err(|err| ClientError::Connect(Arc::new(err.into())))?;
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
pub async fn close(&self) {
|
||||
self.endpoint.close().await;
|
||||
}
|
||||
}
|
||||
125
third_party/iroh-live/web-transport-iroh/src/connect.rs
vendored
Normal file
125
third_party/iroh-live/web-transport-iroh/src/connect.rs
vendored
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
256
third_party/iroh-live/web-transport-iroh/src/error.rs
vendored
Normal file
256
third_party/iroh-live/web-transport-iroh/src/error.rs
vendored
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use n0_error::stack_error;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{ConnectError, SettingsError};
|
||||
|
||||
/// An error returned when connecting to a WebTransport endpoint.
|
||||
#[stack_error(derive, from_sources)]
|
||||
#[derive(Clone)]
|
||||
pub enum ClientError {
|
||||
#[error("unexpected end of stream")]
|
||||
UnexpectedEnd,
|
||||
|
||||
#[error("failed to connect")]
|
||||
Connect(#[error(source)] Arc<iroh::endpoint::ConnectError>),
|
||||
|
||||
#[error("connection failed")]
|
||||
Connection(#[error(source, std_err)] iroh::endpoint::ConnectionError),
|
||||
|
||||
#[error("failed to write")]
|
||||
WriteError(#[error(source, std_err)] quinn::WriteError),
|
||||
|
||||
#[error("failed to read")]
|
||||
ReadError(#[error(source, std_err)] quinn::ReadError),
|
||||
|
||||
#[error("failed to exchange h3 settings")]
|
||||
SettingsError(#[error(from, source, std_err)] SettingsError),
|
||||
|
||||
#[error("failed to exchange h3 connect")]
|
||||
HttpError(#[error(from, source, std_err)] ConnectError),
|
||||
|
||||
#[error("invalid URL")]
|
||||
InvalidUrl,
|
||||
|
||||
#[error("endpoint failed to bind")]
|
||||
Bind(#[error(source)] Arc<iroh::endpoint::BindError>),
|
||||
}
|
||||
|
||||
/// An errors returned by [`crate::Session`], split based on if they are underlying QUIC errors or WebTransport errors.
|
||||
#[derive(Clone, Error, Debug)]
|
||||
pub enum SessionError {
|
||||
#[error("connection error: {0}")]
|
||||
ConnectionError(#[from] iroh::endpoint::ConnectionError),
|
||||
|
||||
#[error("webtransport error: {0}")]
|
||||
WebTransportError(#[from] WebTransportError),
|
||||
|
||||
#[error("send datagram error: {0}")]
|
||||
SendDatagramError(#[from] quinn::SendDatagramError),
|
||||
}
|
||||
|
||||
/// An error that can occur when reading/writing the WebTransport stream header.
|
||||
#[derive(Clone, Error, Debug)]
|
||||
pub enum WebTransportError {
|
||||
#[error("closed: code={0} reason={1}")]
|
||||
Closed(u32, String),
|
||||
|
||||
#[error("unknown session")]
|
||||
UnknownSession,
|
||||
|
||||
#[error("read error: {0}")]
|
||||
ReadError(#[from] quinn::ReadExactError),
|
||||
|
||||
#[error("write error: {0}")]
|
||||
WriteError(#[from] quinn::WriteError),
|
||||
}
|
||||
|
||||
/// An error when writing to [`crate::SendStream`]. Similar to [`quinn::WriteError`].
|
||||
#[derive(Clone, Error, Debug)]
|
||||
pub enum WriteError {
|
||||
#[error("STOP_SENDING: {0}")]
|
||||
Stopped(u32),
|
||||
|
||||
#[error("invalid STOP_SENDING: {0}")]
|
||||
InvalidStopped(quinn::VarInt),
|
||||
|
||||
#[error("session error: {0}")]
|
||||
SessionError(#[from] SessionError),
|
||||
|
||||
#[error("stream closed")]
|
||||
ClosedStream,
|
||||
}
|
||||
|
||||
impl From<quinn::WriteError> for WriteError {
|
||||
fn from(e: quinn::WriteError) -> Self {
|
||||
match e {
|
||||
quinn::WriteError::Stopped(code) => {
|
||||
match web_transport_proto::error_from_http3(code.into_inner()) {
|
||||
Some(code) => WriteError::Stopped(code),
|
||||
None => WriteError::InvalidStopped(code),
|
||||
}
|
||||
}
|
||||
quinn::WriteError::ClosedStream => WriteError::ClosedStream,
|
||||
quinn::WriteError::ConnectionLost(e) => WriteError::SessionError(e.into()),
|
||||
quinn::WriteError::ZeroRttRejected => unreachable!("0-RTT not supported"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An error when reading from [`crate::RecvStream`]. Similar to [`quinn::ReadError`].
|
||||
#[derive(Clone, Error, Debug)]
|
||||
pub enum ReadError {
|
||||
#[error("session error: {0}")]
|
||||
SessionError(#[from] SessionError),
|
||||
|
||||
#[error("RESET_STREAM: {0}")]
|
||||
Reset(u32),
|
||||
|
||||
#[error("invalid RESET_STREAM: {0}")]
|
||||
InvalidReset(quinn::VarInt),
|
||||
|
||||
#[error("stream already closed")]
|
||||
ClosedStream,
|
||||
}
|
||||
|
||||
impl From<quinn::ReadError> for ReadError {
|
||||
fn from(value: quinn::ReadError) -> Self {
|
||||
match value {
|
||||
quinn::ReadError::Reset(code) => {
|
||||
match web_transport_proto::error_from_http3(code.into_inner()) {
|
||||
Some(code) => ReadError::Reset(code),
|
||||
None => ReadError::InvalidReset(code),
|
||||
}
|
||||
}
|
||||
quinn::ReadError::ConnectionLost(e) => ReadError::SessionError(e.into()),
|
||||
quinn::ReadError::ClosedStream => ReadError::ClosedStream,
|
||||
quinn::ReadError::ZeroRttRejected => unreachable!("0-RTT not supported"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An error returned by [`crate::RecvStream::read_exact`]. Similar to [`quinn::ReadExactError`].
|
||||
#[derive(Clone, Error, Debug)]
|
||||
pub enum ReadExactError {
|
||||
#[error("finished early")]
|
||||
FinishedEarly(usize),
|
||||
|
||||
#[error("read error: {0}")]
|
||||
ReadError(#[from] ReadError),
|
||||
}
|
||||
|
||||
impl From<quinn::ReadExactError> for ReadExactError {
|
||||
fn from(e: quinn::ReadExactError) -> Self {
|
||||
match e {
|
||||
quinn::ReadExactError::FinishedEarly(size) => ReadExactError::FinishedEarly(size),
|
||||
quinn::ReadExactError::ReadError(e) => ReadExactError::ReadError(e.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An error returned by [`crate::RecvStream::read_to_end`]. Similar to [`quinn::ReadToEndError`].
|
||||
#[derive(Clone, Error, Debug)]
|
||||
pub enum ReadToEndError {
|
||||
#[error("too long")]
|
||||
TooLong,
|
||||
|
||||
#[error("read error: {0}")]
|
||||
ReadError(#[from] ReadError),
|
||||
}
|
||||
|
||||
impl From<quinn::ReadToEndError> for ReadToEndError {
|
||||
fn from(e: quinn::ReadToEndError) -> Self {
|
||||
match e {
|
||||
quinn::ReadToEndError::TooLong => ReadToEndError::TooLong,
|
||||
quinn::ReadToEndError::Read(e) => ReadToEndError::ReadError(e.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An error indicating the stream was already closed.
|
||||
#[derive(Clone, Error, Debug)]
|
||||
#[error("stream closed")]
|
||||
pub struct ClosedStream;
|
||||
|
||||
impl From<quinn::ClosedStream> for ClosedStream {
|
||||
fn from(_: quinn::ClosedStream) -> Self {
|
||||
ClosedStream
|
||||
}
|
||||
}
|
||||
|
||||
/// An error returned when receiving a new WebTransport session.
|
||||
#[stack_error(derive, from_sources)]
|
||||
#[derive(Clone)]
|
||||
pub enum ServerError {
|
||||
#[error("unexpected end of stream")]
|
||||
UnexpectedEnd,
|
||||
|
||||
#[error("connection failed")]
|
||||
Connection(#[error(source, std_err)] iroh::endpoint::ConnectionError),
|
||||
|
||||
#[error("connection failed during handshake")]
|
||||
Connecting(#[error(source)] Arc<iroh::endpoint::ConnectingError>),
|
||||
|
||||
#[error("failed to write")]
|
||||
WriteError(#[error(source, std_err)] quinn::WriteError),
|
||||
|
||||
#[error("failed to read")]
|
||||
ReadError(#[error(source, std_err)] quinn::ReadError),
|
||||
|
||||
#[error("io error")]
|
||||
IoError(#[error(source)] Arc<std::io::Error>),
|
||||
|
||||
#[error("failed to bind endpoint")]
|
||||
Bind(#[error(source)] Arc<iroh::endpoint::BindError>),
|
||||
|
||||
#[error("failed to exchange h3 connect")]
|
||||
HttpError(#[error(source, from, std_err)] ConnectError),
|
||||
|
||||
#[error("failed to exchange h3 settings")]
|
||||
SettingsError(#[error(source, from, std_err)] SettingsError),
|
||||
}
|
||||
|
||||
impl web_transport_trait::Error for SessionError {
|
||||
fn session_error(&self) -> Option<(u32, String)> {
|
||||
if let SessionError::WebTransportError(WebTransportError::Closed(code, reason)) = self {
|
||||
return Some((*code, reason.to_string()));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl web_transport_trait::Error for WriteError {
|
||||
fn session_error(&self) -> Option<(u32, String)> {
|
||||
if let WriteError::SessionError(e) = self {
|
||||
return e.session_error();
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn stream_error(&self) -> Option<u32> {
|
||||
match self {
|
||||
WriteError::Stopped(code) => Some(*code),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl web_transport_trait::Error for ReadError {
|
||||
fn session_error(&self) -> Option<(u32, String)> {
|
||||
if let ReadError::SessionError(e) = self {
|
||||
return e.session_error();
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn stream_error(&self) -> Option<u32> {
|
||||
match self {
|
||||
ReadError::Reset(code) => Some(*code),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
56
third_party/iroh-live/web-transport-iroh/src/lib.rs
vendored
Normal file
56
third_party/iroh-live/web-transport-iroh/src/lib.rs
vendored
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
//! WebTransport is a protocol for client-server communication over QUIC.
|
||||
//! It's [available in the browser](https://caniuse.com/webtransport) as an alternative to HTTP and WebSockets.
|
||||
//!
|
||||
//! WebTransport is layered on top of HTTP/3 which is then layered on top of QUIC.
|
||||
//! This library hides that detail and tries to expose only the QUIC API, delegating as much as possible to the underlying implementation.
|
||||
//! See the [Quinn documentation](https://docs.rs/quinn/latest/quinn/) for more documentation.
|
||||
//!
|
||||
//! QUIC provides two primary APIs:
|
||||
//!
|
||||
//! # Streams
|
||||
//! QUIC streams are ordered, reliable, flow-controlled, and optionally bidirectional.
|
||||
//! Both endpoints can create and close streams (including an error code) with no overhead.
|
||||
//! You can think of them as TCP connections, but shared over a single QUIC connection.
|
||||
//!
|
||||
//! # Datagrams
|
||||
//! QUIC datagrams are unordered, unreliable, and not flow-controlled.
|
||||
//! Both endpoints can send datagrams below the MTU size (~1.2kb minimum) and they might arrive out of order or not at all.
|
||||
//! They are basically UDP packets, except they are encrypted and congestion controlled.
|
||||
//!
|
||||
//! # Limitations
|
||||
//! WebTransport is able to be pooled with HTTP/3 and multiple WebTransport sessions.
|
||||
//! This crate avoids that complexity, doing the bare minimum to support a single WebTransport session that owns the entire QUIC connection.
|
||||
//! If you want to support HTTP/3 on the same host/port, you should use another crate (ex. `h3-webtransport`).
|
||||
//! If you want to support multiple WebTransport sessions over the same QUIC connection... you should just dial a new QUIC connection instead.
|
||||
|
||||
// External
|
||||
mod client;
|
||||
mod connect;
|
||||
mod error;
|
||||
mod recv;
|
||||
mod send;
|
||||
mod server;
|
||||
mod session;
|
||||
mod settings;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub use client::*;
|
||||
pub use connect::*;
|
||||
pub use error::*;
|
||||
pub use recv::*;
|
||||
pub use send::*;
|
||||
pub use server::*;
|
||||
pub use session::*;
|
||||
pub use settings::*;
|
||||
|
||||
/// The HTTP/3 ALPN is required when negotiating a QUIC connection.
|
||||
pub const ALPN_H3: &str = "h3";
|
||||
|
||||
/// Re-export the http crate because it's in the public API.
|
||||
pub use http;
|
||||
pub use iroh;
|
||||
/// Re-export the underlying QUIC implementation.
|
||||
pub use quinn;
|
||||
/// Re-export the generic WebTransport implementation.
|
||||
pub use web_transport_trait as generic;
|
||||
111
third_party/iroh-live/web-transport-iroh/src/recv.rs
vendored
Normal file
111
third_party/iroh-live/web-transport-iroh/src/recv.rs
vendored
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
use std::{
|
||||
io,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use bytes::Bytes;
|
||||
|
||||
use crate::{ReadError, ReadExactError, ReadToEndError, SessionError};
|
||||
|
||||
/// A stream that can be used to recieve bytes. See [`quinn::RecvStream`].
|
||||
#[derive(Debug)]
|
||||
pub struct RecvStream {
|
||||
inner: quinn::RecvStream,
|
||||
}
|
||||
|
||||
impl RecvStream {
|
||||
pub(crate) fn new(stream: quinn::RecvStream) -> Self {
|
||||
Self { inner: stream }
|
||||
}
|
||||
|
||||
/// Tell the other end to stop sending data with the given error code. See [`quinn::RecvStream::stop`].
|
||||
/// This is a u32 with WebTransport since it shares the error space with HTTP/3.
|
||||
pub fn stop(&mut self, code: u32) -> Result<(), quinn::ClosedStream> {
|
||||
let code = web_transport_proto::error_to_http3(code);
|
||||
let code = quinn::VarInt::try_from(code).unwrap();
|
||||
self.inner.stop(code)
|
||||
}
|
||||
|
||||
// Unfortunately, we have to wrap ReadError for a bunch of functions.
|
||||
|
||||
/// Read some data into the buffer and return the amount read. See [`quinn::RecvStream::read`].
|
||||
pub async fn read(&mut self, buf: &mut [u8]) -> Result<Option<usize>, ReadError> {
|
||||
self.inner.read(buf).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Fill the entire buffer with data. See [`quinn::RecvStream::read_exact`].
|
||||
pub async fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), ReadExactError> {
|
||||
self.inner.read_exact(buf).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Read a chunk of data from the stream. See [`quinn::RecvStream::read_chunk`].
|
||||
pub async fn read_chunk(
|
||||
&mut self,
|
||||
max_length: usize,
|
||||
) -> Result<Option<quinn::Chunk>, ReadError> {
|
||||
self.inner
|
||||
.read_chunk(max_length)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Read chunks of data from the stream. See [`quinn::RecvStream::read_chunks`].
|
||||
pub async fn read_chunks(&mut self, bufs: &mut [Bytes]) -> Result<Option<usize>, ReadError> {
|
||||
self.inner.read_chunks(bufs).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Read until the end of the stream or the limit is hit. See [`quinn::RecvStream::read_to_end`].
|
||||
pub async fn read_to_end(&mut self, size_limit: usize) -> Result<Vec<u8>, ReadToEndError> {
|
||||
self.inner.read_to_end(size_limit).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Block until the stream has been reset and return the error code. See [`quinn::RecvStream::received_reset`].
|
||||
///
|
||||
/// Unlike Quinn, this returns a SessionError, not a ResetError, because 0-RTT is not supported.
|
||||
pub async fn received_reset(&mut self) -> Result<Option<u32>, SessionError> {
|
||||
match self.inner.received_reset().await {
|
||||
Ok(None) => Ok(None),
|
||||
Ok(Some(code)) => Ok(Some(
|
||||
web_transport_proto::error_from_http3(code.into_inner()).unwrap(),
|
||||
)),
|
||||
Err(quinn::ResetError::ConnectionLost(e)) => Err(e.into()),
|
||||
Err(quinn::ResetError::ZeroRttRejected) => unreachable!("0-RTT not supported"),
|
||||
}
|
||||
}
|
||||
|
||||
// We purposely don't expose the stream ID or 0RTT because it's not valid with WebTransport
|
||||
}
|
||||
|
||||
impl tokio::io::AsyncRead for RecvStream {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut tokio::io::ReadBuf,
|
||||
) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.inner).poll_read(cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl web_transport_trait::RecvStream for RecvStream {
|
||||
type Error = ReadError;
|
||||
|
||||
fn stop(&mut self, code: u32) {
|
||||
Self::stop(self, code).ok();
|
||||
}
|
||||
|
||||
async fn read(&mut self, dst: &mut [u8]) -> Result<Option<usize>, Self::Error> {
|
||||
self.read(dst).await
|
||||
}
|
||||
|
||||
async fn read_chunk(&mut self, max: usize) -> Result<Option<Bytes>, Self::Error> {
|
||||
self.read_chunk(max)
|
||||
.await
|
||||
.map(|r| r.map(|chunk| chunk.bytes))
|
||||
}
|
||||
|
||||
async fn closed(&mut self) -> Result<(), Self::Error> {
|
||||
self.received_reset().await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
142
third_party/iroh-live/web-transport-iroh/src/send.rs
vendored
Normal file
142
third_party/iroh-live/web-transport-iroh/src/send.rs
vendored
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
use std::{
|
||||
io,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use bytes::{Buf, Bytes};
|
||||
|
||||
use crate::{ClosedStream, SessionError, WriteError};
|
||||
|
||||
/// A stream that can be used to send bytes. See [`quinn::SendStream`].
|
||||
///
|
||||
/// This wrapper is mainly needed for error codes, which is unfortunate.
|
||||
/// WebTransport uses u32 error codes and they're mapped in a reserved HTTP/3 error space.
|
||||
#[derive(Debug)]
|
||||
pub struct SendStream {
|
||||
stream: quinn::SendStream,
|
||||
}
|
||||
|
||||
impl SendStream {
|
||||
pub(crate) fn new(stream: quinn::SendStream) -> Self {
|
||||
Self { stream }
|
||||
}
|
||||
|
||||
/// Abruptly reset the stream with the provided error code. See [`quinn::SendStream::reset`].
|
||||
/// This is a u32 with WebTransport because we share the error space with HTTP/3.
|
||||
pub fn reset(&mut self, code: u32) -> Result<(), ClosedStream> {
|
||||
let code = web_transport_proto::error_to_http3(code);
|
||||
let code = quinn::VarInt::try_from(code).unwrap();
|
||||
self.stream.reset(code).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Wait until the stream has been stopped and return the error code. See [`quinn::SendStream::stopped`].
|
||||
///
|
||||
/// Unlike Quinn, this returns None if the code is not a valid WebTransport error code.
|
||||
/// Also unlike Quinn, this returns a SessionError, not a StoppedError, because 0-RTT is not supported.
|
||||
pub async fn stopped(&mut self) -> Result<Option<u32>, SessionError> {
|
||||
match self.stream.stopped().await {
|
||||
Ok(Some(code)) => Ok(web_transport_proto::error_from_http3(code.into_inner())),
|
||||
Ok(None) => Ok(None),
|
||||
Err(quinn::StoppedError::ConnectionLost(e)) => Err(e.into()),
|
||||
Err(quinn::StoppedError::ZeroRttRejected) => unreachable!("0-RTT not supported"),
|
||||
}
|
||||
}
|
||||
|
||||
// Unfortunately, we have to wrap WriteError for a bunch of functions.
|
||||
|
||||
/// Write some data to the stream, returning the size written. See [`quinn::SendStream::write`].
|
||||
pub async fn write(&mut self, buf: &[u8]) -> Result<usize, WriteError> {
|
||||
self.stream.write(buf).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Write all of the data to the stream. See [`quinn::SendStream::write_all`].
|
||||
pub async fn write_all(&mut self, buf: &[u8]) -> Result<(), WriteError> {
|
||||
self.stream.write_all(buf).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Write chunks of data to the stream. See [`quinn::SendStream::write_chunks`].
|
||||
pub async fn write_chunks(&mut self, bufs: &mut [Bytes]) -> Result<quinn::Written, WriteError> {
|
||||
self.stream.write_chunks(bufs).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Write a chunk of data to the stream. See [`quinn::SendStream::write_chunk`].
|
||||
pub async fn write_chunk(&mut self, buf: Bytes) -> Result<(), WriteError> {
|
||||
self.stream.write_chunk(buf).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Write all of the chunks of data to the stream. See [`quinn::SendStream::write_all_chunks`].
|
||||
pub async fn write_all_chunks(&mut self, bufs: &mut [Bytes]) -> Result<(), WriteError> {
|
||||
self.stream.write_all_chunks(bufs).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Mark the stream as finished, such that no more data can be written. See [`quinn::SendStream::finish`].
|
||||
pub fn finish(&mut self) -> Result<(), ClosedStream> {
|
||||
self.stream.finish().map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn set_priority(&self, order: i32) -> Result<(), ClosedStream> {
|
||||
self.stream.set_priority(order).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn priority(&self) -> Result<i32, ClosedStream> {
|
||||
self.stream.priority().map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl tokio::io::AsyncWrite for SendStream {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
// We have to use this syntax because quinn added its own poll_write method.
|
||||
tokio::io::AsyncWrite::poll_write(Pin::new(&mut self.stream), cx, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.stream).poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.stream).poll_shutdown(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl web_transport_trait::SendStream for SendStream {
|
||||
type Error = WriteError;
|
||||
|
||||
fn set_priority(&mut self, order: u8) {
|
||||
self.stream.set_priority(order.into()).ok();
|
||||
}
|
||||
|
||||
fn reset(&mut self, code: u32) {
|
||||
Self::reset(self, code).ok();
|
||||
}
|
||||
|
||||
fn finish(&mut self) -> Result<(), Self::Error> {
|
||||
Self::finish(self).map_err(|_| WriteError::ClosedStream)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
|
||||
Self::write(self, buf).await
|
||||
}
|
||||
|
||||
async fn write_buf<B: Buf + Send>(&mut self, buf: &mut B) -> Result<usize, Self::Error> {
|
||||
// This can avoid making a copy when Buf is Bytes, as Quinn will allocate anyway.
|
||||
let size = buf.chunk().len();
|
||||
let chunk = buf.copy_to_bytes(size);
|
||||
self.write_chunk(chunk).await?;
|
||||
Ok(size)
|
||||
}
|
||||
|
||||
async fn write_chunk(&mut self, chunk: Bytes) -> Result<(), Self::Error> {
|
||||
self.write_chunk(chunk).await
|
||||
}
|
||||
|
||||
async fn closed(&mut self) -> Result<(), Self::Error> {
|
||||
self.stopped().await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
76
third_party/iroh-live/web-transport-iroh/src/server.rs
vendored
Normal file
76
third_party/iroh-live/web-transport-iroh/src/server.rs
vendored
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
use url::Url;
|
||||
|
||||
use crate::{Connect, ServerError, Session, Settings};
|
||||
|
||||
/// A QUIC-only WebTransport handshake, awaiting server decision.
|
||||
pub struct QuicRequest {
|
||||
conn: iroh::endpoint::Connection,
|
||||
}
|
||||
|
||||
/// An H3 WebTransport handshake, SETTINGS exchanged and CONNECT accepted,
|
||||
/// awaiting server decision (respond OK / reject).
|
||||
pub struct H3Request {
|
||||
conn: iroh::endpoint::Connection,
|
||||
settings: Settings,
|
||||
connect: Connect,
|
||||
}
|
||||
|
||||
impl QuicRequest {
|
||||
/// Accept a new QUIC-only WebTransport session from a client.
|
||||
pub fn accept(conn: iroh::endpoint::Connection) -> Self {
|
||||
Self { conn }
|
||||
}
|
||||
|
||||
pub fn conn(&self) -> &iroh::endpoint::Connection {
|
||||
&self.conn
|
||||
}
|
||||
|
||||
/// Accept the session.
|
||||
pub fn ok(self) -> Session {
|
||||
Session::raw(self.conn)
|
||||
}
|
||||
|
||||
/// Reject the session.
|
||||
pub fn close(self, status: http::StatusCode) {
|
||||
self.conn
|
||||
.close(status.as_u16().into(), status.as_str().as_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
impl H3Request {
|
||||
/// Accept a new H3 WebTransport session from a client.
|
||||
pub async fn accept(conn: iroh::endpoint::Connection) -> Result<Self, ServerError> {
|
||||
// Perform the H3 handshake by sending/receiving SETTINGS frames.
|
||||
let settings = Settings::connect(&conn).await?;
|
||||
|
||||
// Accept the CONNECT request but don't send a response yet.
|
||||
let connect = Connect::accept(&conn).await?;
|
||||
|
||||
Ok(Self {
|
||||
conn,
|
||||
settings,
|
||||
connect,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the URL provided by the client.
|
||||
pub fn url(&self) -> &Url {
|
||||
self.connect.url()
|
||||
}
|
||||
|
||||
pub fn conn(&self) -> &iroh::endpoint::Connection {
|
||||
&self.conn
|
||||
}
|
||||
|
||||
/// Accept the session, returning a 200 OK.
|
||||
pub async fn ok(mut self) -> Result<Session, ServerError> {
|
||||
self.connect.respond(http::StatusCode::OK).await?;
|
||||
Ok(Session::new_h3(self.conn, self.settings, self.connect))
|
||||
}
|
||||
|
||||
/// Reject the session, returning your favorite HTTP status code.
|
||||
pub async fn close(mut self, status: http::StatusCode) -> Result<(), ServerError> {
|
||||
self.connect.respond(status).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
540
third_party/iroh-live/web-transport-iroh/src/session.rs
vendored
Normal file
540
third_party/iroh-live/web-transport-iroh/src/session.rs
vendored
Normal file
|
|
@ -0,0 +1,540 @@
|
|||
use std::{
|
||||
fmt,
|
||||
future::{Future, poll_fn},
|
||||
io::Cursor,
|
||||
ops::Deref,
|
||||
pin::Pin,
|
||||
sync::{Arc, Mutex},
|
||||
task::{Context, Poll, ready},
|
||||
};
|
||||
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use iroh::endpoint::Connection;
|
||||
use n0_future::{
|
||||
FuturesUnordered,
|
||||
stream::{Stream, StreamExt},
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
ClientError, Connect, RecvStream, SendStream, SessionError, Settings, WebTransportError,
|
||||
};
|
||||
|
||||
use web_transport_proto::{Frame, StreamUni, VarInt};
|
||||
|
||||
/// An established WebTransport session, acting like a full QUIC connection. See [`iroh::endpoint::Connection`].
|
||||
///
|
||||
/// It is important to remember that WebTransport is layered on top of QUIC:
|
||||
/// 1. Each stream starts with a few bytes identifying the stream type and session ID.
|
||||
/// 2. Errors codes are encoded with the session ID, so they aren't full QUIC error codes.
|
||||
/// 3. Stream IDs may have gaps in them, used by HTTP/3 transparant to the application.
|
||||
///
|
||||
/// Deref is used to expose non-overloaded methods on [`iroh::endpoint::Connection`].
|
||||
/// These should be safe to use with WebTransport, but file a PR if you find one that isn't.
|
||||
#[derive(Clone)]
|
||||
pub struct Session {
|
||||
conn: Connection,
|
||||
h3: Option<H3SessionState>,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
/// Create a new session from a raw QUIC connection and a URL.
|
||||
///
|
||||
/// This is used to pretend like a QUIC connection is a WebTransport session.
|
||||
/// It's a hack, but it makes it much easier to support WebTransport and raw QUIC simultaneously.
|
||||
pub fn raw(conn: Connection) -> Self {
|
||||
Self { conn, h3: None }
|
||||
}
|
||||
|
||||
/// Connect using an established QUIC connection if you want to create the connection yourself.
|
||||
/// This will only work with a brand new QUIC connection using the HTTP/3 ALPN.
|
||||
pub async fn connect_h3(conn: Connection, url: Url) -> Result<Session, ClientError> {
|
||||
// Perform the H3 handshake by sending/reciving SETTINGS frames.
|
||||
let settings = Settings::connect(&conn).await?;
|
||||
|
||||
// Send the HTTP/3 CONNECT request.
|
||||
let connect = Connect::open(&conn, url).await?;
|
||||
|
||||
Ok(Self::new_h3(conn, settings, connect))
|
||||
}
|
||||
|
||||
pub fn new_h3(conn: Connection, settings: Settings, connect: Connect) -> Self {
|
||||
let h3 = H3SessionState::connect(conn.clone(), settings, &connect);
|
||||
let this = Session { conn, h3: Some(h3) };
|
||||
// Run a background task to check if the connect stream is closed.
|
||||
let this2 = this.clone();
|
||||
tokio::spawn(async move {
|
||||
let (code, reason) = connect.run_closed().await;
|
||||
if this2.conn().close_reason().is_none() {
|
||||
// TODO We shouldn't be closing the QUIC connection with the same error.
|
||||
this2.close(code, reason.as_bytes());
|
||||
}
|
||||
});
|
||||
this
|
||||
}
|
||||
|
||||
pub fn conn(&self) -> &Connection {
|
||||
&self.conn
|
||||
}
|
||||
|
||||
pub fn url(&self) -> Option<&Url> {
|
||||
self.h3.as_ref().map(|s| &s.url)
|
||||
}
|
||||
|
||||
/// Accept a new unidirectional stream. See [`iroh::endpoint::Connection::accept_uni`].
|
||||
pub async fn accept_uni(&self) -> Result<RecvStream, SessionError> {
|
||||
if let Some(h3) = &self.h3 {
|
||||
poll_fn(|cx| h3.accept.lock().unwrap().poll_accept_uni(cx)).await
|
||||
} else {
|
||||
self.conn
|
||||
.accept_uni()
|
||||
.await
|
||||
.map(RecvStream::new)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// Accept a new bidirectional stream. See [`iroh::endpoint::Connection::accept_bi`].
|
||||
pub async fn accept_bi(&self) -> Result<(SendStream, RecvStream), SessionError> {
|
||||
if let Some(h3) = &self.h3 {
|
||||
poll_fn(|cx| h3.accept.lock().unwrap().poll_accept_bi(cx)).await
|
||||
} else {
|
||||
self.conn
|
||||
.accept_bi()
|
||||
.await
|
||||
.map(|(send, recv)| (SendStream::new(send), RecvStream::new(recv)))
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// Open a new unidirectional stream. See [`iroh::endpoint::Connection::open_uni`].
|
||||
pub async fn open_uni(&self) -> Result<SendStream, SessionError> {
|
||||
let mut send = self.conn.open_uni().await?;
|
||||
|
||||
if let Some(h3) = self.h3.as_ref() {
|
||||
write_full_with_max_prio(&mut send, &h3.header_uni).await?;
|
||||
}
|
||||
|
||||
Ok(SendStream::new(send))
|
||||
}
|
||||
|
||||
/// Open a new bidirectional stream. See [`iroh::endpoint::Connection::open_bi`].
|
||||
pub async fn open_bi(&self) -> Result<(SendStream, RecvStream), SessionError> {
|
||||
let (mut send, recv) = self.conn.open_bi().await?;
|
||||
|
||||
if let Some(h3) = self.h3.as_ref() {
|
||||
write_full_with_max_prio(&mut send, &h3.header_bi).await?;
|
||||
}
|
||||
|
||||
Ok((SendStream::new(send), RecvStream::new(recv)))
|
||||
}
|
||||
|
||||
/// 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 bytes.
|
||||
pub async fn read_datagram(&self) -> Result<Bytes, SessionError> {
|
||||
let mut datagram = self
|
||||
.conn
|
||||
.read_datagram()
|
||||
.await
|
||||
.map_err(SessionError::from)?;
|
||||
|
||||
let datagram = if let Some(h3) = self.h3.as_ref() {
|
||||
let mut cursor = Cursor::new(&datagram);
|
||||
|
||||
// We have to check and strip the session ID from the datagram.
|
||||
let actual_id =
|
||||
VarInt::decode(&mut cursor).map_err(|_| WebTransportError::UnknownSession)?;
|
||||
if actual_id != h3.session_id {
|
||||
return Err(WebTransportError::UnknownSession.into());
|
||||
}
|
||||
|
||||
// Return the datagram without the session ID.
|
||||
let datagram = datagram.split_off(cursor.position() as usize);
|
||||
datagram
|
||||
} else {
|
||||
datagram
|
||||
};
|
||||
|
||||
Ok(datagram)
|
||||
}
|
||||
|
||||
/// Sends an application datagram to the remote peer.
|
||||
///
|
||||
/// Datagrams are unreliable and may be dropped or delivered out of order.
|
||||
/// The data must be smaller than [`max_datagram_size`](Self::max_datagram_size).
|
||||
pub fn send_datagram(&self, data: Bytes) -> Result<(), SessionError> {
|
||||
let datagram = if let Some(h3) = self.h3.as_ref() {
|
||||
// Unfortunately, we need to allocate/copy each datagram because of the Quinn API.
|
||||
// Pls go +1 if you care: https://github.com/quinn-rs/quinn/issues/1724
|
||||
let mut buf = BytesMut::with_capacity(h3.header_datagram.len() + data.len());
|
||||
// Prepend the datagram with the header indicating the session ID.
|
||||
buf.extend_from_slice(&h3.header_datagram);
|
||||
buf.extend_from_slice(&data);
|
||||
buf.into()
|
||||
} else {
|
||||
data
|
||||
};
|
||||
|
||||
self.conn.send_datagram(datagram)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Computes the maximum size of datagrams that may be passed to
|
||||
/// [`send_datagram`](Self::send_datagram).
|
||||
pub fn max_datagram_size(&self) -> usize {
|
||||
let mtu = self
|
||||
.conn
|
||||
.max_datagram_size()
|
||||
.expect("datagram support is required");
|
||||
if let Some(h3) = self.h3.as_ref() {
|
||||
mtu.saturating_sub(h3.header_datagram.len())
|
||||
} else {
|
||||
mtu
|
||||
}
|
||||
}
|
||||
|
||||
/// Immediately close the connection with an error code and reason. See [`iroh::endpoint::Connection::close`].
|
||||
pub fn close(&self, code: u32, reason: &[u8]) {
|
||||
let code = if self.h3.is_some() {
|
||||
web_transport_proto::error_to_http3(code)
|
||||
.try_into()
|
||||
.unwrap()
|
||||
} else {
|
||||
code.into()
|
||||
};
|
||||
|
||||
self.conn.close(code, reason)
|
||||
}
|
||||
|
||||
/// Wait until the session is closed, returning the error. See [`iroh::endpoint::Connection::closed`].
|
||||
pub async fn closed(&self) -> SessionError {
|
||||
self.conn.closed().await.into()
|
||||
}
|
||||
|
||||
/// Return why the session was closed, or None if it's not closed. See [`iroh::endpoint::Connection::close_reason`].
|
||||
pub fn close_reason(&self) -> Option<SessionError> {
|
||||
self.conn.close_reason().map(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
async fn write_full_with_max_prio(
|
||||
send: &mut quinn::SendStream,
|
||||
buf: &[u8],
|
||||
) -> Result<(), SessionError> {
|
||||
// Set the stream priority to max and then write the stream header.
|
||||
// Otherwise the application could write data with lower priority than the header, resulting in queuing.
|
||||
// Also the header is very important for determining the session ID without reliable reset.
|
||||
send.set_priority(i32::MAX).ok();
|
||||
let res = match send.write_all(buf).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(quinn::WriteError::ConnectionLost(err)) => Err(err.into()),
|
||||
Err(err) => Err(WebTransportError::WriteError(err).into()),
|
||||
};
|
||||
// Reset the stream priority back to the default of 0.
|
||||
send.set_priority(0).ok();
|
||||
res
|
||||
}
|
||||
|
||||
impl Deref for Session {
|
||||
type Target = Connection;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.conn
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Session {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.conn.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Session {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.conn.stable_id() == other.conn.stable_id()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Session {}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct H3SessionState {
|
||||
url: Url,
|
||||
// The session ID, as determined by the stream ID of the connect request.
|
||||
session_id: VarInt,
|
||||
// Cache the headers in front of each stream we open.
|
||||
header_uni: Vec<u8>,
|
||||
header_bi: Vec<u8>,
|
||||
header_datagram: Vec<u8>,
|
||||
|
||||
// Keep a reference to the settings and connect stream to avoid closing them until dropped.
|
||||
#[allow(dead_code)]
|
||||
settings: Arc<Settings>,
|
||||
// The accept logic is stateful, so use an Arc<Mutex> to share it.
|
||||
accept: Arc<Mutex<H3SessionAccept>>,
|
||||
}
|
||||
|
||||
impl H3SessionState {
|
||||
fn connect(conn: Connection, settings: Settings, connect: &Connect) -> Self {
|
||||
// The session ID is the stream ID of the CONNECT request.
|
||||
let session_id = connect.session_id();
|
||||
|
||||
// Cache the tiny header we write in front of each stream we open.
|
||||
let mut header_uni = Vec::new();
|
||||
StreamUni::WEBTRANSPORT.encode(&mut header_uni);
|
||||
session_id.encode(&mut header_uni);
|
||||
|
||||
let mut header_bi = Vec::new();
|
||||
Frame::WEBTRANSPORT.encode(&mut header_bi);
|
||||
session_id.encode(&mut header_bi);
|
||||
|
||||
let mut header_datagram = Vec::new();
|
||||
session_id.encode(&mut header_datagram);
|
||||
|
||||
// Accept logic is stateful, so use an Arc<Mutex> to share it.
|
||||
let accept = H3SessionAccept::new(conn, session_id);
|
||||
Self {
|
||||
url: connect.url().clone(),
|
||||
session_id,
|
||||
header_uni,
|
||||
header_bi,
|
||||
header_datagram,
|
||||
settings: Arc::new(settings),
|
||||
accept: Arc::new(Mutex::new(accept)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Type aliases just so clippy doesn't complain about the complexity.
|
||||
type AcceptUni =
|
||||
dyn Stream<Item = Result<quinn::RecvStream, iroh::endpoint::ConnectionError>> + Send;
|
||||
type AcceptBi = dyn Stream<Item = Result<(quinn::SendStream, quinn::RecvStream), iroh::endpoint::ConnectionError>>
|
||||
+ Send;
|
||||
type PendingUni = dyn Future<Output = Result<(StreamUni, quinn::RecvStream), SessionError>> + Send;
|
||||
type PendingBi = dyn Future<Output = Result<Option<(quinn::SendStream, quinn::RecvStream)>, SessionError>>
|
||||
+ Send;
|
||||
|
||||
// Logic just for accepting streams, which is annoying because of the stream header.
|
||||
pub struct H3SessionAccept {
|
||||
session_id: VarInt,
|
||||
|
||||
// We also need to keep a reference to the qpack streams if the endpoint (incorrectly) creates them.
|
||||
// Again, this is just so they don't get closed until we drop the session.
|
||||
qpack_encoder: Option<quinn::RecvStream>,
|
||||
qpack_decoder: Option<quinn::RecvStream>,
|
||||
|
||||
accept_uni: Pin<Box<AcceptUni>>,
|
||||
accept_bi: Pin<Box<AcceptBi>>,
|
||||
|
||||
// Keep track of work being done to read/write the WebTransport stream header.
|
||||
pending_uni: FuturesUnordered<Pin<Box<PendingUni>>>,
|
||||
pending_bi: FuturesUnordered<Pin<Box<PendingBi>>>,
|
||||
}
|
||||
|
||||
impl H3SessionAccept {
|
||||
pub(crate) fn new(conn: Connection, session_id: VarInt) -> Self {
|
||||
// Create a stream that just outputs new streams, so it's easy to call from poll.
|
||||
let accept_uni = Box::pin(n0_future::stream::unfold(conn.clone(), |conn| async {
|
||||
Some((conn.accept_uni().await, conn))
|
||||
}));
|
||||
|
||||
let accept_bi = Box::pin(n0_future::stream::unfold(conn, |conn| async {
|
||||
Some((conn.accept_bi().await, conn))
|
||||
}));
|
||||
|
||||
Self {
|
||||
session_id,
|
||||
|
||||
qpack_decoder: None,
|
||||
qpack_encoder: None,
|
||||
|
||||
accept_uni,
|
||||
accept_bi,
|
||||
|
||||
pending_uni: FuturesUnordered::new(),
|
||||
pending_bi: FuturesUnordered::new(),
|
||||
}
|
||||
}
|
||||
|
||||
// This is poll-based because we accept and decode streams in parallel.
|
||||
// In async land I would use tokio::JoinSet, but that requires a runtime.
|
||||
// It's better to use FuturesUnordered instead because it's agnostic.
|
||||
pub fn poll_accept_uni(
|
||||
&mut self,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Result<RecvStream, SessionError>> {
|
||||
loop {
|
||||
// Accept any new streams.
|
||||
if let Poll::Ready(Some(res)) = self.accept_uni.poll_next(cx) {
|
||||
// Start decoding the header and add the future to the list of pending streams.
|
||||
let recv = res?;
|
||||
let pending = Self::decode_uni(recv, self.session_id);
|
||||
self.pending_uni.push(Box::pin(pending));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Poll the list of pending streams.
|
||||
let (typ, recv) = match ready!(self.pending_uni.poll_next(cx)) {
|
||||
Some(Ok(res)) => res,
|
||||
Some(Err(err)) => {
|
||||
// Ignore the error, the stream was probably reset early.
|
||||
tracing::warn!("failed to decode unidirectional stream: {err:?}");
|
||||
continue;
|
||||
}
|
||||
None => return Poll::Pending,
|
||||
};
|
||||
|
||||
// Decide if we keep looping based on the type.
|
||||
match typ {
|
||||
StreamUni::WEBTRANSPORT => {
|
||||
let recv = RecvStream::new(recv);
|
||||
return Poll::Ready(Ok(recv));
|
||||
}
|
||||
StreamUni::QPACK_DECODER => {
|
||||
self.qpack_decoder = Some(recv);
|
||||
}
|
||||
StreamUni::QPACK_ENCODER => {
|
||||
self.qpack_encoder = Some(recv);
|
||||
}
|
||||
_ => {
|
||||
// ignore unknown streams
|
||||
tracing::debug!("ignoring unknown unidirectional stream: {typ:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reads the stream header, returning the stream type.
|
||||
async fn decode_uni(
|
||||
mut recv: quinn::RecvStream,
|
||||
expected_session: VarInt,
|
||||
) -> Result<(StreamUni, quinn::RecvStream), SessionError> {
|
||||
// Read the VarInt at the start of the stream.
|
||||
let typ = VarInt::read(&mut recv)
|
||||
.await
|
||||
.map_err(|_| WebTransportError::UnknownSession)?;
|
||||
let typ = StreamUni(typ);
|
||||
|
||||
if typ == StreamUni::WEBTRANSPORT {
|
||||
// Read the session_id and validate it
|
||||
let session_id = VarInt::read(&mut recv)
|
||||
.await
|
||||
.map_err(|_| WebTransportError::UnknownSession)?;
|
||||
if session_id != expected_session {
|
||||
return Err(WebTransportError::UnknownSession.into());
|
||||
}
|
||||
}
|
||||
|
||||
// We need to keep a reference to the qpack streams if the endpoint (incorrectly) creates them, so return everything.
|
||||
Ok((typ, recv))
|
||||
}
|
||||
|
||||
pub fn poll_accept_bi(
|
||||
&mut self,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Result<(SendStream, RecvStream), SessionError>> {
|
||||
loop {
|
||||
// Accept any new streams.
|
||||
if let Poll::Ready(Some(res)) = self.accept_bi.poll_next(cx) {
|
||||
// Start decoding the header and add the future to the list of pending streams.
|
||||
let (send, recv) = res?;
|
||||
let pending = Self::decode_bi(send, recv, self.session_id);
|
||||
self.pending_bi.push(Box::pin(pending));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Poll the list of pending streams.
|
||||
let res = match ready!(self.pending_bi.poll_next(cx)) {
|
||||
Some(Ok(res)) => res,
|
||||
Some(Err(err)) => {
|
||||
// Ignore the error, the stream was probably reset early.
|
||||
tracing::warn!("failed to decode bidirectional stream: {err:?}");
|
||||
continue;
|
||||
}
|
||||
None => return Poll::Pending,
|
||||
};
|
||||
|
||||
if let Some((send, recv)) = res {
|
||||
// Wrap the streams in our own types for correct error codes.
|
||||
let send = SendStream::new(send);
|
||||
let recv = RecvStream::new(recv);
|
||||
return Poll::Ready(Ok((send, recv)));
|
||||
}
|
||||
|
||||
// Keep looping if it's a stream we want to ignore.
|
||||
}
|
||||
}
|
||||
|
||||
// Reads the stream header, returning Some if it's a WebTransport stream.
|
||||
async fn decode_bi(
|
||||
send: quinn::SendStream,
|
||||
mut recv: quinn::RecvStream,
|
||||
expected_session: VarInt,
|
||||
) -> Result<Option<(quinn::SendStream, quinn::RecvStream)>, SessionError> {
|
||||
let typ = VarInt::read(&mut recv)
|
||||
.await
|
||||
.map_err(|_| WebTransportError::UnknownSession)?;
|
||||
if Frame(typ) != Frame::WEBTRANSPORT {
|
||||
tracing::debug!("ignoring unknown bidirectional stream: {typ:?}");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Read the session ID and validate it.
|
||||
let session_id = VarInt::read(&mut recv)
|
||||
.await
|
||||
.map_err(|_| WebTransportError::UnknownSession)?;
|
||||
if session_id != expected_session {
|
||||
return Err(WebTransportError::UnknownSession.into());
|
||||
}
|
||||
|
||||
Ok(Some((send, recv)))
|
||||
}
|
||||
}
|
||||
|
||||
impl web_transport_trait::Session for Session {
|
||||
type SendStream = SendStream;
|
||||
type RecvStream = RecvStream;
|
||||
type Error = SessionError;
|
||||
|
||||
async fn accept_uni(&self) -> Result<Self::RecvStream, Self::Error> {
|
||||
Self::accept_uni(self).await
|
||||
}
|
||||
|
||||
async fn accept_bi(&self) -> Result<(Self::SendStream, Self::RecvStream), Self::Error> {
|
||||
Self::accept_bi(self).await
|
||||
}
|
||||
|
||||
async fn open_bi(&self) -> Result<(Self::SendStream, Self::RecvStream), Self::Error> {
|
||||
Self::open_bi(self).await
|
||||
}
|
||||
|
||||
async fn open_uni(&self) -> Result<Self::SendStream, Self::Error> {
|
||||
Self::open_uni(self).await
|
||||
}
|
||||
|
||||
fn close(&self, code: u32, reason: &str) {
|
||||
Self::close(self, code, reason.as_bytes());
|
||||
}
|
||||
|
||||
async fn closed(&self) -> Self::Error {
|
||||
Self::closed(self).await
|
||||
}
|
||||
|
||||
fn send_datagram(&self, data: Bytes) -> Result<(), Self::Error> {
|
||||
Self::send_datagram(self, data)
|
||||
}
|
||||
|
||||
async fn recv_datagram(&self) -> Result<Bytes, Self::Error> {
|
||||
Self::read_datagram(self).await
|
||||
}
|
||||
|
||||
fn max_datagram_size(&self) -> usize {
|
||||
Self::max_datagram_size(self)
|
||||
}
|
||||
}
|
||||
69
third_party/iroh-live/web-transport-iroh/src/settings.rs
vendored
Normal file
69
third_party/iroh-live/web-transport-iroh/src/settings.rs
vendored
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
use thiserror::Error;
|
||||
use tokio::try_join;
|
||||
|
||||
#[derive(Error, Debug, Clone)]
|
||||
pub enum SettingsError {
|
||||
#[error("quic stream was closed early")]
|
||||
UnexpectedEnd,
|
||||
|
||||
#[error("protocol error: {0}")]
|
||||
ProtoError(#[from] web_transport_proto::SettingsError),
|
||||
|
||||
#[error("WebTransport is not supported")]
|
||||
WebTransportUnsupported,
|
||||
|
||||
#[error("connection error")]
|
||||
ConnectionError(#[from] iroh::endpoint::ConnectionError),
|
||||
|
||||
#[error("read error")]
|
||||
ReadError(#[from] quinn::ReadError),
|
||||
|
||||
#[error("write error")]
|
||||
WriteError(#[from] quinn::WriteError),
|
||||
}
|
||||
|
||||
pub struct Settings {
|
||||
// A reference to the send/recv stream, so we don't close it until dropped.
|
||||
#[allow(dead_code)]
|
||||
send: quinn::SendStream,
|
||||
|
||||
#[allow(dead_code)]
|
||||
recv: quinn::RecvStream,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
// Establish the H3 connection.
|
||||
pub async fn connect(conn: &iroh::endpoint::Connection) -> Result<Self, SettingsError> {
|
||||
let recv = Self::accept(conn);
|
||||
let send = Self::open(conn);
|
||||
|
||||
// Run both tasks concurrently until one errors or they both complete.
|
||||
let (send, recv) = try_join!(send, recv)?;
|
||||
Ok(Self { send, recv })
|
||||
}
|
||||
|
||||
async fn accept(conn: &iroh::endpoint::Connection) -> Result<quinn::RecvStream, SettingsError> {
|
||||
let mut recv = conn.accept_uni().await?;
|
||||
let settings = web_transport_proto::Settings::read(&mut recv).await?;
|
||||
|
||||
tracing::debug!("received SETTINGS frame: {settings:?}");
|
||||
|
||||
if settings.supports_webtransport() == 0 {
|
||||
return Err(SettingsError::WebTransportUnsupported);
|
||||
}
|
||||
|
||||
Ok(recv)
|
||||
}
|
||||
|
||||
async fn open(conn: &iroh::endpoint::Connection) -> Result<quinn::SendStream, SettingsError> {
|
||||
let mut settings = web_transport_proto::Settings::default();
|
||||
settings.enable_webtransport(1);
|
||||
|
||||
tracing::debug!("sending SETTINGS frame: {settings:?}");
|
||||
|
||||
let mut send = conn.open_uni().await?;
|
||||
settings.write(&mut send).await?;
|
||||
|
||||
Ok(send)
|
||||
}
|
||||
}
|
||||
128
third_party/iroh-live/web-transport-iroh/src/tests.rs
vendored
Normal file
128
third_party/iroh-live/web-transport-iroh/src/tests.rs
vendored
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
use iroh::{Endpoint, endpoint::ConnectionError};
|
||||
use n0_tracing_test::traced_test;
|
||||
use tracing::Instrument;
|
||||
use url::Url;
|
||||
|
||||
use crate::{ALPN_H3, Client, H3Request, QuicRequest, SessionError};
|
||||
|
||||
#[tokio::test]
|
||||
#[traced_test]
|
||||
async fn h3_smoke() -> n0_error::Result<()> {
|
||||
let client = Endpoint::bind()
|
||||
.instrument(tracing::error_span!("client-ep"))
|
||||
.await
|
||||
.unwrap();
|
||||
let client_id = client.id();
|
||||
let client = Client::new(client);
|
||||
|
||||
let server = Endpoint::builder()
|
||||
.alpns(vec![ALPN_H3.as_bytes().to_vec()])
|
||||
.bind()
|
||||
.instrument(tracing::error_span!("server-ep"))
|
||||
.await
|
||||
.unwrap();
|
||||
let server_id = server.id();
|
||||
let server_addr = server.addr();
|
||||
|
||||
let url: Url = format!("https://{}/foo", server_id).parse().unwrap();
|
||||
|
||||
let client_task = tokio::task::spawn({
|
||||
let url = url.clone();
|
||||
async move {
|
||||
let session = client.connect_h3(server_addr, url.clone()).await.inspect_err(|err| println!("{err:#?}")).unwrap();
|
||||
assert_eq!(session.remote_id(), server_id);
|
||||
assert_eq!(session.url(), Some(&url));
|
||||
|
||||
let mut stream = session.open_uni().await.unwrap();
|
||||
stream.write_all(b"hi").await.unwrap();
|
||||
stream.finish().unwrap();
|
||||
let reason = session.closed().await;
|
||||
assert!(
|
||||
matches!(reason, SessionError::ConnectionError(ConnectionError::ApplicationClosed(frame)) if web_transport_proto::error_from_http3(frame.error_code.into_inner()) == Some(23))
|
||||
);
|
||||
|
||||
drop(session);
|
||||
client.close().await;
|
||||
}.instrument(tracing::error_span!("client"))
|
||||
});
|
||||
|
||||
let server_task = tokio::task::spawn(
|
||||
async move {
|
||||
let conn = server.accept().await.unwrap().await.unwrap();
|
||||
assert_eq!(conn.alpn(), ALPN_H3.as_bytes());
|
||||
let request = H3Request::accept(conn)
|
||||
.await
|
||||
.inspect_err(|err| tracing::error!("accept failed: {err:?}"))
|
||||
.unwrap();
|
||||
assert_eq!(request.url(), &url);
|
||||
assert_eq!(request.conn().remote_id(), client_id);
|
||||
let session = request.ok().await.unwrap();
|
||||
assert_eq!(session.url(), Some(&url));
|
||||
assert_eq!(session.conn().remote_id(), client_id);
|
||||
let mut stream = session.accept_uni().await.unwrap();
|
||||
let buf = stream.read_to_end(2).await.unwrap();
|
||||
assert_eq!(buf, b"hi");
|
||||
session.close(23, b"bye");
|
||||
server.close().await;
|
||||
}
|
||||
.instrument(tracing::error_span!("server")),
|
||||
);
|
||||
|
||||
client_task.await.unwrap();
|
||||
server_task.await.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[traced_test]
|
||||
async fn quic_smoke() -> n0_error::Result<()> {
|
||||
const ALPN: &str = "moql";
|
||||
|
||||
let client = Endpoint::bind().await.unwrap();
|
||||
let client_id = client.id();
|
||||
let client = Client::new(client);
|
||||
|
||||
let server = Endpoint::builder()
|
||||
.alpns(vec![ALPN.as_bytes().to_vec()])
|
||||
.bind()
|
||||
.await
|
||||
.unwrap();
|
||||
let server_id = server.id();
|
||||
let server_addr = server.addr();
|
||||
|
||||
let client_task = tokio::task::spawn({
|
||||
async move {
|
||||
let session = client
|
||||
.connect_quic(server_addr, ALPN.as_bytes())
|
||||
.await
|
||||
.unwrap();
|
||||
println!("session established");
|
||||
assert_eq!(session.remote_id(), server_id);
|
||||
assert_eq!(session.url(), None);
|
||||
let reason = session.closed().await;
|
||||
assert!(
|
||||
matches!(reason, SessionError::ConnectionError(ConnectionError::ApplicationClosed(frame)) if frame.error_code.into_inner() == 23)
|
||||
)
|
||||
}.instrument(tracing::error_span!("client"))
|
||||
});
|
||||
|
||||
let server_task = tokio::task::spawn({
|
||||
async move {
|
||||
let conn = server.accept().await.unwrap().await.unwrap();
|
||||
assert_eq!(conn.alpn(), ALPN.as_bytes());
|
||||
let request = QuicRequest::accept(conn);
|
||||
assert_eq!(request.conn().remote_id(), client_id);
|
||||
let session = request.ok();
|
||||
assert_eq!(session.url(), None);
|
||||
assert_eq!(session.conn().remote_id(), client_id);
|
||||
session.close(23, b"bye");
|
||||
}
|
||||
.instrument(tracing::error_span!("server"))
|
||||
});
|
||||
|
||||
client_task.await.unwrap();
|
||||
server_task.await.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue