http_authentication/schemes/basic/
challenge.rs

1use alloc::{boxed::Box, string::String, vec};
2
3use http_auth::ChallengeRef;
4
5use crate::{
6    schemes::NAME_BASIC as NAME, CHALLENGE_PARAM_REALM as PARAM_REALM, COMMA, D_Q_M, EQ_S, SP,
7};
8
9//
10const PARAM_CHARSET: &str = "charset";
11
12//
13#[derive(Debug, Clone)]
14pub struct Challenge {
15    pub realm: Box<str>,
16    pub charset: Option<Box<str>>,
17}
18
19impl Challenge {
20    pub fn new(realm: impl AsRef<str>) -> Self {
21        Self {
22            realm: realm.as_ref().into(),
23            charset: None,
24        }
25    }
26
27    fn internal_to_string(&self) -> String {
28        let mut s = String::with_capacity(30);
29        s.push_str(NAME);
30        s.push(SP);
31
32        s.push_str(PARAM_REALM);
33        s.push(EQ_S);
34        s.push(D_Q_M);
35        s.push_str(self.realm.as_ref());
36        s.push(D_Q_M);
37
38        let mut params = vec![];
39        if let Some(charset) = &self.charset {
40            params.push((PARAM_CHARSET, true, charset));
41        }
42
43        for (k, is_quoted, v) in params {
44            s.push(COMMA);
45            s.push(SP);
46            s.push_str(k);
47            s.push(EQ_S);
48            if is_quoted {
49                s.push(D_Q_M);
50            }
51            s.push_str(v.as_ref());
52            if is_quoted {
53                s.push(D_Q_M);
54            }
55        }
56
57        s
58    }
59}
60
61//
62// Ref https://github.com/scottlamb/http-auth/blob/v0.1.5/src/basic.rs#L69-L90
63//
64impl TryFrom<&ChallengeRef<'_>> for Challenge {
65    type Error = ChallengeParseError;
66
67    fn try_from(c: &ChallengeRef<'_>) -> Result<Self, Self::Error> {
68        if !c.scheme.eq_ignore_ascii_case(NAME) {
69            return Err(ChallengeParseError::SchemeMismatch);
70        }
71
72        let realm = c
73            .params
74            .iter()
75            .find(|(k, _)| k.eq_ignore_ascii_case(PARAM_REALM))
76            .map(|(_, v)| v.as_escaped().into());
77        //
78        // TODO, Optional
79        // Ref https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate#basic
80        //
81        let realm = realm.unwrap_or_default();
82
83        let charset = c
84            .params
85            .iter()
86            .find(|(k, _)| k.eq_ignore_ascii_case(PARAM_CHARSET))
87            .map(|(_, v)| v.as_escaped().into());
88
89        Ok(Self { realm, charset })
90    }
91}
92
93//
94#[derive(Debug)]
95pub enum ChallengeParseError {
96    SchemeMismatch,
97    Other(&'static str),
98}
99
100impl core::fmt::Display for ChallengeParseError {
101    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
102        write!(f, "{self:?}")
103    }
104}
105
106#[cfg(feature = "std")]
107impl std::error::Error for ChallengeParseError {}
108
109//
110impl core::fmt::Display for Challenge {
111    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
112        write!(f, "{}", self.internal_to_string())
113    }
114}
115
116//
117//
118//
119#[cfg(test)]
120pub(crate) const DEMO_CHALLENGE_STR_SIMPLE: &str = r#"Basic realm="foo""#;
121#[cfg(test)]
122pub(crate) const DEMO_CHALLENGE_STR: &str = r#"Basic realm="foo", charset="UTF-8""#;
123#[cfg(test)]
124pub(crate) const DEMO_CHALLENGE_REALM_STR: &str = "foo";
125#[cfg(test)]
126pub(crate) const DEMO_CHALLENGE_CHARSET_STR: &str = "UTF-8";
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    use alloc::string::ToString as _;
133
134    use http_auth::ParamValue;
135
136    #[test]
137    fn test_try_from_challenge_ref() {
138        let mut c = ChallengeRef::new(NAME);
139        c.params
140            .push((PARAM_REALM, ParamValue::try_from_escaped("foo").unwrap()));
141        c.params.push((
142            PARAM_CHARSET,
143            ParamValue::try_from_escaped("UTF-8").unwrap(),
144        ));
145
146        let c = Challenge::try_from(&c).unwrap();
147        assert_eq!(c.realm, "foo".into());
148        assert_eq!(c.charset, Some("UTF-8".into()));
149    }
150
151    #[test]
152    fn test_render() {
153        let mut c = Challenge::new(DEMO_CHALLENGE_REALM_STR);
154        assert_eq!(c.to_string(), DEMO_CHALLENGE_STR_SIMPLE);
155
156        c.charset = Some(DEMO_CHALLENGE_CHARSET_STR.into());
157        assert_eq!(c.to_string(), DEMO_CHALLENGE_STR);
158    }
159}