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