use crate::NetError;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HttpHead {
pub status: u16,
pub reason: String,
pub headers: Vec<(String, String)>,
}
impl HttpHead {
pub fn header(&self, name: &str) -> Option<&str> {
self.headers
.iter()
.find(|(key, _)| key.eq_ignore_ascii_case(name))
.map(|(_, value)| value.as_str())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HttpBodyMode {
ContentLength(usize),
Chunked,
UntilEof,
Empty,
}
pub fn build_http_request_head(
method: &str,
target: &str,
host: &str,
content_length: Option<usize>,
headers: &[(String, String)],
) -> Result<String, NetError> {
validate_token("method", method)?;
validate_field_value("target", target)?;
validate_field_value("host", host)?;
let mut head = format!("{method} {target} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n");
if let Some(length) = content_length {
head.push_str(&format!("Content-Length: {length}\r\n"));
}
for (name, value) in headers {
validate_token("header name", name)?;
validate_field_value("header value", value)?;
head.push_str(name);
head.push_str(": ");
head.push_str(value);
head.push_str("\r\n");
}
head.push_str("\r\n");
Ok(head)
}
pub fn parse_http_head(head_text: &str) -> Result<HttpHead, NetError> {
let body = head_text.trim_end_matches("\r\n\r\n");
let mut lines = body.split("\r\n");
let status_line = lines
.next()
.ok_or_else(|| NetError::InvalidHead("missing status line".to_owned()))?;
let mut parts = status_line.split_whitespace();
let _version = parts
.next()
.ok_or_else(|| NetError::InvalidHead("missing version".to_owned()))?;
let status = parts
.next()
.ok_or_else(|| NetError::InvalidHead("missing status".to_owned()))?
.parse::<u16>()
.map_err(|_| NetError::InvalidHead("invalid status code".to_owned()))?;
let reason = parts.collect::<Vec<_>>().join(" ");
let mut headers = Vec::new();
for line in lines {
if line.is_empty() {
continue;
}
let (key, value) = line
.split_once(':')
.ok_or_else(|| NetError::InvalidHead("invalid header line".to_owned()))?;
headers.push((key.to_owned(), value.trim().to_owned()));
}
Ok(HttpHead {
status,
reason,
headers,
})
}
fn validate_token(label: &str, value: &str) -> Result<(), NetError> {
if value.is_empty()
|| value
.bytes()
.any(|byte| byte <= b' ' || byte == b':' || byte >= 0x7f)
{
return Err(NetError::InvalidHead(format!("invalid {label}")));
}
Ok(())
}
fn validate_field_value(label: &str, value: &str) -> Result<(), NetError> {
if value.contains('\r') || value.contains('\n') {
return Err(NetError::InvalidHead(format!("invalid {label}")));
}
Ok(())
}
pub fn body_mode(head: &HttpHead) -> Result<HttpBodyMode, NetError> {
if let Some(encoding) = head.header("Transfer-Encoding")
&& encoding.eq_ignore_ascii_case("chunked")
{
return Ok(HttpBodyMode::Chunked);
}
match head.header("Content-Length") {
Some(raw) => {
let length = raw
.parse::<usize>()
.map_err(|_| NetError::InvalidHead("invalid content-length".to_owned()))?;
if length == 0 {
Ok(HttpBodyMode::UntilEof)
} else {
Ok(HttpBodyMode::ContentLength(length))
}
}
None => Ok(HttpBodyMode::UntilEof),
}
}