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::ops::{Deref, DerefMut};
11
12use bytes::Bytes;
13use http_body_util::Full;
14
15use crate::context::Context;
16
17pub type Response = hyper::Response<Full<Bytes>>;
18
19/// Incoming HTTP request with an attached per-request [`Context`].
20pub struct Request {
21    inner: hyper::Request<hyper::body::Incoming>,
22    ctx: Context,
23}
24
25impl Request {
26    pub(crate) fn new(inner: hyper::Request<hyper::body::Incoming>) -> Self {
27        Self {
28            inner,
29            ctx: Context::new(),
30        }
31    }
32
33    /// Read-only access to the per-request [`Context`].
34    pub fn ctx(&self) -> &Context {
35        &self.ctx
36    }
37
38    /// Mutable access to the per-request [`Context`].
39    pub fn ctx_mut(&mut self) -> &mut Context {
40        &mut self.ctx
41    }
42
43    /// Parse the URL query string into a [`FormData`].
44    ///
45    /// Returns an empty `FormData` when the request has no query string.
46    pub fn query(&self) -> FormData {
47        FormData::parse(self.inner.uri().query().unwrap_or(""))
48    }
49
50    /// Consume this request, returning the underlying hyper parts, body,
51    /// and the attached context.
52    pub fn into_parts(self) -> (hyper::http::request::Parts, hyper::body::Incoming, Context) {
53        let (parts, body) = self.inner.into_parts();
54        (parts, body, self.ctx)
55    }
56}
57
58impl Deref for Request {
59    type Target = hyper::Request<hyper::body::Incoming>;
60    fn deref(&self) -> &Self::Target {
61        &self.inner
62    }
63}
64
65impl DerefMut for Request {
66    fn deref_mut(&mut self) -> &mut Self::Target {
67        &mut self.inner
68    }
69}
70
71/// Build a `200 OK` response with `text/plain` content type.
72pub fn text(body: impl Into<String>) -> Response {
73    response(200, "text/plain; charset=utf-8", body.into().into_bytes())
74}
75
76/// Build a `200 OK` response with `text/html` content type.
77pub fn html(body: impl Into<String>) -> Response {
78    response(200, "text/html; charset=utf-8", body.into().into_bytes())
79}
80
81/// Build a `200 OK` response with `application/json` content type.
82///
83/// The body is written verbatim; it is the caller's responsibility to pass
84/// a valid JSON document (e.g. from `serde_json::to_string(&value)?`).
85pub fn json_raw(body: impl Into<String>) -> Response {
86    response(
87        200,
88        "application/json; charset=utf-8",
89        body.into().into_bytes(),
90    )
91}
92
93/// Build a response with an arbitrary status code and a `text/plain` body.
94pub fn status_text(status: u16, body: impl Into<String>) -> Response {
95    response(
96        status,
97        "text/plain; charset=utf-8",
98        body.into().into_bytes(),
99    )
100}
101
102fn response(status: u16, content_type: &'static str, body: Vec<u8>) -> Response {
103    hyper::Response::builder()
104        .status(status)
105        .header("content-type", content_type)
106        .body(Full::new(Bytes::from(body)))
107        .expect("valid response")
108}
109
110/// Parsed `application/x-www-form-urlencoded` data.
111///
112/// Used for both URL query strings (via [`Request::query`]) and POST
113/// request bodies (the admin layer reads form submissions this way).
114pub struct FormData {
115    map: HashMap<String, String>,
116}
117
118impl FormData {
119    /// Parse a URL-encoded key/value string.
120    pub fn parse(body: &str) -> Self {
121        let mut map = HashMap::new();
122        for pair in body.split('&') {
123            if pair.is_empty() {
124                continue;
125            }
126            let mut iter = pair.splitn(2, '=');
127            let raw_key = match iter.next() {
128                Some(k) if !k.is_empty() => k,
129                _ => continue,
130            };
131            let raw_val = iter.next().unwrap_or("");
132            map.insert(percent_decode(raw_key), percent_decode(raw_val));
133        }
134        FormData { map }
135    }
136
137    pub fn get(&self, key: &str) -> Option<&str> {
138        self.map.get(key).map(String::as_str)
139    }
140
141    pub fn len(&self) -> usize {
142        self.map.len()
143    }
144
145    pub fn is_empty(&self) -> bool {
146        self.map.is_empty()
147    }
148}
149
150pub(crate) fn percent_decode(input: &str) -> String {
151    let bytes = input.as_bytes();
152    let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
153    let mut i = 0;
154    while i < bytes.len() {
155        let b = bytes[i];
156        if b == b'+' {
157            out.push(b' ');
158            i += 1;
159        } else if b == b'%' && i + 2 < bytes.len() {
160            if let (Some(h), Some(l)) = (hex_digit(bytes[i + 1]), hex_digit(bytes[i + 2])) {
161                out.push((h << 4) | l);
162                i += 3;
163                continue;
164            }
165            out.push(b);
166            i += 1;
167        } else {
168            out.push(b);
169            i += 1;
170        }
171    }
172    String::from_utf8_lossy(&out).into_owned()
173}
174
175fn hex_digit(b: u8) -> Option<u8> {
176    match b {
177        b'0'..=b'9' => Some(b - b'0'),
178        b'a'..=b'f' => Some(b - b'a' + 10),
179        b'A'..=b'F' => Some(b - b'A' + 10),
180        _ => None,
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn form_parse_decodes_basic_pairs() {
190        let form = FormData::parse("a=1&b=2");
191        assert_eq!(form.get("a"), Some("1"));
192        assert_eq!(form.get("b"), Some("2"));
193    }
194
195    #[test]
196    fn form_parse_decodes_plus_as_space() {
197        let form = FormData::parse("name=John+Doe");
198        assert_eq!(form.get("name"), Some("John Doe"));
199    }
200
201    #[test]
202    fn form_parse_decodes_percent_encoded() {
203        let form = FormData::parse("q=hello%20world%21");
204        assert_eq!(form.get("q"), Some("hello world!"));
205    }
206
207    #[test]
208    fn form_parse_handles_empty_values() {
209        let form = FormData::parse("a=&b=x");
210        assert_eq!(form.get("a"), Some(""));
211        assert_eq!(form.get("b"), Some("x"));
212    }
213
214    #[test]
215    fn form_parse_ignores_empty_pairs() {
216        let form = FormData::parse("&a=1&&b=2&");
217        assert_eq!(form.get("a"), Some("1"));
218        assert_eq!(form.get("b"), Some("2"));
219        assert_eq!(form.len(), 2);
220    }
221
222    #[test]
223    fn form_missing_key_is_none() {
224        let form = FormData::parse("a=1");
225        assert!(form.get("missing").is_none());
226    }
227
228    #[test]
229    fn percent_decode_passes_through_unreserved() {
230        assert_eq!(percent_decode("abcXYZ123-_.~"), "abcXYZ123-_.~");
231    }
232
233    #[test]
234    fn percent_decode_handles_lowercase_and_uppercase_hex() {
235        assert_eq!(percent_decode("%2f%2F"), "//");
236    }
237
238    #[test]
239    fn percent_decode_leaves_invalid_percent_sequences_alone() {
240        assert_eq!(percent_decode("%GG"), "%GG");
241        assert_eq!(percent_decode("end%"), "end%");
242    }
243}