use std::{fmt, time::Duration};
use bytes::Bytes;
use http_body_util::{BodyExt, Full, LengthLimitError, Limited};
use hyper::{Request, client::conn::http1};
use hyper_util::rt::TokioIo;
use tokio::{net::UnixStream, time::timeout};
use crate::{
Diagnostic, DiagnosticCode, LibpodHeader, LibpodHeaders, LibpodMethod, LibpodRequest, LibpodResponse,
LibpodTransport, LibpodTransportFuture, PodmanLensResult, TransportError, TransportLimits, UnixConnection,
};
pub const MIN_HTTP1_HEADER_BYTES: usize = 8_192;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ReadOnlyUnixTransportTimeouts {
connect: Duration,
headers: Duration,
body: Duration,
total: Duration,
}
impl ReadOnlyUnixTransportTimeouts {
pub fn new(connect: Duration, headers: Duration, body: Duration, total: Duration) -> PodmanLensResult<Self> {
if connect.is_zero() || headers.is_zero() || body.is_zero() || total.is_zero() {
return Err(Diagnostic::new(DiagnosticCode::InvalidTransportMessage));
}
Ok(Self {
connect,
headers,
body,
total,
})
}
#[must_use]
pub const fn connect(self) -> Duration {
self.connect
}
#[must_use]
pub const fn headers(self) -> Duration {
self.headers
}
#[must_use]
pub const fn body(self) -> Duration {
self.body
}
#[must_use]
pub const fn total(self) -> Duration {
self.total
}
}
impl Default for ReadOnlyUnixTransportTimeouts {
fn default() -> Self {
Self {
connect: Duration::from_secs(5),
headers: Duration::from_secs(10),
body: Duration::from_secs(30),
total: Duration::from_secs(45),
}
}
}
#[derive(Clone)]
pub struct ReadOnlyUnixTransport {
connection: UnixConnection,
limits: TransportLimits,
timeouts: ReadOnlyUnixTransportTimeouts,
}
impl ReadOnlyUnixTransport {
pub fn new(
connection: UnixConnection,
limits: TransportLimits,
timeouts: ReadOnlyUnixTransportTimeouts,
) -> PodmanLensResult<Self> {
if limits.max_header_bytes() < MIN_HTTP1_HEADER_BYTES {
return Err(Diagnostic::new(DiagnosticCode::InvalidTransportMessage));
}
Ok(Self {
connection,
limits,
timeouts,
})
}
#[must_use]
pub const fn connection(&self) -> &UnixConnection {
&self.connection
}
#[must_use]
pub const fn limits(&self) -> TransportLimits {
self.limits
}
#[must_use]
pub const fn timeouts(&self) -> ReadOnlyUnixTransportTimeouts {
self.timeouts
}
async fn send_get(&self, request: &LibpodRequest) -> Result<LibpodResponse, TransportError> {
validate_acquisition_request(request, self.limits)?;
let request = build_http_request(request)?;
let stream = timeout(
self.timeouts.connect(),
UnixStream::connect(self.connection.socket_path()),
)
.await
.map_err(|_| TransportError::unavailable())?
.map_err(|_| TransportError::unavailable())?;
let mut builder = http1::Builder::new();
builder.max_headers(self.limits.max_header_count());
builder.max_buf_size(self.limits.max_header_bytes());
let (mut sender, connection) = builder
.handshake(TokioIo::new(stream))
.await
.map_err(|_| TransportError::unavailable())?;
let mut connection = Box::pin(connection);
let response_future = sender.send_request(request);
tokio::pin!(response_future);
let response = timeout(self.timeouts.headers(), async {
let response = tokio::select! {
biased;
response = &mut response_future => response,
result = &mut connection => {
let _connection_result = result;
response_future.await
}
};
response.map_err(|error| classify_hyper_error(&error))
})
.await
.map_err(|_| TransportError::unavailable())??;
let status = response.status().as_u16();
let headers = convert_headers(response.headers())?;
let body = Limited::new(response.into_body(), self.limits.max_body_bytes());
let body_future = body.collect();
tokio::pin!(body_future);
let collected = timeout(self.timeouts.body(), async {
let body = tokio::select! {
biased;
body = &mut body_future => body,
result = &mut connection => {
let _connection_result = result;
body_future.await
}
};
body.map_err(|error| classify_body_error(error.as_ref()))
})
.await
.map_err(|_| TransportError::unavailable())??;
LibpodResponse::with_limits(self.limits, status, headers, collected.to_bytes().to_vec())
.map_err(|_| TransportError::invalid_message())
}
}
impl LibpodTransport for ReadOnlyUnixTransport {
fn send<'a>(&'a self, request: &'a LibpodRequest) -> LibpodTransportFuture<'a> {
if let Err(error) = validate_acquisition_request(request, self.limits) {
return Box::pin(async move { Err(error) });
}
Box::pin(async move {
timeout(self.timeouts.total(), self.send_get(request))
.await
.map_err(|_| TransportError::unavailable())?
})
}
}
fn validate_acquisition_request(request: &LibpodRequest, limits: TransportLimits) -> Result<(), TransportError> {
if request.method() != LibpodMethod::Get {
return Err(TransportError::read_only_rejected());
}
if !request.body().is_empty() || request.headers().len() > limits.max_header_count() {
return Err(TransportError::invalid_message());
}
let header_bytes = request.headers().iter().try_fold(0_usize, |total, header| {
total.checked_add(header.name().len() + header.value().len())
});
if header_bytes.is_none_or(|total| total > limits.max_header_bytes()) {
return Err(TransportError::invalid_message());
}
Ok(())
}
impl fmt::Debug for ReadOnlyUnixTransport {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("ReadOnlyUnixTransport([redacted])")
}
}
fn build_http_request(request: &LibpodRequest) -> Result<Request<Full<Bytes>>, TransportError> {
let mut builder = Request::builder()
.method(request.method().as_str())
.uri(request.path().as_str());
let headers = builder.headers_mut().ok_or_else(TransportError::unavailable)?;
for header in request.headers().iter() {
if header.name().eq_ignore_ascii_case("host") {
return Err(TransportError::invalid_message());
}
let name = hyper::http::header::HeaderName::from_bytes(header.name().as_bytes())
.map_err(|_| TransportError::invalid_message())?;
let value =
hyper::http::HeaderValue::from_str(header.value()).map_err(|_| TransportError::invalid_message())?;
headers.append(name, value);
}
headers.insert(
hyper::http::header::HOST,
hyper::http::HeaderValue::from_static("localhost"),
);
builder
.body(Full::new(Bytes::copy_from_slice(request.body())))
.map_err(|_| TransportError::invalid_message())
}
fn convert_headers(headers: &hyper::http::HeaderMap) -> Result<LibpodHeaders, TransportError> {
let mut converted = Vec::with_capacity(headers.len());
for (name, value) in headers {
let value = value.to_str().map_err(|_| TransportError::invalid_message())?;
converted.push(LibpodHeader::new(name.as_str(), value).map_err(|_| TransportError::invalid_message())?);
}
Ok(LibpodHeaders::new(converted))
}
fn classify_hyper_error(error: &hyper::Error) -> TransportError {
if error.is_parse() {
TransportError::invalid_message()
} else {
TransportError::unavailable()
}
}
fn classify_body_hyper_error(_error: &hyper::Error) -> TransportError {
TransportError::invalid_message()
}
fn classify_body_error(error: &(dyn std::error::Error + Send + Sync + 'static)) -> TransportError {
if error.downcast_ref::<LengthLimitError>().is_some() {
return TransportError::invalid_message();
}
error
.downcast_ref::<hyper::Error>()
.map_or_else(TransportError::unavailable, classify_body_hyper_error)
}