use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncReadExt, BufReader};
use super::{FetchError, MAX_HEADER_BYTES, Target, connect_and_send, parse_status_line};
pub const MAX_POST_BODY_BYTES: usize = 8 * 1024;
#[derive(Debug, Default)]
pub struct PostReply {
pub body: String,
}
pub async fn post_json(
target: &Target,
headers: &[(String, String)],
body: &[u8],
timeout: Duration,
) -> Result<PostReply, FetchError> {
match tokio::time::timeout(timeout, post_inner(target, headers, body)).await {
Ok(result) => result,
Err(_elapsed) => Err(FetchError::Timeout),
}
}
async fn post_inner(
target: &Target,
headers: &[(String, String)],
body: &[u8],
) -> Result<PostReply, FetchError> {
let stream = connect_and_send(target, &build_post_request(target, headers, body)).await?;
read_post_reply(stream).await
}
fn build_post_request(target: &Target, headers: &[(String, String)], body: &[u8]) -> Vec<u8> {
let default_port = Target::default_port(target.https);
let host = if target.port == default_port {
target.host.clone()
} else {
format!("{}:{}", target.host, target.port)
};
let mut head = format!(
"POST {} HTTP/1.1\r\nHost: {host}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n",
target.path,
body.len(),
);
for (name, value) in headers {
head.push_str(name);
head.push_str(": ");
head.push_str(value);
head.push_str("\r\n");
}
head.push_str("\r\n");
let mut request = head.into_bytes();
request.extend_from_slice(body);
request
}
async fn read_post_reply<S: AsyncRead + Unpin>(stream: S) -> Result<PostReply, FetchError> {
let mut head = BufReader::new(stream).take(MAX_HEADER_BYTES as u64);
let mut status_line = String::new();
head.read_line(&mut status_line).await?;
let code = parse_status_line(&status_line)?;
let mut content_length: Option<u64> = None;
let mut content_length_ok = true;
let mut transfer_encoding = false;
loop {
let mut line = String::new();
let read = head.read_line(&mut line).await?;
if read == 0 {
if head.limit() == 0 {
return Err(FetchError::HeadersTooLarge {
limit: MAX_HEADER_BYTES,
});
}
return Err(FetchError::Transport(std::io::Error::other(
"response headers ended without a blank line",
)));
}
if line == "\r\n" || line == "\n" {
break;
}
let Some((name, value)) = line.split_once(':') else {
return Err(FetchError::Transport(std::io::Error::other(
"malformed header line: no colon",
)));
};
let value = value.trim();
match name.trim().to_ascii_lowercase().as_str() {
"transfer-encoding" => transfer_encoding = true,
"content-length" => match value.parse::<u64>() {
Ok(parsed) => match content_length {
Some(existing) if existing != parsed => content_length_ok = false,
Some(_) => {}
None => content_length = Some(parsed),
},
Err(_err) => content_length_ok = false,
},
_ => {}
}
}
if !(200..300).contains(&code) {
return Err(FetchError::Status(code));
}
if transfer_encoding {
return Err(FetchError::Chunked);
}
if !content_length_ok {
return Err(FetchError::Transport(std::io::Error::other(
"content-length header was not a number, or two content-length headers disagreed",
)));
}
let Some(content_length) = content_length else {
return Ok(PostReply::default());
};
if content_length > MAX_POST_BODY_BYTES as u64 {
return Err(FetchError::TooLarge {
limit: MAX_POST_BODY_BYTES,
});
}
let expected = content_length as usize;
let mut reader = head.into_inner();
let mut body = vec![0u8; expected];
let mut filled = 0;
while filled < expected {
let read = reader.read(&mut body[filled..]).await?;
if read == 0 {
return Err(FetchError::Truncated {
expected,
got: filled,
});
}
filled += read;
}
Ok(PostReply {
body: String::from_utf8_lossy(&body).into_owned(),
})
}