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::routing::host::normalize_host_header;
11#[cfg(feature = "auth")]
12use crate::AuthPrincipal;
13use crate::{validate_value, BootError, ModuleRef, ProviderToken, Result, Validate};
14use serde::{de::DeserializeOwned, Serialize};
15use std::collections::BTreeMap;
16use std::fmt;
17use std::str::FromStr;
18use std::sync::Arc;
19#[cfg(feature = "auth")]
20use std::sync::RwLock;
21
22/// Framework-neutral HTTP request passed to Boot route handlers.
23#[derive(Debug, Clone)]
24pub struct BootRequest {
25    pub method: HttpMethod,
26    pub path: String,
27    pub query_string: Option<String>,
28    pub query: BTreeMap<String, String>,
29    pub params: BTreeMap<String, String>,
30    pub host_params: BTreeMap<String, String>,
31    pub headers: BTreeMap<String, String>,
32    pub appended_headers: Vec<(String, String)>,
33    pub body: Vec<u8>,
34    module_ref: Option<ModuleRef>,
35    #[cfg(feature = "auth")]
36    auth_principal: Arc<RwLock<Option<AuthPrincipal>>>,
37}
38
39impl PartialEq for BootRequest {
40    fn eq(&self, other: &Self) -> bool {
41        self.method == other.method
42            && self.path == other.path
43            && self.query_string == other.query_string
44            && self.query == other.query
45            && self.params == other.params
46            && self.host_params == other.host_params
47            && self.headers == other.headers
48            && self.appended_headers == other.appended_headers
49            && self.body == other.body
50    }
51}
52
53impl Eq for BootRequest {}
54
55impl BootRequest {
56    pub fn new(method: HttpMethod, path: impl Into<String>) -> Self {
57        let (path, query_string, query) = split_path_query(path.into());
58        Self {
59            method,
60            path,
61            query_string,
62            query,
63            params: BTreeMap::new(),
64            host_params: BTreeMap::new(),
65            headers: BTreeMap::new(),
66            appended_headers: Vec::new(),
67            body: Vec::new(),
68            module_ref: None,
69            #[cfg(feature = "auth")]
70            auth_principal: Arc::new(RwLock::new(None)),
71        }
72    }
73
74    pub fn method(&self) -> HttpMethod {
75        self.method
76    }
77
78    pub fn path(&self) -> &str {
79        &self.path
80    }
81
82    pub fn query_string(&self) -> Option<&str> {
83        self.query_string.as_deref()
84    }
85
86    pub fn with_query_string(mut self, query_string: impl Into<String>) -> Self {
87        let query_string = query_string.into();
88        self.query = parse_query(&query_string);
89        self.query_string = Some(query_string);
90        self
91    }
92
93    pub(crate) fn with_matched_path(mut self, path: impl Into<String>) -> Self {
94        self.path = path.into();
95        self
96    }
97
98    pub(crate) fn with_module_ref(mut self, module_ref: ModuleRef) -> Self {
99        self.module_ref = Some(module_ref);
100        self
101    }
102
103    pub fn module_ref(&self) -> Option<&ModuleRef> {
104        self.module_ref.as_ref()
105    }
106
107    pub fn get<T>(&self) -> Result<Arc<T>>
108    where
109        T: Send + Sync + 'static,
110    {
111        self.module_ref
112            .as_ref()
113            .ok_or_else(|| BootError::MissingProvider(ProviderToken::of::<T>().to_string()))?
114            .get::<T>()
115    }
116
117    pub fn get_named<T>(&self, token: &str) -> Result<Arc<T>>
118    where
119        T: Send + Sync + 'static,
120    {
121        self.module_ref
122            .as_ref()
123            .ok_or_else(|| BootError::MissingProvider(ProviderToken::named(token).to_string()))?
124            .get_named::<T>(token)
125    }
126
127    pub fn get_optional<T>(&self) -> Result<Option<Arc<T>>>
128    where
129        T: Send + Sync + 'static,
130    {
131        match &self.module_ref {
132            Some(module_ref) => module_ref.get_optional::<T>(),
133            None => Ok(None),
134        }
135    }
136
137    pub fn get_optional_named<T>(&self, token: &str) -> Result<Option<Arc<T>>>
138    where
139        T: Send + Sync + 'static,
140    {
141        match &self.module_ref {
142            Some(module_ref) => module_ref.get_optional_named::<T>(token),
143            None => Ok(None),
144        }
145    }
146
147    #[cfg(feature = "auth")]
148    pub fn with_auth_principal(mut self, principal: AuthPrincipal) -> Self {
149        self.auth_principal = Arc::new(RwLock::new(Some(principal)));
150        self
151    }
152
153    #[cfg(feature = "auth")]
154    pub fn set_auth_principal(&self, principal: AuthPrincipal) -> Result<()> {
155        *self
156            .auth_principal
157            .write()
158            .map_err(|_| BootError::Internal("auth principal lock is poisoned".to_string()))? =
159            Some(principal);
160        Ok(())
161    }
162
163    #[cfg(feature = "auth")]
164    pub fn clear_auth_principal(&self) -> Result<()> {
165        *self
166            .auth_principal
167            .write()
168            .map_err(|_| BootError::Internal("auth principal lock is poisoned".to_string()))? =
169            None;
170        Ok(())
171    }
172
173    #[cfg(feature = "auth")]
174    pub fn auth_principal(&self) -> Result<Option<AuthPrincipal>> {
175        Ok(self
176            .auth_principal
177            .read()
178            .map_err(|_| BootError::Internal("auth principal lock is poisoned".to_string()))?
179            .clone())
180    }
181
182    #[cfg(feature = "auth")]
183    pub fn require_auth_principal(&self) -> Result<AuthPrincipal> {
184        self.auth_principal()?
185            .ok_or_else(|| BootError::Unauthorized("missing authenticated principal".to_string()))
186    }
187
188    pub fn with_path_params(mut self, params: BTreeMap<String, String>) -> Self {
189        self.params = params;
190        self
191    }
192
193    pub fn with_host_params(mut self, params: BTreeMap<String, String>) -> Self {
194        self.host_params = params;
195        self
196    }
197
198    pub fn with_param(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
199        self.params.insert(name.into(), value.into());
200        self
201    }
202
203    pub fn with_host_param(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
204        self.host_params.insert(name.into(), value.into());
205        self
206    }
207
208    pub fn with_query_param(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
209        self.query.insert(name.into(), value.into());
210        self.query_string = None;
211        self
212    }
213
214    pub fn with_body(mut self, body: impl Into<Vec<u8>>) -> Self {
215        self.body = body.into();
216        self
217    }
218
219    pub fn body(&self) -> &[u8] {
220        &self.body
221    }
222
223    pub fn into_body(self) -> Vec<u8> {
224        self.body
225    }
226
227    pub fn with_text(self, body: impl Into<String>) -> Self {
228        self.with_body(body.into())
229            .with_header("content-type", "text/plain; charset=utf-8")
230    }
231
232    pub fn with_json<T>(self, body: &T) -> Result<Self>
233    where
234        T: Serialize,
235    {
236        let body = serde_json::to_vec(body).map_err(|err| BootError::Internal(err.to_string()))?;
237        Ok(self
238            .with_body(body)
239            .with_header("content-type", "application/json"))
240    }
241
242    pub fn with_content_type(self, content_type: impl Into<String>) -> Self {
243        self.with_header("content-type", content_type)
244    }
245
246    pub fn with_content_length(self, content_length: u64) -> Self {
247        self.with_header("content-length", content_length.to_string())
248    }
249
250    pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
251        self.headers
252            .insert(normalize_header_name(name), value.into());
253        self
254    }
255
256    pub fn with_headers(mut self, headers: BTreeMap<String, String>) -> Self {
257        self.headers = normalize_headers(headers);
258        self
259    }
260
261    pub fn append_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
262        self.appended_headers
263            .push((normalize_header_name(name), value.into()));
264        self
265    }
266
267    pub fn header_entries(&self) -> impl Iterator<Item = (&str, &str)> {
268        self.headers
269            .iter()
270            .map(|(name, value)| (name.as_str(), value.as_str()))
271            .chain(
272                self.appended_headers
273                    .iter()
274                    .map(|(name, value)| (name.as_str(), value.as_str())),
275            )
276    }
277
278    pub fn validate_headers(&self) -> Result<()> {
279        for (name, value) in self.header_entries() {
280            validate_request_header(name, value)?;
281        }
282
283        Ok(())
284    }
285
286    pub fn text(&self) -> Result<String> {
287        String::from_utf8(self.body.clone()).map_err(|err| BootError::BadRequest(err.to_string()))
288    }
289
290    pub fn param(&self, name: &str) -> Option<&str> {
291        self.params.get(name).map(String::as_str)
292    }
293
294    pub fn param_as<T>(&self, name: &str) -> Result<T>
295    where
296        T: FromStr,
297        T::Err: fmt::Display,
298    {
299        parse_required_value(
300            self.param(name).map(ToString::to_string),
301            "path parameter",
302            name,
303        )
304    }
305
306    pub fn optional_param_as<T>(&self, name: &str) -> Result<Option<T>>
307    where
308        T: FromStr,
309        T::Err: fmt::Display,
310    {
311        parse_optional_value(
312            self.param(name).map(ToString::to_string),
313            "path parameter",
314            name,
315        )
316    }
317
318    pub fn host_param(&self, name: &str) -> Option<&str> {
319        self.host_params.get(name).map(String::as_str)
320    }
321
322    pub fn host_param_as<T>(&self, name: &str) -> Result<T>
323    where
324        T: FromStr,
325        T::Err: fmt::Display,
326    {
327        parse_required_value(
328            self.host_param(name).map(ToString::to_string),
329            "host parameter",
330            name,
331        )
332    }
333
334    pub fn optional_host_param_as<T>(&self, name: &str) -> Result<Option<T>>
335    where
336        T: FromStr,
337        T::Err: fmt::Display,
338    {
339        parse_optional_value(
340            self.host_param(name).map(ToString::to_string),
341            "host parameter",
342            name,
343        )
344    }
345
346    pub fn params<T>(&self) -> Result<T>
347    where
348        T: DeserializeOwned,
349    {
350        let params = serde_urlencoded::to_string(&self.params)
351            .map_err(|err| BootError::BadRequest(err.to_string()))?;
352        serde_urlencoded::from_str(&params).map_err(|err| BootError::BadRequest(err.to_string()))
353    }
354
355    pub fn validated_params<T>(&self) -> Result<T>
356    where
357        T: DeserializeOwned + Validate,
358    {
359        let value = self.params()?;
360        validate_value(value)
361    }
362
363    pub fn host_params<T>(&self) -> Result<T>
364    where
365        T: DeserializeOwned,
366    {
367        let params = serde_urlencoded::to_string(&self.host_params)
368            .map_err(|err| BootError::BadRequest(err.to_string()))?;
369        serde_urlencoded::from_str(&params).map_err(|err| BootError::BadRequest(err.to_string()))
370    }
371
372    pub fn validated_host_params<T>(&self) -> Result<T>
373    where
374        T: DeserializeOwned + Validate,
375    {
376        let value = self.host_params()?;
377        validate_value(value)
378    }
379
380    pub fn query_param(&self, name: &str) -> Option<&str> {
381        self.query.get(name).map(String::as_str)
382    }
383
384    pub fn query_value(&self, name: &str) -> Result<Option<String>> {
385        Ok(self
386            .query_pairs()?
387            .into_iter()
388            .find_map(|(key, value)| (key == name).then_some(value)))
389    }
390
391    pub fn query_value_as<T>(&self, name: &str) -> Result<T>
392    where
393        T: FromStr,
394        T::Err: fmt::Display,
395    {
396        parse_required_value(self.query_value(name)?, "query parameter", name)
397    }
398
399    pub fn optional_query_value_as<T>(&self, name: &str) -> Result<Option<T>>
400    where
401        T: FromStr,
402        T::Err: fmt::Display,
403    {
404        parse_optional_value(self.query_value(name)?, "query parameter", name)
405    }
406
407    pub fn query_values(&self, name: &str) -> Result<Vec<String>> {
408        Ok(self
409            .query_pairs()?
410            .into_iter()
411            .filter_map(|(key, value)| (key == name).then_some(value))
412            .collect())
413    }
414
415    pub fn query_values_as<T>(&self, name: &str) -> Result<Vec<T>>
416    where
417        T: FromStr,
418        T::Err: fmt::Display,
419    {
420        self.query_values(name)?
421            .into_iter()
422            .map(|value| parse_value(value, "query parameter", name))
423            .collect()
424    }
425
426    pub fn query_pairs(&self) -> Result<Vec<(String, String)>> {
427        match self.query_string.as_deref() {
428            Some(query) => parse_query_pairs(query),
429            None => Ok(self
430                .query
431                .iter()
432                .map(|(key, value)| (key.clone(), value.clone()))
433                .collect()),
434        }
435    }
436
437    pub fn header(&self, name: &str) -> Option<&str> {
438        get_header(&self.headers, name)
439    }
440
441    pub fn header_as<T>(&self, name: &str) -> Result<T>
442    where
443        T: FromStr,
444        T::Err: fmt::Display,
445    {
446        parse_required_value(self.header(name).map(ToString::to_string), "header", name)
447    }
448
449    pub fn optional_header_as<T>(&self, name: &str) -> Result<Option<T>>
450    where
451        T: FromStr,
452        T::Err: fmt::Display,
453    {
454        parse_optional_value(self.header(name).map(ToString::to_string), "header", name)
455    }
456
457    pub fn host(&self) -> Option<&str> {
458        self.header("host").and_then(normalize_host_header)
459    }
460
461    pub fn ip(&self) -> Option<String> {
462        self.forwarded_for_ip()
463            .or_else(|| self.forwarded_header_ip("x-forwarded-for"))
464            .or_else(|| self.forwarded_header_ip("x-real-ip"))
465    }
466
467    pub fn ip_as<T>(&self) -> Result<T>
468    where
469        T: FromStr,
470        T::Err: fmt::Display,
471    {
472        parse_required_value(self.ip(), "IP address", "ip")
473    }
474
475    pub fn optional_ip_as<T>(&self) -> Result<Option<T>>
476    where
477        T: FromStr,
478        T::Err: fmt::Display,
479    {
480        parse_optional_value(self.ip(), "IP address", "ip")
481    }
482
483    pub fn header_values(&self, name: &str) -> Vec<&str> {
484        let mut values = self.header(name).into_iter().collect::<Vec<_>>();
485        values.extend(
486            self.appended_headers
487                .iter()
488                .filter(|(key, _)| key.eq_ignore_ascii_case(name))
489                .map(|(_, value)| value.as_str()),
490        );
491        values
492    }
493
494    pub fn authorization(&self) -> Option<&str> {
495        self.header_values("authorization").into_iter().next()
496    }
497
498    fn forwarded_header_ip(&self, name: &str) -> Option<String> {
499        self.header_values(name)
500            .into_iter()
501            .flat_map(|value| value.split(','))
502            .map(str::trim)
503            .find(|value| !value.is_empty())
504            .map(ToString::to_string)
505    }
506
507    fn forwarded_for_ip(&self) -> Option<String> {
508        self.header_values("forwarded")
509            .into_iter()
510            .flat_map(|value| value.split(','))
511            .flat_map(|entry| entry.split(';'))
512            .filter_map(|part| part.trim().split_once('='))
513            .find_map(|(key, value)| {
514                key.trim().eq_ignore_ascii_case("for").then(|| {
515                    value
516                        .trim()
517                        .trim_matches('"')
518                        .trim_matches(['[', ']'])
519                        .to_string()
520                })
521            })
522            .filter(|value| !value.is_empty())
523    }
524
525    pub fn bearer_token(&self) -> Option<&str> {
526        let authorization = self.authorization()?.trim();
527        let mut parts = authorization.splitn(2, char::is_whitespace);
528        let scheme = parts.next()?;
529        let token = parts.next()?.trim();
530
531        if scheme.eq_ignore_ascii_case("bearer") && !token.is_empty() {
532            Some(token)
533        } else {
534            None
535        }
536    }
537
538    pub fn require_bearer_token(&self) -> Result<&str> {
539        self.bearer_token()
540            .ok_or_else(|| BootError::Unauthorized("missing bearer token".to_string()))
541    }
542
543    pub fn cookie_pairs(&self) -> Result<Vec<(String, String)>> {
544        parse_cookie_header_values(&self.header_values("cookie"))
545    }
546
547    pub fn cookie(&self, name: &str) -> Result<Option<String>> {
548        Ok(self
549            .cookie_pairs()?
550            .into_iter()
551            .find_map(|(key, value)| (key == name).then_some(value)))
552    }
553
554    pub fn require_cookie(&self, name: &str) -> Result<String> {
555        self.cookie(name)?
556            .ok_or_else(|| BootError::Unauthorized(format!("missing cookie: {name}")))
557    }
558
559    pub fn cookie_values(&self, name: &str) -> Result<Vec<String>> {
560        Ok(self
561            .cookie_pairs()?
562            .into_iter()
563            .filter_map(|(key, value)| (key == name).then_some(value))
564            .collect())
565    }
566
567    pub fn cookies(&self) -> Result<BTreeMap<String, String>> {
568        let mut cookies = BTreeMap::new();
569        for (name, value) in self.cookie_pairs()? {
570            cookies.entry(name).or_insert(value);
571        }
572        Ok(cookies)
573    }
574
575    pub fn content_type(&self) -> Option<&str> {
576        self.header_values("content-type").into_iter().next()
577    }
578
579    pub fn content_length(&self) -> Result<Option<u64>> {
580        let Some(content_length) = self.header_values("content-length").into_iter().next() else {
581            return Ok(None);
582        };
583
584        parse_content_length(content_length)
585            .map(Some)
586            .ok_or_else(|| {
587                BootError::BadRequest(format!("invalid content-length header: {content_length}"))
588            })
589    }
590
591    pub fn strict_content_length(&self) -> Result<Option<u64>> {
592        strict_content_length_values(
593            self.header_values("content-length"),
594            |content_length| {
595                BootError::BadRequest(format!("invalid content-length header: {content_length}"))
596            },
597            |expected_content_length, content_length| {
598                BootError::BadRequest(format!(
599                    "conflicting content-length headers: {expected_content_length} != {content_length}"
600                ))
601            },
602        )
603    }
604
605    pub fn validate_content_length(&self) -> Result<()> {
606        let Some(content_length) = self.strict_content_length()? else {
607            return Ok(());
608        };
609        let actual_body_length = self.body.len() as u64;
610        if actual_body_length == content_length {
611            return Ok(());
612        }
613
614        Err(BootError::BadRequest(format!(
615            "content-length header does not match request body length: expected {content_length}, got {actual_body_length}"
616        )))
617    }
618
619    pub fn validate_body_limit(&self, body_limit: usize) -> Result<()> {
620        if self
621            .strict_content_length()?
622            .is_some_and(|content_length| content_length > body_limit as u64)
623            || self.body.len() > body_limit
624        {
625            return Err(BootError::PayloadTooLarge(format!(
626                "request body exceeds {body_limit} bytes"
627            )));
628        }
629
630        Ok(())
631    }
632
633    pub fn validate(&self) -> Result<()> {
634        self.validate_headers()?;
635        self.validate_content_length()
636    }
637
638    pub fn validate_with_body_limit(&self, body_limit: usize) -> Result<()> {
639        self.validate_headers()?;
640        self.validate_body_limit(body_limit)?;
641        self.validate_content_length()
642    }
643
644    pub fn is_content_type(&self, media_type: &str) -> bool {
645        self.content_type()
646            .is_some_and(|content_type| matches_media_type(content_type, media_type))
647    }
648
649    pub fn is_json_content_type(&self) -> bool {
650        self.content_type().is_some_and(is_json_media_type)
651    }
652
653    pub fn require_json_content_type(&self) -> Result<()> {
654        if self.is_json_content_type() {
655            return Ok(());
656        }
657
658        let message = match self.content_type() {
659            Some(content_type) => format!("expected JSON content type, got {content_type}"),
660            None => "expected JSON content type".to_string(),
661        };
662        Err(BootError::UnsupportedMediaType(message))
663    }
664
665    pub fn accepts_json(&self) -> bool {
666        accepts_json_response(&self.header_values("accept"))
667    }
668
669    pub fn require_accepts_json(&self) -> Result<()> {
670        if self.accepts_json() {
671            return Ok(());
672        }
673
674        Err(BootError::NotAcceptable(
675            "expected client to accept JSON response".to_string(),
676        ))
677    }
678
679    pub fn accepts_event_stream(&self) -> bool {
680        accepts_event_stream_response(&self.header_values("accept"))
681    }
682
683    pub fn require_accepts_event_stream(&self) -> Result<()> {
684        if self.accepts_event_stream() {
685            return Ok(());
686        }
687
688        Err(BootError::NotAcceptable(
689            "expected client to accept text/event-stream response".to_string(),
690        ))
691    }
692
693    pub fn json<T>(&self) -> Result<T>
694    where
695        T: DeserializeOwned,
696    {
697        serde_json::from_slice(&self.body).map_err(|err| BootError::BadRequest(err.to_string()))
698    }
699
700    pub fn validated_json<T>(&self) -> Result<T>
701    where
702        T: DeserializeOwned + Validate,
703    {
704        let value = self.json()?;
705        validate_value(value)
706    }
707
708    pub fn json_with_content_type<T>(&self) -> Result<T>
709    where
710        T: DeserializeOwned,
711    {
712        self.require_json_content_type()?;
713        self.json()
714    }
715
716    pub fn validated_json_with_content_type<T>(&self) -> Result<T>
717    where
718        T: DeserializeOwned + Validate,
719    {
720        self.require_json_content_type()?;
721        self.validated_json()
722    }
723
724    pub fn query<T>(&self) -> Result<T>
725    where
726        T: DeserializeOwned,
727    {
728        let query = match self.query_string.as_deref() {
729            Some(query) => {
730                validate_percent_encoding(query)?;
731                query.to_string()
732            }
733            None => serde_urlencoded::to_string(&self.query)
734                .map_err(|err| BootError::BadRequest(err.to_string()))?,
735        };
736        serde_urlencoded::from_str(&query).map_err(|err| BootError::BadRequest(err.to_string()))
737    }
738
739    pub fn validated_query<T>(&self) -> Result<T>
740    where
741        T: DeserializeOwned + Validate,
742    {
743        let value = self.query()?;
744        validate_value(value)
745    }
746}
747
748fn parse_required_value<T>(value: Option<String>, label: &str, name: &str) -> Result<T>
749where
750    T: FromStr,
751    T::Err: fmt::Display,
752{
753    let Some(value) = value else {
754        return Err(BootError::BadRequest(format!("missing {label}: {name}")));
755    };
756    parse_value(value, label, name)
757}
758
759fn parse_optional_value<T>(value: Option<String>, label: &str, name: &str) -> Result<Option<T>>
760where
761    T: FromStr,
762    T::Err: fmt::Display,
763{
764    value
765        .map(|value| parse_value(value, label, name))
766        .transpose()
767}
768
769fn parse_value<T>(value: String, label: &str, name: &str) -> Result<T>
770where
771    T: FromStr,
772    T::Err: fmt::Display,
773{
774    value
775        .parse::<T>()
776        .map_err(|error| BootError::BadRequest(format!("invalid {label} {name}: {error}")))
777}
778
779fn validate_request_header(name: &str, value: &str) -> Result<()> {
780    validate_header_name(name).map_err(|message| {
781        BootError::BadRequest(format!("invalid request header name {name:?}: {message}"))
782    })?;
783    validate_header_value(value).map_err(|message| {
784        BootError::BadRequest(format!(
785            "invalid request header value for {name:?}: {message}"
786        ))
787    })
788}