use crate::request::body::Send;
use std::pin::Pin;
pub async fn send<'socket, Socket: futures_io::AsyncWrite + ?Sized>(
method: &str,
request_target: &str,
headers: &[crate::Header<'_>],
mut socket: Pin<&'socket mut Socket>,
) -> std::io::Result<Send<'socket, Socket>> {
use crate::util::io::AsyncWriteExt as _;
use crate::util::is_token;
debug_assert!(is_token(method), "Request method {method} is not a token");
debug_assert!(
method != "CONNECT",
"Request method CONNECT is not supported"
);
debug_assert!(
crate::util::is_request_target(request_target),
"Request target contains invalid characters"
);
for header in headers {
debug_assert!(
is_token(header.name),
"Request header {} is not a token",
header.name
);
debug_assert!(
crate::util::is_field_value(header.value),
"Request header value {:?} is not a valid field value",
header.value
);
debug_assert!(
!header.name.eq_ignore_ascii_case("TE"),
"Request header TE is not supported"
);
if header.name.eq_ignore_ascii_case("Transfer-Encoding") {
debug_assert!(
header.value == b"chunked",
"Request Transfer-Encoding is {:?}, but only chunked is supported",
header.value
);
} else if header.name.eq_ignore_ascii_case("Content-Length") {
debug_assert!(
std::str::from_utf8(header.value)
.ok()
.and_then(|v| v.parse::<u64>().ok())
.is_some(),
"Request Content-Length {:?} is not a non-negative integer",
header.value
);
}
}
debug_assert!(
headers
.iter()
.filter(|h| h.name.eq_ignore_ascii_case("content-length")
|| h.name.eq_ignore_ascii_case("transfer-encoding"))
.count() <= 1,
"Request must contain at most one of Content-Length and Transfer-Encoding"
);
socket.as_mut().write_all(method.as_bytes()).await?;
socket.as_mut().write_all(b" ").await?;
socket.as_mut().write_all(request_target.as_bytes()).await?;
socket.as_mut().write_all(b" HTTP/1.1\r\n").await?;
for &header in headers {
socket.as_mut().write_all(header.name.as_bytes()).await?;
socket.as_mut().write_all(b":").await?;
socket.as_mut().write_all(header.value).await?;
socket.as_mut().write_all(b"\r\n").await?;
}
socket.as_mut().write_all(b"\r\n").await?;
let metadata = super::Metadata {
head: method == "HEAD",
connection_close: crate::util::is_connection_close(headers),
};
if headers
.iter()
.any(|h| h.name.eq_ignore_ascii_case("Transfer-Encoding"))
{
Ok(Send::new_chunked(socket, metadata))
} else {
let h = headers
.iter()
.find(|h| h.name.eq_ignore_ascii_case("Content-Length"));
let length = h
.and_then(|h| std::str::from_utf8(h.value).ok())
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(0);
Ok(Send::new_fixed(socket, metadata, length))
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::Header;
use futures_executor::block_on;
#[test]
fn basic() {
let headers = [
Header {
name: "User-Agent",
value: b"Thingy/1.0 OtherThing/2.0",
},
Header {
name: "Host",
value: b"someplace.example.com",
},
];
let mut sink: Vec<u8> = Vec::new();
let _ = block_on(send("POST", "/abcd/efgh", &headers, Pin::new(&mut sink))).unwrap();
assert_eq!(sink, &b"POST /abcd/efgh HTTP/1.1\r\nUser-Agent:Thingy/1.0 OtherThing/2.0\r\nHost:someplace.example.com\r\n\r\n"[..]);
}
#[test]
fn content_length() {
let headers = [
Header {
name: "User-Agent",
value: b"Thingy/1.0 OtherThing/2.0",
},
Header {
name: "Content-Length",
value: b"1234",
},
];
let mut sink: Vec<u8> = Vec::new();
let _ = block_on(send("POST", "/abcd/efgh", &headers, Pin::new(&mut sink))).unwrap();
assert_eq!(sink, &b"POST /abcd/efgh HTTP/1.1\r\nUser-Agent:Thingy/1.0 OtherThing/2.0\r\nContent-Length:1234\r\n\r\n"[..]);
}
#[test]
fn transfer_encoding() {
let headers = [
Header {
name: "User-Agent",
value: b"Thingy/1.0 OtherThing/2.0",
},
Header {
name: "Transfer-Encoding",
value: b"chunked",
},
];
let mut sink: Vec<u8> = Vec::new();
let _ = block_on(send("POST", "/abcd/efgh", &headers, Pin::new(&mut sink))).unwrap();
assert_eq!(sink, &b"POST /abcd/efgh HTTP/1.1\r\nUser-Agent:Thingy/1.0 OtherThing/2.0\r\nTransfer-Encoding:chunked\r\n\r\n"[..]);
}
}