Skip to main content

rustio_core/
http.rs

1//! HTTP primitives: [`Request`], [`Response`], response builders, and a small
2//! [`FormData`] parser shared by query strings and form bodies.
3//!
4//! [`Request`] wraps [`hyper::Request`] and adds a typed per-request
5//! [`Context`]. It derefs to the underlying hyper request so the usual
6//! accessors (`.method()`, `.uri()`, `.headers()`, `.body_mut()`) are
7//! available directly.
8
9use std::collections::HashMap;
10use std::net::SocketAddr;
11use std::ops::{Deref, DerefMut};
12
13use bytes::Bytes;
14use http_body_util::Full;
15
16use crate::context::Context;
17
18pub type Response = hyper::Response<Full<Bytes>>;
19
20/// Maximum bytes accepted from a request body across the framework.
21///
22/// The global body-limit middleware rejects any request whose
23/// `Content-Length` exceeds this value with `Error::PayloadTooLarge`
24/// (HTTP 413); the admin form reader enforces the same cap while
25/// collecting the body so chunked or mis-labelled requests can't slip
26/// past. Custom handlers that read bodies directly should use the
27/// same constant.
28pub const MAX_REQUEST_BODY_BYTES: usize = 2 * 1024 * 1024;
29
30/// Incoming HTTP request with an attached per-request [`Context`]
31/// and, when known, the peer address the TCP connection came from.
32pub struct Request {
33    inner: hyper::Request<hyper::body::Incoming>,
34    ctx: Context,
35    peer: Option<SocketAddr>,
36}
37
38impl Request {
39    pub(crate) fn new(
40        inner: hyper::Request<hyper::body::Incoming>,
41        peer: Option<SocketAddr>,
42    ) -> Self {
43        Self {
44            inner,
45            ctx: Context::new(),
46            peer,
47        }
48    }
49
50    /// The client's socket address, if the server could determine it.
51    ///
52    /// Populated by [`crate::server::Server`] from the accept result.
53    /// May be `None` when the request is constructed from a source
54    /// that doesn't carry it (tests, reverse proxies that terminate
55    /// the connection — the `X-Forwarded-For` header is not parsed
56    /// here; projects that need the upstream IP must parse it
57    /// themselves).
58    pub fn peer_addr(&self) -> Option<SocketAddr> {
59        self.peer
60    }
61
62    /// Read-only access to the per-request [`Context`].
63    pub fn ctx(&self) -> &Context {
64        &self.ctx
65    }
66
67    /// Mutable access to the per-request [`Context`].
68    pub fn ctx_mut(&mut self) -> &mut Context {
69        &mut self.ctx
70    }
71
72    /// Parse the URL query string into a [`FormData`].
73    ///
74    /// Returns an empty `FormData` when the request has no query string.
75    pub fn query(&self) -> FormData {
76        FormData::parse(self.inner.uri().query().unwrap_or(""))
77    }
78
79    /// Look up a single cookie value by name.
80    ///
81    /// Returns `None` if the request has no `Cookie` header, the header is
82    /// not valid UTF-8, or no cookie with this name is present. The value
83    /// is returned as-is (not URL-decoded).
84    pub fn cookie(&self, name: &str) -> Option<String> {
85        let header = self
86            .inner
87            .headers()
88            .get(hyper::header::COOKIE)?
89            .to_str()
90            .ok()?;
91        for pair in header.split(';') {
92            let pair = pair.trim();
93            if let Some((k, v)) = pair.split_once('=') {
94                if k == name {
95                    return Some(v.to_string());
96                }
97            }
98        }
99        None
100    }
101
102    /// Consume this request, returning the underlying hyper parts, body,
103    /// and the attached context.
104    pub fn into_parts(self) -> (hyper::http::request::Parts, hyper::body::Incoming, Context) {
105        let (parts, body) = self.inner.into_parts();
106        (parts, body, self.ctx)
107    }
108}
109
110impl Deref for Request {
111    type Target = hyper::Request<hyper::body::Incoming>;
112    fn deref(&self) -> &Self::Target {
113        &self.inner
114    }
115}
116
117impl DerefMut for Request {
118    fn deref_mut(&mut self) -> &mut Self::Target {
119        &mut self.inner
120    }
121}
122
123/// Build a `200 OK` response with `text/plain` content type.
124pub fn text(body: impl Into<String>) -> Response {
125    response(200, "text/plain; charset=utf-8", body.into().into_bytes())
126}
127
128/// Build a `200 OK` response with `text/html` content type.
129pub fn html(body: impl Into<String>) -> Response {
130    response(200, "text/html; charset=utf-8", body.into().into_bytes())
131}
132
133/// Build a `200 OK` response with `application/json` content type.
134///
135/// The body is written verbatim; it is the caller's responsibility to pass
136/// a valid JSON document (e.g. from `serde_json::to_string(&value)?`).
137pub fn json_raw(body: impl Into<String>) -> Response {
138    response(
139        200,
140        "application/json; charset=utf-8",
141        body.into().into_bytes(),
142    )
143}
144
145/// Build a response with an arbitrary status code and a `text/plain` body.
146pub fn status_text(status: u16, body: impl Into<String>) -> Response {
147    response(
148        status,
149        "text/plain; charset=utf-8",
150        body.into().into_bytes(),
151    )
152}
153
154/// Append a `Set-Cookie` header to a response.
155///
156/// The caller is responsible for formatting `value` as a valid
157/// `Set-Cookie` string (e.g. `"name=val; Path=/; HttpOnly; SameSite=Lax"`).
158/// Returns silently if `value` contains characters that aren't valid in
159/// an HTTP header.
160pub fn set_cookie(resp: &mut Response, value: &str) {
161    if let Ok(hv) = value.parse() {
162        resp.headers_mut().append(hyper::header::SET_COOKIE, hv);
163    }
164}
165
166fn response(status: u16, content_type: &'static str, body: Vec<u8>) -> Response {
167    hyper::Response::builder()
168        .status(status)
169        .header("content-type", content_type)
170        .body(Full::new(Bytes::from(body)))
171        .expect("valid response")
172}
173
174/// Parsed `application/x-www-form-urlencoded` data.
175///
176/// Used for both URL query strings (via [`Request::query`]) and POST
177/// request bodies (the admin layer reads form submissions this way).
178///
179/// Stores both a deduped `key → last-value` map (for the common
180/// single-value lookup path via [`get`](Self::get)) **and** the full
181/// ordered pair list (for forms that submit the same key multiple
182/// times — e.g. bulk-action checkboxes producing
183/// `ids=1&ids=2&ids=3`). [`get_all`](Self::get_all) returns every
184/// value bound to a key.
185pub struct FormData {
186    map: HashMap<String, String>,
187    pairs: Vec<(String, String)>,
188}
189
190impl FormData {
191    /// Parse a URL-encoded key/value string.
192    pub fn parse(body: &str) -> Self {
193        let mut map = HashMap::new();
194        let mut pairs = Vec::new();
195        for pair in body.split('&') {
196            if pair.is_empty() {
197                continue;
198            }
199            let mut iter = pair.splitn(2, '=');
200            let raw_key = match iter.next() {
201                Some(k) if !k.is_empty() => k,
202                _ => continue,
203            };
204            let raw_val = iter.next().unwrap_or("");
205            let key = percent_decode(raw_key);
206            let val = percent_decode(raw_val);
207            map.insert(key.clone(), val.clone());
208            pairs.push((key, val));
209        }
210        FormData { map, pairs }
211    }
212
213    pub fn get(&self, key: &str) -> Option<&str> {
214        self.map.get(key).map(String::as_str)
215    }
216
217    /// Return every value bound to `key`, in submission order. Used
218    /// by bulk-action handlers to read all `ids=…` entries (HTML
219    /// checkbox groups send the same key once per checked row).
220    pub fn get_all(&self, key: &str) -> Vec<&str> {
221        self.pairs
222            .iter()
223            .filter_map(|(k, v)| (k == key).then_some(v.as_str()))
224            .collect()
225    }
226
227    pub fn len(&self) -> usize {
228        self.map.len()
229    }
230
231    pub fn is_empty(&self) -> bool {
232        self.map.is_empty()
233    }
234
235    /// Consume into the underlying key/value map. Useful for callers
236    /// that want to hand the parsed form to a handler that takes a
237    /// plain `&HashMap<String, String>`.
238    pub fn into_map(self) -> HashMap<String, String> {
239        self.map
240    }
241}
242
243pub(crate) fn percent_decode(input: &str) -> String {
244    let bytes = input.as_bytes();
245    let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
246    let mut i = 0;
247    while i < bytes.len() {
248        let b = bytes[i];
249        if b == b'+' {
250            out.push(b' ');
251            i += 1;
252        } else if b == b'%' && i + 2 < bytes.len() {
253            if let (Some(h), Some(l)) = (hex_digit(bytes[i + 1]), hex_digit(bytes[i + 2])) {
254                out.push((h << 4) | l);
255                i += 3;
256                continue;
257            }
258            out.push(b);
259            i += 1;
260        } else {
261            out.push(b);
262            i += 1;
263        }
264    }
265    String::from_utf8_lossy(&out).into_owned()
266}
267
268fn hex_digit(b: u8) -> Option<u8> {
269    match b {
270        b'0'..=b'9' => Some(b - b'0'),
271        b'a'..=b'f' => Some(b - b'a' + 10),
272        b'A'..=b'F' => Some(b - b'A' + 10),
273        _ => None,
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280
281    #[test]
282    fn form_parse_decodes_basic_pairs() {
283        let form = FormData::parse("a=1&b=2");
284        assert_eq!(form.get("a"), Some("1"));
285        assert_eq!(form.get("b"), Some("2"));
286    }
287
288    #[test]
289    fn form_parse_decodes_plus_as_space() {
290        let form = FormData::parse("name=John+Doe");
291        assert_eq!(form.get("name"), Some("John Doe"));
292    }
293
294    #[test]
295    fn form_parse_decodes_percent_encoded() {
296        let form = FormData::parse("q=hello%20world%21");
297        assert_eq!(form.get("q"), Some("hello world!"));
298    }
299
300    #[test]
301    fn form_parse_handles_empty_values() {
302        let form = FormData::parse("a=&b=x");
303        assert_eq!(form.get("a"), Some(""));
304        assert_eq!(form.get("b"), Some("x"));
305    }
306
307    #[test]
308    fn form_parse_ignores_empty_pairs() {
309        let form = FormData::parse("&a=1&&b=2&");
310        assert_eq!(form.get("a"), Some("1"));
311        assert_eq!(form.get("b"), Some("2"));
312        assert_eq!(form.len(), 2);
313    }
314
315    #[test]
316    fn form_missing_key_is_none() {
317        let form = FormData::parse("a=1");
318        assert!(form.get("missing").is_none());
319    }
320
321    #[test]
322    fn percent_decode_passes_through_unreserved() {
323        assert_eq!(percent_decode("abcXYZ123-_.~"), "abcXYZ123-_.~");
324    }
325
326    #[test]
327    fn percent_decode_handles_lowercase_and_uppercase_hex() {
328        assert_eq!(percent_decode("%2f%2F"), "//");
329    }
330
331    #[test]
332    fn percent_decode_leaves_invalid_percent_sequences_alone() {
333        assert_eq!(percent_decode("%GG"), "%GG");
334        assert_eq!(percent_decode("end%"), "end%");
335    }
336}