use std::io::Read;
use std::time::{Duration, Instant};
pub const MAX_HEAD_BYTES: usize = 8 * 1024;
#[derive(Debug, PartialEq, Eq)]
pub enum ReadOutcome {
Complete {
method: String,
path: String,
headers: Vec<(String, String)>,
body: Vec<u8>,
},
Empty,
Rejected {
status: u16,
code: &'static str,
},
}
pub fn read_request<R: Read>(
stream: &mut R,
max_body_bytes: usize,
deadline: Duration,
) -> ReadOutcome {
let started = Instant::now();
let mut buf: Vec<u8> = Vec::with_capacity(1024);
let head_end = match read_head(stream, &mut buf, started, deadline) {
Ok(pos) => pos,
Err(outcome) => return outcome,
};
let head = String::from_utf8_lossy(&buf[..head_end]).to_string();
let Some((method, path, headers)) = parse_head(&head) else {
return ReadOutcome::Rejected {
status: 400,
code: "malformed_request_line",
};
};
if headers.iter().any(|(k, _)| k == "transfer-encoding") {
return ReadOutcome::Rejected {
status: 501,
code: "transfer_encoding_unsupported",
};
}
let content_length = match parse_content_length(&headers) {
Ok(n) => n,
Err(code) => {
return ReadOutcome::Rejected { status: 400, code };
}
};
if content_length > max_body_bytes {
return ReadOutcome::Rejected {
status: 413,
code: "body_too_large",
};
}
let mut body: Vec<u8> = buf[head_end + 4..].to_vec();
if body.len() > content_length {
body.truncate(content_length);
}
if let Some(outcome) = read_body(stream, &mut body, content_length, started, deadline) {
return outcome;
}
ReadOutcome::Complete {
method,
path,
headers,
body,
}
}
fn read_head<R: Read>(
stream: &mut R,
buf: &mut Vec<u8>,
started: Instant,
deadline: Duration,
) -> Result<usize, ReadOutcome> {
let mut chunk = [0u8; 1024];
loop {
if let Some(pos) = find_head_end(buf) {
return Ok(pos);
}
if buf.len() > MAX_HEAD_BYTES {
return Err(ReadOutcome::Rejected {
status: 431,
code: "head_too_large",
});
}
if started.elapsed() >= deadline {
return Err(if buf.is_empty() {
ReadOutcome::Empty
} else {
ReadOutcome::Rejected {
status: 408,
code: "request_timeout",
}
});
}
if let Some(outcome) = read_head_chunk(stream, buf, &mut chunk, started, deadline) {
return Err(outcome);
}
}
}
fn read_head_chunk<R: Read>(
stream: &mut R,
buf: &mut Vec<u8>,
chunk: &mut [u8; 1024],
started: Instant,
deadline: Duration,
) -> Option<ReadOutcome> {
match stream.read(chunk) {
Ok(0) => Some(if buf.is_empty() {
ReadOutcome::Empty
} else {
ReadOutcome::Rejected {
status: 400,
code: "incomplete_head",
}
}),
Ok(n) => {
buf.extend_from_slice(&chunk[..n]);
None
}
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
if started.elapsed() >= deadline {
Some(ReadOutcome::Rejected {
status: 408,
code: "request_timeout",
})
} else {
None
}
}
Err(_) => Some(if buf.is_empty() {
ReadOutcome::Empty
} else {
ReadOutcome::Rejected {
status: 400,
code: "read_error",
}
}),
}
}
fn read_body<R: Read>(
stream: &mut R,
body: &mut Vec<u8>,
content_length: usize,
started: Instant,
deadline: Duration,
) -> Option<ReadOutcome> {
let mut chunk = [0u8; 1024];
while body.len() < content_length {
if started.elapsed() >= deadline {
return Some(ReadOutcome::Rejected {
status: 408,
code: "request_timeout",
});
}
match stream.read(&mut chunk) {
Ok(0) => {
return Some(ReadOutcome::Rejected {
status: 400,
code: "incomplete_body",
});
}
Ok(n) => {
let want = content_length - body.len();
body.extend_from_slice(&chunk[..n.min(want)]);
}
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {}
Err(_) => {
return Some(ReadOutcome::Rejected {
status: 400,
code: "read_error",
});
}
}
}
None
}
type ParsedHead = (String, String, Vec<(String, String)>);
fn find_head_end(buf: &[u8]) -> Option<usize> {
buf.windows(4).position(|w| w == b"\r\n\r\n")
}
fn parse_head(head: &str) -> Option<ParsedHead> {
let mut lines = head.lines();
let request_line = lines.next()?;
let mut parts = request_line.split_whitespace();
let method = parts.next()?.to_string();
let path = parts.next()?.to_string();
let mut headers = Vec::new();
for line in lines {
let line = line.trim_end();
if line.is_empty() {
break;
}
if let Some((k, v)) = line.split_once(':') {
headers.push((k.trim().to_lowercase(), v.trim().to_string()));
}
}
Some((method, path, headers))
}
fn parse_content_length(headers: &[(String, String)]) -> Result<usize, &'static str> {
let mut found: Option<usize> = None;
for (k, v) in headers {
if k != "content-length" {
continue;
}
if found.is_some() {
return Err("duplicate_content_length");
}
found = Some(v.parse::<usize>().map_err(|_| "invalid_content_length")?);
}
Ok(found.unwrap_or(0))
}
#[must_use]
pub fn response(status: u16, code: &str) -> Vec<u8> {
let body = serde_json::json!({ "status": code }).to_string();
let reason = status_reason(status);
let mut out = format!(
"HTTP/1.1 {status} {reason}\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
Connection: close\r\n",
body.len()
);
if status == 405 {
out.push_str("Allow: POST\r\n");
}
if status == 503 {
out.push_str("Retry-After: 5\r\n");
}
out.push_str("\r\n");
out.push_str(&body);
out.into_bytes()
}
#[must_use]
pub fn json_response(status: u16, body: &str) -> Vec<u8> {
let reason = status_reason(status);
let mut out = format!(
"HTTP/1.1 {status} {reason}\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
Connection: close\r\n",
body.len()
);
if status == 405 {
out.push_str("Allow: POST\r\n");
}
out.push_str("\r\n");
out.push_str(body);
out.into_bytes()
}
const STATUS_REASONS: &[(u16, &str)] = &[
(200, "OK"),
(400, "Bad Request"),
(401, "Unauthorized"),
(403, "Forbidden"),
(404, "Not Found"),
(405, "Method Not Allowed"),
(408, "Request Timeout"),
(413, "Content Too Large"),
(431, "Request Header Fields Too Large"),
(500, "Internal Server Error"),
(501, "Not Implemented"),
(503, "Service Unavailable"),
];
fn status_reason(code: u16) -> &'static str {
STATUS_REASONS
.iter()
.find(|(status, _)| *status == code)
.map_or("Unknown", |&(_, reason)| reason)
}