use tokio::io::AsyncWriteExt;
use super::{Client, PodmanError, Result};
const MAX_HEAD_BYTES: usize = 16 * 1024;
#[derive(Debug)]
pub(crate) struct Hijacked {
pub(crate) stream: tokio::net::UnixStream,
}
impl Client {
pub(crate) async fn post_hijack(&self, path: &str, body: &[u8]) -> Result<Hijacked> {
let mut stream = tokio::net::UnixStream::connect(&self.socket_path).await?;
let head = format!(
"POST {path} HTTP/1.1\r\n\
Host: localhost\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
Connection: close\r\n\
\r\n",
body.len()
);
stream.write_all(head.as_bytes()).await?;
stream.write_all(body).await?;
stream.flush().await?;
let status = read_response_head(&mut stream).await?;
if !(200..300).contains(&status) {
return Err(PodmanError::Api {
status,
message: format!("exec start refused with HTTP {status}"),
});
}
Ok(Hijacked { stream })
}
}
async fn read_response_head(stream: &mut tokio::net::UnixStream) -> Result<u16> {
use tokio::io::AsyncReadExt;
let mut head = Vec::with_capacity(256);
let mut byte = [0u8; 1];
while !head.ends_with(b"\r\n\r\n") {
if head.len() >= MAX_HEAD_BYTES {
return Err(PodmanError::Api {
status: 0,
message: "exec start response head exceeded its limit".to_string(),
});
}
let n = stream.read(&mut byte).await?;
if n == 0 {
return Err(PodmanError::Api {
status: 0,
message: "connection closed before the exec start response".to_string(),
});
}
head.push(byte[0]);
}
let text = String::from_utf8_lossy(&head);
let status_line = text.lines().next().unwrap_or_default();
status_line
.split_whitespace()
.nth(1)
.and_then(|c| c.parse::<u16>().ok())
.ok_or_else(|| PodmanError::Api {
status: 0,
message: format!("unparseable exec start response: {status_line:?}"),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn a_non_2xx_head_is_an_error_not_a_stream() {
let dir = tempfile::tempdir().unwrap();
let sock = dir.path().join("s.sock");
let listener = tokio::net::UnixListener::bind(&sock).unwrap();
tokio::spawn(async move {
if let Ok((mut c, _)) = listener.accept().await {
use tokio::io::AsyncWriteExt;
let _ = c
.write_all(b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n")
.await;
}
});
let client = Client::new(sock.to_string_lossy().to_string());
let err = client
.post_hijack("/exec/abc/start", b"{}")
.await
.expect_err("a 404 must not yield a stream");
assert!(err.is_status(404), "got {err:?}");
}
#[tokio::test]
async fn a_closed_connection_is_an_error() {
let dir = tempfile::tempdir().unwrap();
let sock = dir.path().join("s.sock");
let listener = tokio::net::UnixListener::bind(&sock).unwrap();
tokio::spawn(async move {
let _ = listener.accept().await;
});
let client = Client::new(sock.to_string_lossy().to_string());
assert!(client.post_hijack("/exec/abc/start", b"{}").await.is_err());
}
}