use std::io::{self, Read, Write};
use std::net::TcpStream;
#[derive(Debug)]
pub struct Request {
pub method: String,
pub path: String,
pub query: String,
pub headers: Vec<(String, String)>,
pub body: Vec<u8>,
}
impl Request {
#[must_use]
pub fn header(&self, name: &str) -> Option<&str> {
self.headers
.iter()
.find(|(named, _)| named.eq_ignore_ascii_case(name))
.map(|(_, value)| value.as_str())
}
#[must_use]
pub fn cookie(&self, name: &str) -> Option<&str> {
self.headers
.iter()
.filter(|(named, _)| named == "cookie")
.flat_map(|(_, value)| value.split(';'))
.filter_map(|pair| pair.trim().split_once('='))
.find(|(named, _)| *named == name)
.map(|(_, value)| value)
}
#[must_use]
pub fn parameter(&self, name: &str) -> Option<String> {
url::form_urlencoded::parse(self.query.as_bytes())
.find(|(named, _)| named == name)
.map(|(_, value)| value.into_owned())
}
}
fn refused(words: &'static str) -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, words)
}
pub fn read(input: &mut impl Read, head_cap: usize, body_cap: usize) -> io::Result<Request> {
let mut head = Vec::new();
let mut byte = [0_u8; 1];
while !head.ends_with(b"\r\n\r\n") {
if head.len() >= head_cap {
return Err(refused("the request head is past its bound"));
}
input.read_exact(&mut byte)?;
head.push(byte[0]);
}
let text = std::str::from_utf8(&head).map_err(|_| refused("the request head is not UTF-8"))?;
let mut lines = text.split("\r\n");
let start = lines
.next()
.ok_or_else(|| refused("the request has no line"))?;
let mut parts = start.split(' ');
let (Some(method), Some(target), Some("HTTP/1.1" | "HTTP/1.0"), None) =
(parts.next(), parts.next(), parts.next(), parts.next())
else {
return Err(refused("the request line is malformed"));
};
let (path, query) = target.split_once('?').unwrap_or((target, ""));
let mut headers = Vec::new();
for line in lines.filter(|line| !line.is_empty()) {
let (name, value) = line
.split_once(':')
.ok_or_else(|| refused("a header is malformed"))?;
headers.push((name.trim().to_ascii_lowercase(), value.trim().to_owned()));
}
let mut request = Request {
method: method.to_owned(),
path: path.to_owned(),
query: query.to_owned(),
headers,
body: Vec::new(),
};
if request.header("transfer-encoding").is_some() {
return Err(refused("a chunked body is not read; send Content-Length"));
}
let length = match request.header("content-length") {
Some(length) => length
.parse::<usize>()
.map_err(|_| refused("Content-Length is not a number"))?,
None => 0,
};
if length > body_cap {
return Err(refused("the request body is past its bound"));
}
request.body = vec![0; length];
input.read_exact(&mut request.body)?;
Ok(request)
}
type Streamed = Box<dyn FnOnce(&mut dyn Write, Option<&TcpStream>) -> io::Result<()> + Send>;
enum Body {
Whole(Vec<u8>),
Stream(Streamed),
}
impl std::fmt::Debug for Body {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Whole(bytes) => write!(formatter, "Whole({} bytes)", bytes.len()),
Self::Stream(_) => formatter.write_str("Stream"),
}
}
}
#[derive(Debug)]
pub struct Response {
status: u16,
reason: &'static str,
headers: Vec<(&'static str, String)>,
cache: &'static str,
body: Body,
}
impl Response {
#[must_use]
pub fn new(status: u16, content_type: &'static str, body: Vec<u8>) -> Self {
let reason = match status {
200 => "OK",
204 => "No Content",
303 => "See Other",
400 => "Bad Request",
401 => "Unauthorized",
403 => "Forbidden",
404 => "Not Found",
405 => "Method Not Allowed",
502 => "Bad Gateway",
503 => "Service Unavailable",
_ => "Status",
};
Self {
status,
reason,
headers: vec![("Content-Type", content_type.to_owned())],
cache: "no-store",
body: Body::Whole(body),
}
}
#[must_use]
pub fn stream(
content_type: &'static str,
body: impl FnOnce(&mut dyn Write) -> io::Result<()> + Send + 'static,
) -> Self {
Self::stream_with_peer(content_type, move |output, _peer| body(output))
}
pub(crate) fn stream_with_peer(
content_type: &'static str,
body: impl FnOnce(&mut dyn Write, Option<&TcpStream>) -> io::Result<()> + Send + 'static,
) -> Self {
let mut response = Self::new(200, content_type, Vec::new());
response.body = Body::Stream(Box::new(body));
response
}
#[must_use]
pub const fn cached(mut self, policy: &'static str) -> Self {
self.cache = policy;
self
}
#[must_use]
pub fn json(status: u16, value: &serde_json::Value) -> Self {
Self::new(status, "application/json", value.to_string().into_bytes())
}
#[must_use]
pub fn no_content() -> Self {
let mut response = Self::new(204, "text/plain; charset=utf-8", Vec::new());
response.headers.clear();
response
}
#[must_use]
pub fn redirect(location: &str) -> Self {
Self::new(303, "text/plain; charset=utf-8", Vec::new()).with("Location", location)
}
#[must_use]
pub fn with(mut self, name: &'static str, value: &str) -> Self {
self.headers.push((name, value.to_owned()));
self
}
pub fn write(self, output: &mut impl Write) -> io::Result<()> {
self.write_with_peer(output, None)
}
pub(super) fn write_to(self, stream: &TcpStream) -> io::Result<()> {
self.write_with_peer(&mut &*stream, Some(stream))
}
fn write_with_peer(self, output: &mut impl Write, peer: Option<&TcpStream>) -> io::Result<()> {
if self
.headers
.iter()
.any(|(_, value)| value.contains(['\r', '\n']))
{
return Err(refused("a header value holds a line break"));
}
let mut head = format!("HTTP/1.1 {} {}\r\n", self.status, self.reason);
for (name, value) in &self.headers {
head.push_str(name);
head.push_str(": ");
head.push_str(value);
head.push_str("\r\n");
}
if let Body::Whole(body) = &self.body
&& self.status != 204
{
head.push_str("Content-Length: ");
head.push_str(&body.len().to_string());
head.push_str("\r\n");
}
head.push_str("Cache-Control: ");
head.push_str(self.cache);
head.push_str("\r\nConnection: close\r\n\r\n");
let mut bytes = head.into_bytes();
match self.body {
Body::Whole(body) => {
bytes.extend_from_slice(&body);
output.write_all(&bytes)?;
output.flush()
}
Body::Stream(body) => {
output.write_all(&bytes)?;
output.flush()?;
body(&mut *output, peer)?;
output.flush()
}
}
}
}
#[cfg(test)]
#[path = "http_tests.rs"]
mod tests;