mod app;
mod connect;
mod error;
mod lifetime;
mod stream;
use std::sync::Arc;
use bytes::Buf;
use http_body::Body;
use sciparse::address::ip_socket_addr::ScionSocketIpAddr;
use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken;
use self::{
app::{Http3ClientApp, close_connection},
connect::connect,
lifetime::ConnLifetime,
};
pub use self::{
error::{EstablishError, RequestError, UploadError},
lifetime::{CloseReason, ConnectionToken},
stream::{
CollectError, CollectToStringError, H3DuplexStream, H3ResponseBody, RequestBodyWriter,
ResponseFut,
},
};
pub use crate::h3::common::H3Error;
use crate::{
h3::common::is_terminated,
quic::{config::QuicConfig, connection::ConnectionHandle},
socket::GenericScionUdpSocket,
};
pub struct Http3Client {
remote: ScionSocketIpAddr,
socket: Arc<dyn GenericScionUdpSocket>,
server_name: Option<String>,
config: QuicConfig,
current: Mutex<Option<Connection>>,
closed: CancellationToken,
}
#[derive(Clone)]
struct Connection {
handle: ConnectionHandle<Http3ClientApp>,
lifetime: Arc<ConnLifetime>,
}
impl Connection {
fn is_usable(&self) -> bool {
if self.lifetime.is_closed() {
return false;
}
let conn = self.handle.lock();
!is_terminated(&conn.inner) && !conn.app.driver_stopped
}
}
impl Drop for Http3Client {
fn drop(&mut self) {
self.closed.cancel();
}
}
impl Http3Client {
pub fn new(
remote: ScionSocketIpAddr,
socket: Arc<dyn GenericScionUdpSocket>,
server_name: Option<String>,
) -> Self {
Self::with_config(remote, socket, server_name, QuicConfig::default())
}
pub fn with_config(
remote: ScionSocketIpAddr,
socket: Arc<dyn GenericScionUdpSocket>,
server_name: Option<String>,
config: QuicConfig,
) -> Self {
Self {
remote,
socket,
server_name,
config,
current: Mutex::new(None),
closed: CancellationToken::new(),
}
}
pub fn remote(&self) -> ScionSocketIpAddr {
self.remote
}
pub async fn request_with_writer(
&self,
req: http::Request<()>,
) -> Result<(ResponseFut, RequestBodyWriter), RequestError> {
let conn = self.get_connection().await?;
let (parts, ()) = req.into_parts();
stream::initiate_request(&conn.handle, parts)
}
pub async fn request<B>(
&self,
req: http::Request<B>,
) -> Result<http::Response<H3ResponseBody>, RequestError>
where
B: Body + Send + 'static,
B::Data: Send,
B::Error: Send + Into<Box<dyn std::error::Error + Send + Sync>>,
{
let conn = self.get_connection().await?;
let (parts, body) = req.into_parts();
let (response, writer) = stream::initiate_request(&conn.handle, parts)?;
tokio::spawn(async move {
if let Err(e) = pump_request_body(body, writer).await {
tracing::debug!(?e, "request body upload failed")
}
});
response.await
}
pub async fn connect(&self) -> Result<ConnectionToken, EstablishError> {
let conn = self.get_connection().await?;
Ok(ConnectionToken::new(conn.lifetime))
}
pub async fn close(&self) {
self.closed.cancel();
if let Some(conn) = self.current.lock().await.take() {
conn.lifetime.close(CloseReason::Local);
close_connection(&conn.handle);
}
}
pub fn is_closed(&self) -> bool {
self.closed.is_cancelled()
}
async fn get_connection(&self) -> Result<Connection, EstablishError> {
if self.is_closed() {
return Err(EstablishError::Closed);
}
let mut guard = self.current.lock().await;
if self.is_closed() {
return Err(EstablishError::Closed);
}
if let Some(conn) = guard.as_ref()
&& conn.is_usable()
{
return Ok(conn.clone());
}
let quiche_config = self
.config
.to_quiche_config()
.map_err(EstablishError::Quic)?;
let lifetime = ConnLifetime::new();
let connect = connect(
self.remote,
self.socket.clone(),
self.server_name.clone(),
quiche_config,
self.config.handshake_timeout,
self.closed.clone(),
lifetime.clone(),
);
let handle = tokio::select! {
res = connect => res?,
_ = self.closed.cancelled() => return Err(EstablishError::Closed),
};
if self.is_closed() {
return Err(EstablishError::Closed);
}
let conn = Connection { handle, lifetime };
*guard = Some(conn.clone());
Ok(conn)
}
#[doc(hidden)]
pub async fn tracked_stream_state(&self) -> usize {
let guard = self.current.lock().await;
let Some(conn) = guard.as_ref() else {
return 0;
};
let conn = conn.handle.lock();
conn.app.streams.len() + conn.app.response_heads.len()
}
}
async fn pump_request_body<B>(body: B, mut writer: RequestBodyWriter) -> Result<(), UploadError>
where
B: Body,
B::Error: Send + Into<Box<dyn std::error::Error + Send + Sync>>,
{
let mut body = std::pin::pin!(body);
let mut trailers: Option<http::HeaderMap> = None;
while let Some(frame) = std::future::poll_fn(|cx| body.as_mut().poll_frame(cx)).await {
let frame = frame.map_err(|e| UploadError::Body(e.into()))?;
match frame.into_data() {
Ok(mut data) => {
let chunk = data.copy_to_bytes(data.remaining());
writer.write_chunk(chunk).await.map_err(UploadError::Send)?;
}
Err(frame) => {
if let Ok(t) = frame.into_trailers() {
trailers = Some(t);
}
}
}
}
match trailers {
Some(trailers) => {
writer
.write_trailers(trailers)
.await
.map_err(UploadError::Send)?
}
None => writer.finish().await.map_err(UploadError::Send)?,
}
Ok(())
}