use std::io::{BufRead, Write};
use super::Respond;
pub(super) struct HttpRequest {
pub method: String,
pub path: String,
headers: Vec<(String, String)>,
pub body: Vec<u8>,
}
impl HttpRequest {
pub fn header(&self, name: &str) -> Option<&str> {
self.headers
.iter()
.find(|(n, _)| n == name)
.map(|(_, v)| v.as_str())
}
pub fn wants_close(&self) -> bool {
self.header("connection")
.is_some_and(|v| v.eq_ignore_ascii_case("close"))
}
}
pub(super) fn read_request(r: &mut dyn BufRead) -> Result<Option<HttpRequest>, String> {
let mut line = String::new();
if read_line(r, &mut line)? == 0 {
return Ok(None);
}
let mut parts = line.split_whitespace();
let (method, path, version) = (parts.next(), parts.next(), parts.next());
let (Some(method), Some(path), Some(version)) = (method, path, version) else {
return Err(format!("bad request line `{}`", line.trim_end()));
};
if !version.starts_with("HTTP/1") {
return Err(format!("unsupported protocol version `{version}`"));
}
let (method, path) = (method.to_owned(), path.to_owned());
let mut headers = Vec::new();
loop {
line.clear();
if read_line(r, &mut line)? == 0 {
return Err("connection closed inside the header block".into());
}
let trimmed = line.trim_end();
if trimmed.is_empty() {
break;
}
let Some((name, value)) = trimmed.split_once(':') else {
return Err(format!("bad header line `{trimmed}`"));
};
headers.push((name.trim().to_ascii_lowercase(), value.trim().to_owned()));
}
let req = HttpRequest {
method,
path,
headers,
body: Vec::new(),
};
let length: usize = match req.header("content-length") {
None => 0,
Some(v) => v.parse().map_err(|_| format!("bad content-length `{v}`"))?,
};
let mut body = vec![0; length];
r.read_exact(&mut body)
.map_err(|e| format!("body shorter than content-length: {e}"))?;
Ok(Some(HttpRequest { body, ..req }))
}
fn read_line(r: &mut dyn BufRead, buf: &mut String) -> Result<usize, String> {
r.read_line(buf).map_err(|e| format!("read failed: {e}"))
}
pub(super) struct HttpRespond<'a> {
w: &'a mut dyn Write,
sse: bool,
status: u16,
body: Vec<u8>,
dead: bool,
}
impl<'a> HttpRespond<'a> {
pub fn new(w: &'a mut dyn Write) -> Self {
HttpRespond {
w,
sse: false,
status: 200,
body: Vec::new(),
dead: false,
}
}
pub fn dead(&self) -> bool {
self.dead
}
fn track(&mut self, out: std::io::Result<()>) -> std::io::Result<()> {
self.dead |= out.is_err();
out
}
}
impl Respond for HttpRespond<'_> {
fn begin(&mut self, status: u16, sse: bool) -> std::io::Result<()> {
self.status = status;
self.sse = sse;
if sse {
let out = write!(
self.w,
"HTTP/1.1 {status} {}\r\ncontent-type: text/event-stream\r\ncache-control: no-cache\r\ntransfer-encoding: chunked\r\n\r\n",
reason(status)
);
self.track(out)?;
}
Ok(())
}
fn chunk(&mut self, bytes: &[u8]) -> std::io::Result<()> {
if self.sse {
let out = write!(self.w, "{:x}\r\n", bytes.len())
.and_then(|()| self.w.write_all(bytes))
.and_then(|()| self.w.write_all(b"\r\n"))
.and_then(|()| self.w.flush());
self.track(out)
} else {
self.body.extend_from_slice(bytes);
Ok(())
}
}
fn end(&mut self) -> std::io::Result<()> {
if self.sse {
let out = self.w.write_all(b"0\r\n\r\n").and_then(|()| self.w.flush());
return self.track(out);
}
let (status, len) = (self.status, self.body.len());
let body = std::mem::take(&mut self.body);
let out = write!(
self.w,
"HTTP/1.1 {status} {}\r\ncontent-type: application/json\r\ncontent-length: {len}\r\n\r\n",
reason(status)
)
.and_then(|()| self.w.write_all(&body))
.and_then(|()| self.w.flush());
self.track(out)
}
}
fn reason(status: u16) -> &'static str {
match status {
200 => "OK",
400 => "Bad Request",
401 => "Unauthorized",
404 => "Not Found",
429 => "Too Many Requests",
500 => "Internal Server Error",
502 => "Bad Gateway",
_ => "",
}
}