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