Skip to main content

a3s_boot/http/
request.rs

1use super::header::{
2    accepts_event_stream_response, accepts_json_response, get_header, is_json_media_type,
3    matches_media_type, normalize_header_name, normalize_headers, parse_content_length,
4    parse_cookie_header_values, strict_content_length_values, validate_header_name,
5    validate_header_value,
6};
7use super::method::HttpMethod;
8use super::query::{parse_query, parse_query_pairs, split_path_query};
9use crate::percent::validate_percent_encoding;
10use crate::{BootError, Result};
11use serde::{de::DeserializeOwned, Serialize};
12use std::collections::BTreeMap;
13
14/// Framework-neutral HTTP request passed to Boot route handlers.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct BootRequest {
17    pub method: HttpMethod,
18    pub path: String,
19    pub query_string: Option<String>,
20    pub query: BTreeMap<String, String>,
21    pub params: BTreeMap<String, String>,
22    pub headers: BTreeMap<String, String>,
23    pub appended_headers: Vec<(String, String)>,
24    pub body: Vec<u8>,
25}
26
27impl BootRequest {
28    pub fn new(method: HttpMethod, path: impl Into<String>) -> Self {
29        let (path, query_string, query) = split_path_query(path.into());
30        Self {
31            method,
32            path,
33            query_string,
34            query,
35            params: BTreeMap::new(),
36            headers: BTreeMap::new(),
37            appended_headers: Vec::new(),
38            body: Vec::new(),
39        }
40    }
41
42    pub fn method(&self) -> HttpMethod {
43        self.method
44    }
45
46    pub fn path(&self) -> &str {
47        &self.path
48    }
49
50    pub fn query_string(&self) -> Option<&str> {
51        self.query_string.as_deref()
52    }
53
54    pub fn with_query_string(mut self, query_string: impl Into<String>) -> Self {
55        let query_string = query_string.into();
56        self.query = parse_query(&query_string);
57        self.query_string = Some(query_string);
58        self
59    }
60
61    pub fn with_path_params(mut self, params: BTreeMap<String, String>) -> Self {
62        self.params = params;
63        self
64    }
65
66    pub fn with_param(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
67        self.params.insert(name.into(), value.into());
68        self
69    }
70
71    pub fn with_query_param(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
72        self.query.insert(name.into(), value.into());
73        self.query_string = None;
74        self
75    }
76
77    pub fn with_body(mut self, body: impl Into<Vec<u8>>) -> Self {
78        self.body = body.into();
79        self
80    }
81
82    pub fn body(&self) -> &[u8] {
83        &self.body
84    }
85
86    pub fn into_body(self) -> Vec<u8> {
87        self.body
88    }
89
90    pub fn with_text(self, body: impl Into<String>) -> Self {
91        self.with_body(body.into())
92            .with_header("content-type", "text/plain; charset=utf-8")
93    }
94
95    pub fn with_json<T>(self, body: &T) -> Result<Self>
96    where
97        T: Serialize,
98    {
99        let body = serde_json::to_vec(body).map_err(|err| BootError::Internal(err.to_string()))?;
100        Ok(self
101            .with_body(body)
102            .with_header("content-type", "application/json"))
103    }
104
105    pub fn with_content_type(self, content_type: impl Into<String>) -> Self {
106        self.with_header("content-type", content_type)
107    }
108
109    pub fn with_content_length(self, content_length: u64) -> Self {
110        self.with_header("content-length", content_length.to_string())
111    }
112
113    pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
114        self.headers
115            .insert(normalize_header_name(name), value.into());
116        self
117    }
118
119    pub fn with_headers(mut self, headers: BTreeMap<String, String>) -> Self {
120        self.headers = normalize_headers(headers);
121        self
122    }
123
124    pub fn append_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
125        self.appended_headers
126            .push((normalize_header_name(name), value.into()));
127        self
128    }
129
130    pub fn header_entries(&self) -> impl Iterator<Item = (&str, &str)> {
131        self.headers
132            .iter()
133            .map(|(name, value)| (name.as_str(), value.as_str()))
134            .chain(
135                self.appended_headers
136                    .iter()
137                    .map(|(name, value)| (name.as_str(), value.as_str())),
138            )
139    }
140
141    pub fn validate_headers(&self) -> Result<()> {
142        for (name, value) in self.header_entries() {
143            validate_request_header(name, value)?;
144        }
145
146        Ok(())
147    }
148
149    pub fn text(&self) -> Result<String> {
150        String::from_utf8(self.body.clone()).map_err(|err| BootError::BadRequest(err.to_string()))
151    }
152
153    pub fn param(&self, name: &str) -> Option<&str> {
154        self.params.get(name).map(String::as_str)
155    }
156
157    pub fn params<T>(&self) -> Result<T>
158    where
159        T: DeserializeOwned,
160    {
161        let params = serde_urlencoded::to_string(&self.params)
162            .map_err(|err| BootError::BadRequest(err.to_string()))?;
163        serde_urlencoded::from_str(&params).map_err(|err| BootError::BadRequest(err.to_string()))
164    }
165
166    pub fn query_param(&self, name: &str) -> Option<&str> {
167        self.query.get(name).map(String::as_str)
168    }
169
170    pub fn query_value(&self, name: &str) -> Result<Option<String>> {
171        Ok(self
172            .query_pairs()?
173            .into_iter()
174            .find_map(|(key, value)| (key == name).then_some(value)))
175    }
176
177    pub fn query_values(&self, name: &str) -> Result<Vec<String>> {
178        Ok(self
179            .query_pairs()?
180            .into_iter()
181            .filter_map(|(key, value)| (key == name).then_some(value))
182            .collect())
183    }
184
185    pub fn query_pairs(&self) -> Result<Vec<(String, String)>> {
186        match self.query_string.as_deref() {
187            Some(query) => parse_query_pairs(query),
188            None => Ok(self
189                .query
190                .iter()
191                .map(|(key, value)| (key.clone(), value.clone()))
192                .collect()),
193        }
194    }
195
196    pub fn header(&self, name: &str) -> Option<&str> {
197        get_header(&self.headers, name)
198    }
199
200    pub fn header_values(&self, name: &str) -> Vec<&str> {
201        let mut values = self.header(name).into_iter().collect::<Vec<_>>();
202        values.extend(
203            self.appended_headers
204                .iter()
205                .filter(|(key, _)| key.eq_ignore_ascii_case(name))
206                .map(|(_, value)| value.as_str()),
207        );
208        values
209    }
210
211    pub fn authorization(&self) -> Option<&str> {
212        self.header_values("authorization").into_iter().next()
213    }
214
215    pub fn bearer_token(&self) -> Option<&str> {
216        let authorization = self.authorization()?.trim();
217        let mut parts = authorization.splitn(2, char::is_whitespace);
218        let scheme = parts.next()?;
219        let token = parts.next()?.trim();
220
221        if scheme.eq_ignore_ascii_case("bearer") && !token.is_empty() {
222            Some(token)
223        } else {
224            None
225        }
226    }
227
228    pub fn require_bearer_token(&self) -> Result<&str> {
229        self.bearer_token()
230            .ok_or_else(|| BootError::Unauthorized("missing bearer token".to_string()))
231    }
232
233    pub fn cookie_pairs(&self) -> Result<Vec<(String, String)>> {
234        parse_cookie_header_values(&self.header_values("cookie"))
235    }
236
237    pub fn cookie(&self, name: &str) -> Result<Option<String>> {
238        Ok(self
239            .cookie_pairs()?
240            .into_iter()
241            .find_map(|(key, value)| (key == name).then_some(value)))
242    }
243
244    pub fn require_cookie(&self, name: &str) -> Result<String> {
245        self.cookie(name)?
246            .ok_or_else(|| BootError::Unauthorized(format!("missing cookie: {name}")))
247    }
248
249    pub fn cookie_values(&self, name: &str) -> Result<Vec<String>> {
250        Ok(self
251            .cookie_pairs()?
252            .into_iter()
253            .filter_map(|(key, value)| (key == name).then_some(value))
254            .collect())
255    }
256
257    pub fn cookies(&self) -> Result<BTreeMap<String, String>> {
258        let mut cookies = BTreeMap::new();
259        for (name, value) in self.cookie_pairs()? {
260            cookies.entry(name).or_insert(value);
261        }
262        Ok(cookies)
263    }
264
265    pub fn content_type(&self) -> Option<&str> {
266        self.header_values("content-type").into_iter().next()
267    }
268
269    pub fn content_length(&self) -> Result<Option<u64>> {
270        let Some(content_length) = self.header_values("content-length").into_iter().next() else {
271            return Ok(None);
272        };
273
274        parse_content_length(content_length)
275            .map(Some)
276            .ok_or_else(|| {
277                BootError::BadRequest(format!("invalid content-length header: {content_length}"))
278            })
279    }
280
281    pub fn strict_content_length(&self) -> Result<Option<u64>> {
282        strict_content_length_values(
283            self.header_values("content-length"),
284            |content_length| {
285                BootError::BadRequest(format!("invalid content-length header: {content_length}"))
286            },
287            |expected_content_length, content_length| {
288                BootError::BadRequest(format!(
289                    "conflicting content-length headers: {expected_content_length} != {content_length}"
290                ))
291            },
292        )
293    }
294
295    pub fn validate_content_length(&self) -> Result<()> {
296        let Some(content_length) = self.strict_content_length()? else {
297            return Ok(());
298        };
299        let actual_body_length = self.body.len() as u64;
300        if actual_body_length == content_length {
301            return Ok(());
302        }
303
304        Err(BootError::BadRequest(format!(
305            "content-length header does not match request body length: expected {content_length}, got {actual_body_length}"
306        )))
307    }
308
309    pub fn validate_body_limit(&self, body_limit: usize) -> Result<()> {
310        if self
311            .strict_content_length()?
312            .is_some_and(|content_length| content_length > body_limit as u64)
313            || self.body.len() > body_limit
314        {
315            return Err(BootError::PayloadTooLarge(format!(
316                "request body exceeds {body_limit} bytes"
317            )));
318        }
319
320        Ok(())
321    }
322
323    pub fn validate(&self) -> Result<()> {
324        self.validate_headers()?;
325        self.validate_content_length()
326    }
327
328    pub fn validate_with_body_limit(&self, body_limit: usize) -> Result<()> {
329        self.validate_headers()?;
330        self.validate_body_limit(body_limit)?;
331        self.validate_content_length()
332    }
333
334    pub fn is_content_type(&self, media_type: &str) -> bool {
335        self.content_type()
336            .is_some_and(|content_type| matches_media_type(content_type, media_type))
337    }
338
339    pub fn is_json_content_type(&self) -> bool {
340        self.content_type().is_some_and(is_json_media_type)
341    }
342
343    pub fn require_json_content_type(&self) -> Result<()> {
344        if self.is_json_content_type() {
345            return Ok(());
346        }
347
348        let message = match self.content_type() {
349            Some(content_type) => format!("expected JSON content type, got {content_type}"),
350            None => "expected JSON content type".to_string(),
351        };
352        Err(BootError::UnsupportedMediaType(message))
353    }
354
355    pub fn accepts_json(&self) -> bool {
356        accepts_json_response(&self.header_values("accept"))
357    }
358
359    pub fn require_accepts_json(&self) -> Result<()> {
360        if self.accepts_json() {
361            return Ok(());
362        }
363
364        Err(BootError::NotAcceptable(
365            "expected client to accept JSON response".to_string(),
366        ))
367    }
368
369    pub fn accepts_event_stream(&self) -> bool {
370        accepts_event_stream_response(&self.header_values("accept"))
371    }
372
373    pub fn require_accepts_event_stream(&self) -> Result<()> {
374        if self.accepts_event_stream() {
375            return Ok(());
376        }
377
378        Err(BootError::NotAcceptable(
379            "expected client to accept text/event-stream response".to_string(),
380        ))
381    }
382
383    pub fn json<T>(&self) -> Result<T>
384    where
385        T: DeserializeOwned,
386    {
387        serde_json::from_slice(&self.body).map_err(|err| BootError::BadRequest(err.to_string()))
388    }
389
390    pub fn json_with_content_type<T>(&self) -> Result<T>
391    where
392        T: DeserializeOwned,
393    {
394        self.require_json_content_type()?;
395        self.json()
396    }
397
398    pub fn query<T>(&self) -> Result<T>
399    where
400        T: DeserializeOwned,
401    {
402        let query = match self.query_string.as_deref() {
403            Some(query) => {
404                validate_percent_encoding(query)?;
405                query.to_string()
406            }
407            None => serde_urlencoded::to_string(&self.query)
408                .map_err(|err| BootError::BadRequest(err.to_string()))?,
409        };
410        serde_urlencoded::from_str(&query).map_err(|err| BootError::BadRequest(err.to_string()))
411    }
412}
413
414fn validate_request_header(name: &str, value: &str) -> Result<()> {
415    validate_header_name(name).map_err(|message| {
416        BootError::BadRequest(format!("invalid request header name {name:?}: {message}"))
417    })?;
418    validate_header_value(value).map_err(|message| {
419        BootError::BadRequest(format!(
420            "invalid request header value for {name:?}: {message}"
421        ))
422    })
423}