Skip to main content

haematite_cli/door/
http.rs

1//! The door's HTTP/1.1, bounded (ACCESS-002 R2).
2//!
3//! One request per connection, a head of at most `head_cap` bytes, a body
4//! only by Content-Length and at most `body_cap`. A chunked body is refused. A
5//! response is written whole, or streamed until it ends with the connection's
6//! close (an event stream); it is never cached unless it says so, and a
7//! header value that would split it is refused before a byte is written.
8
9use std::io::{self, Read, Write};
10use std::net::TcpStream;
11
12/// One request as the door reads it.
13#[derive(Debug)]
14pub struct Request {
15    /// The method, as sent.
16    pub method: String,
17    /// The path, without its query.
18    pub path: String,
19    /// The query, without its `?`.
20    pub query: String,
21    /// Each header as sent, its name lowercased.
22    pub headers: Vec<(String, String)>,
23    /// The body, exactly Content-Length bytes.
24    pub body: Vec<u8>,
25}
26
27impl Request {
28    /// The first header named `name`, case-insensitively.
29    #[must_use]
30    pub fn header(&self, name: &str) -> Option<&str> {
31        self.headers
32            .iter()
33            .find(|(named, _)| named.eq_ignore_ascii_case(name))
34            .map(|(_, value)| value.as_str())
35    }
36
37    /// The cookie named `name`.
38    #[must_use]
39    pub fn cookie(&self, name: &str) -> Option<&str> {
40        self.headers
41            .iter()
42            .filter(|(named, _)| named == "cookie")
43            .flat_map(|(_, value)| value.split(';'))
44            .filter_map(|pair| pair.trim().split_once('='))
45            .find(|(named, _)| *named == name)
46            .map(|(_, value)| value)
47    }
48
49    /// The query parameter named `name`, percent-decoded.
50    #[must_use]
51    pub fn parameter(&self, name: &str) -> Option<String> {
52        url::form_urlencoded::parse(self.query.as_bytes())
53            .find(|(named, _)| named == name)
54            .map(|(_, value)| value.into_owned())
55    }
56}
57
58fn refused(words: &'static str) -> io::Error {
59    io::Error::new(io::ErrorKind::InvalidData, words)
60}
61
62/// Read one request from `input`.
63///
64/// # Errors
65///
66/// `InvalidData` for a head past `head_cap`, a malformed head, a chunked
67/// body or a Content-Length past `body_cap`; the read's own error, or
68/// `UnexpectedEof` for a body cut short.
69pub fn read(input: &mut impl Read, head_cap: usize, body_cap: usize) -> io::Result<Request> {
70    let mut head = Vec::new();
71    let mut byte = [0_u8; 1];
72    while !head.ends_with(b"\r\n\r\n") {
73        if head.len() >= head_cap {
74            return Err(refused("the request head is past its bound"));
75        }
76        input.read_exact(&mut byte)?;
77        head.push(byte[0]);
78    }
79    let text = std::str::from_utf8(&head).map_err(|_| refused("the request head is not UTF-8"))?;
80    let mut lines = text.split("\r\n");
81    let start = lines
82        .next()
83        .ok_or_else(|| refused("the request has no line"))?;
84    let mut parts = start.split(' ');
85    let (Some(method), Some(target), Some("HTTP/1.1" | "HTTP/1.0"), None) =
86        (parts.next(), parts.next(), parts.next(), parts.next())
87    else {
88        return Err(refused("the request line is malformed"));
89    };
90    let (path, query) = target.split_once('?').unwrap_or((target, ""));
91    let mut headers = Vec::new();
92    for line in lines.filter(|line| !line.is_empty()) {
93        let (name, value) = line
94            .split_once(':')
95            .ok_or_else(|| refused("a header is malformed"))?;
96        headers.push((name.trim().to_ascii_lowercase(), value.trim().to_owned()));
97    }
98    let mut request = Request {
99        method: method.to_owned(),
100        path: path.to_owned(),
101        query: query.to_owned(),
102        headers,
103        body: Vec::new(),
104    };
105    if request.header("transfer-encoding").is_some() {
106        return Err(refused("a chunked body is not read; send Content-Length"));
107    }
108    let length = match request.header("content-length") {
109        Some(length) => length
110            .parse::<usize>()
111            .map_err(|_| refused("Content-Length is not a number"))?,
112        None => 0,
113    };
114    if length > body_cap {
115        return Err(refused("the request body is past its bound"));
116    }
117    request.body = vec![0; length];
118    input.read_exact(&mut request.body)?;
119    Ok(request)
120}
121
122/// A streamed body: written after the head until it returns.
123type Streamed = Box<dyn FnOnce(&mut dyn Write, Option<&TcpStream>) -> io::Result<()> + Send>;
124
125/// What follows a response's head.
126enum Body {
127    /// Every byte, counted by Content-Length.
128    Whole(Vec<u8>),
129    /// Written as it is made, with no Content-Length: the connection's close
130    /// ends it.
131    Stream(Streamed),
132}
133
134impl std::fmt::Debug for Body {
135    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136        match self {
137            Self::Whole(bytes) => write!(formatter, "Whole({} bytes)", bytes.len()),
138            Self::Stream(_) => formatter.write_str("Stream"),
139        }
140    }
141}
142
143/// One response, written whole or streamed.
144#[derive(Debug)]
145pub struct Response {
146    status: u16,
147    reason: &'static str,
148    headers: Vec<(&'static str, String)>,
149    /// The Cache-Control value; `no-store` unless the response says otherwise.
150    cache: &'static str,
151    body: Body,
152}
153
154impl Response {
155    /// `body` with `status` and its content type.
156    #[must_use]
157    pub fn new(status: u16, content_type: &'static str, body: Vec<u8>) -> Self {
158        let reason = match status {
159            200 => "OK",
160            204 => "No Content",
161            303 => "See Other",
162            400 => "Bad Request",
163            401 => "Unauthorized",
164            403 => "Forbidden",
165            404 => "Not Found",
166            405 => "Method Not Allowed",
167            502 => "Bad Gateway",
168            503 => "Service Unavailable",
169            _ => "Status",
170        };
171        Self {
172            status,
173            reason,
174            headers: vec![("Content-Type", content_type.to_owned())],
175            cache: "no-store",
176            body: Body::Whole(body),
177        }
178    }
179
180    /// A 200 whose body `body` writes after the head, as it is made, until it
181    /// returns; the connection's close then ends it. It is never cached.
182    #[must_use]
183    pub fn stream(
184        content_type: &'static str,
185        body: impl FnOnce(&mut dyn Write) -> io::Result<()> + Send + 'static,
186    ) -> Self {
187        Self::stream_with_peer(content_type, move |output, _peer| body(output))
188    }
189
190    pub(crate) fn stream_with_peer(
191        content_type: &'static str,
192        body: impl FnOnce(&mut dyn Write, Option<&TcpStream>) -> io::Result<()> + Send + 'static,
193    ) -> Self {
194        let mut response = Self::new(200, content_type, Vec::new());
195        response.body = Body::Stream(Box::new(body));
196        response
197    }
198
199    /// The same response kept by caches as `policy` says, in place of
200    /// `no-store`.
201    #[must_use]
202    pub const fn cached(mut self, policy: &'static str) -> Self {
203        self.cache = policy;
204        self
205    }
206
207    /// A JSON answer.
208    #[must_use]
209    pub fn json(status: u16, value: &serde_json::Value) -> Self {
210        Self::new(status, "application/json", value.to_string().into_bytes())
211    }
212
213    /// A 204: no body, so no Content-Type and no Content-Length.
214    #[must_use]
215    pub fn no_content() -> Self {
216        let mut response = Self::new(204, "text/plain; charset=utf-8", Vec::new());
217        response.headers.clear();
218        response
219    }
220
221    /// Send the browser to `location`.
222    #[must_use]
223    pub fn redirect(location: &str) -> Self {
224        Self::new(303, "text/plain; charset=utf-8", Vec::new()).with("Location", location)
225    }
226
227    /// The same response with one more header.
228    #[must_use]
229    pub fn with(mut self, name: &'static str, value: &str) -> Self {
230        self.headers.push((name, value.to_owned()));
231        self
232    }
233
234    /// Write the response to `output`; a streamed body is written until it
235    /// ends.
236    ///
237    /// # Errors
238    ///
239    /// `InvalidData`, before anything is written, for a header value holding
240    /// a line break; otherwise the write's own error, or the stream's.
241    pub fn write(self, output: &mut impl Write) -> io::Result<()> {
242        self.write_with_peer(output, None)
243    }
244
245    pub(super) fn write_to(self, stream: &TcpStream) -> io::Result<()> {
246        self.write_with_peer(&mut &*stream, Some(stream))
247    }
248
249    fn write_with_peer(self, output: &mut impl Write, peer: Option<&TcpStream>) -> io::Result<()> {
250        if self
251            .headers
252            .iter()
253            .any(|(_, value)| value.contains(['\r', '\n']))
254        {
255            return Err(refused("a header value holds a line break"));
256        }
257        let mut head = format!("HTTP/1.1 {} {}\r\n", self.status, self.reason);
258        for (name, value) in &self.headers {
259            head.push_str(name);
260            head.push_str(": ");
261            head.push_str(value);
262            head.push_str("\r\n");
263        }
264        // A 204 carries no Content-Length (RFC 9110 8.6).
265        if let Body::Whole(body) = &self.body
266            && self.status != 204
267        {
268            head.push_str("Content-Length: ");
269            head.push_str(&body.len().to_string());
270            head.push_str("\r\n");
271        }
272        head.push_str("Cache-Control: ");
273        head.push_str(self.cache);
274        head.push_str("\r\nConnection: close\r\n\r\n");
275        let mut bytes = head.into_bytes();
276        match self.body {
277            Body::Whole(body) => {
278                bytes.extend_from_slice(&body);
279                output.write_all(&bytes)?;
280                output.flush()
281            }
282            Body::Stream(body) => {
283                output.write_all(&bytes)?;
284                output.flush()?;
285                body(&mut *output, peer)?;
286                output.flush()
287            }
288        }
289    }
290}
291
292#[cfg(test)]
293#[path = "http_tests.rs"]
294mod tests;