ttpkit_auth/digest/
challenge.rs

1use std::{
2    borrow::Cow,
3    fmt::{self, Display, Formatter},
4    str::FromStr,
5};
6
7use ttpkit::header::HeaderFieldValue;
8
9use crate::{
10    AuthChallenge, DisplayEscaped, Error,
11    digest::{DigestAlgorithm, QualityOfProtection, response::DigestResponseBuilder},
12};
13
14/// Digest challenge builder.
15pub struct DigestChallengeBuilder {
16    realm: String,
17    nonce: Option<String>,
18    qops: Vec<QualityOfProtection>,
19    algorithm: DigestAlgorithm,
20    emit_md5: bool,
21    opaque: Option<String>,
22}
23
24impl DigestChallengeBuilder {
25    /// Create a new Digest challenge builder for a given realm.
26    fn new<T>(realm: T) -> Self
27    where
28        T: Into<String>,
29    {
30        Self {
31            realm: realm.into(),
32            nonce: None,
33            qops: Vec::new(),
34            algorithm: DigestAlgorithm::Md5,
35            emit_md5: true,
36            opaque: None,
37        }
38    }
39
40    /// Set the nonce value.
41    ///
42    /// If not set, a random nonce will be generated.
43    pub fn nonce<T>(mut self, nonce: T) -> Self
44    where
45        T: Into<String>,
46    {
47        self.nonce = Some(nonce.into());
48        self
49    }
50
51    /// Set the quality of protection values.
52    ///
53    /// If empty, no qop parameter will be included in the challenge. The
54    /// default value is an empty list.
55    pub fn qops<I>(mut self, qops: I) -> Self
56    where
57        I: IntoIterator<Item = QualityOfProtection>,
58    {
59        self.qops = Vec::from_iter(qops);
60        self
61    }
62
63    /// Set the digest algorithm.
64    ///
65    /// The default value is MD5.
66    #[inline]
67    pub fn algorithm(mut self, algorithm: DigestAlgorithm) -> Self {
68        self.algorithm = algorithm;
69        self
70    }
71
72    /// Set whether to emit the `algorithm=MD5` parameter in the challenge.
73    ///
74    /// The default value is `true`.
75    #[inline]
76    pub fn emit_md5(mut self, emit_md5: bool) -> Self {
77        self.emit_md5 = emit_md5;
78        self
79    }
80
81    /// Set the opaque parameter.
82    pub fn opaque<T>(mut self, opaque: T) -> Self
83    where
84        T: Into<String>,
85    {
86        self.opaque = Some(opaque.into());
87        self
88    }
89
90    /// Build the Digest challenge.
91    pub fn build(self) -> DigestChallenge {
92        let nonce = self
93            .nonce
94            .unwrap_or_else(|| format!("{:016x}", rand::random::<u64>()));
95
96        let algorithm_param = if !self.emit_md5 && self.algorithm.is_md5() {
97            None
98        } else {
99            Some(Cow::Borrowed(self.algorithm.name()))
100        };
101
102        DigestChallenge {
103            realm: self.realm,
104            nonce,
105            algorithm_param,
106            algorithm: self.algorithm,
107            qops: self.qops,
108            opaque: self.opaque,
109        }
110    }
111}
112
113/// Digest challenge.
114#[derive(Clone)]
115pub struct DigestChallenge {
116    realm: String,
117    nonce: String,
118    algorithm_param: Option<Cow<'static, str>>,
119    algorithm: DigestAlgorithm,
120    qops: Vec<QualityOfProtection>,
121    opaque: Option<String>,
122}
123
124impl DigestChallenge {
125    /// Get a Digest challenge builder for a given realm.
126    pub fn builder<T>(realm: T) -> DigestChallengeBuilder
127    where
128        T: Into<String>,
129    {
130        DigestChallengeBuilder::new(realm)
131    }
132
133    /// Get the realm.
134    #[inline]
135    pub fn realm(&self) -> &str {
136        &self.realm
137    }
138
139    /// Get the nonce.
140    #[inline]
141    pub fn nonce(&self) -> &str {
142        &self.nonce
143    }
144
145    /// Get the algorithm parameter as sent in the challenge.
146    #[inline]
147    pub fn algorithm_param(&self) -> Option<&str> {
148        self.algorithm_param.as_deref()
149    }
150
151    /// Get the actual digest algorithm.
152    #[inline]
153    pub fn algorithm(&self) -> DigestAlgorithm {
154        self.algorithm
155    }
156
157    /// Get the quality of protection values.
158    #[inline]
159    pub fn qops(&self) -> &[QualityOfProtection] {
160        &self.qops
161    }
162
163    /// Get the opaque parameter.
164    #[inline]
165    pub fn opaque(&self) -> Option<&str> {
166        self.opaque.as_deref()
167    }
168
169    /// Get a Digest response builder for this challenge.
170    #[inline]
171    pub fn response_builder(&self) -> DigestResponseBuilder<'_> {
172        DigestResponseBuilder::new(self)
173    }
174}
175
176impl Display for DigestChallenge {
177    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
178        let realm = DisplayEscaped::new(&self.realm);
179        let nonce = DisplayEscaped::new(&self.nonce);
180
181        write!(f, "Digest realm=\"{realm}\", nonce=\"{nonce}\"")?;
182
183        if let Some(algorithm) = self.algorithm_param.as_deref() {
184            write!(f, ", algorithm={algorithm}")?;
185        }
186
187        if !self.qops.is_empty() {
188            write!(f, ", qop=\"")?;
189
190            let mut qops = self.qops.iter();
191
192            if let Some(qop) = qops.next() {
193                write!(f, "{qop}")?;
194            }
195
196            for qop in qops {
197                write!(f, ", {qop}")?;
198            }
199
200            write!(f, "\"")?;
201        }
202
203        if let Some(opaque) = &self.opaque {
204            write!(f, ", opaque=\"{}\"", DisplayEscaped::new(opaque))?;
205        }
206
207        Ok(())
208    }
209}
210
211impl TryFrom<&AuthChallenge> for DigestChallenge {
212    type Error = Error;
213
214    fn try_from(challenge: &AuthChallenge) -> Result<Self, Self::Error> {
215        // helper function
216        fn pick_algorithm(algorithm: &str) -> Result<(&str, DigestAlgorithm), Error> {
217            // NOTE: The specification does not allow multiple algorithms in
218            //   the challenge, but some servers send multiple algorithms
219            //   separated by a comma. We will try to pick the first one that
220            //   we support.
221            for alg in algorithm.split(',') {
222                let alg = alg.trim();
223
224                if let Ok(res) = DigestAlgorithm::from_str(alg) {
225                    return Ok((alg, res));
226                }
227            }
228
229            DigestAlgorithm::from_str(algorithm).map(|res| (algorithm, res))
230        }
231
232        if challenge.scheme() != "digest" {
233            return Err(Error::from_static_msg("not a Digest challenge"));
234        }
235
236        let mut realm = None;
237        let mut nonce = None;
238        let mut algorithm_param = None;
239        let mut qop_param = None;
240        let mut opaque = None;
241
242        for param in challenge.params() {
243            let value = param.value();
244
245            match param.name() {
246                "realm" => realm = Some(value.into()),
247                "nonce" => nonce = Some(value.into()),
248                "algorithm" => algorithm_param = Some(Cow::Owned(value.into())),
249                "qop" => qop_param = Some(value),
250                "opaque" => opaque = Some(value.into()),
251                _ => (),
252            }
253        }
254
255        let realm =
256            realm.ok_or_else(|| Error::from_static_msg("the realm parameter is missing"))?;
257
258        let nonce =
259            nonce.ok_or_else(|| Error::from_static_msg("the nonce parameter is missing"))?;
260
261        let (algorithm_param, algorithm) = algorithm_param
262            .as_deref()
263            .map(pick_algorithm)
264            .transpose()?
265            .map(|(param, alg)| (Some(Cow::Owned(param.into())), alg))
266            .unwrap_or((None, DigestAlgorithm::Md5));
267
268        let qops = qop_param
269            .map(QualityOfProtection::parse_many)
270            .unwrap_or_else(Vec::new);
271
272        if qop_param.is_some() && qops.is_empty() {
273            return Err(Error::from_static_msg("unknown/unsupported qop values"));
274        }
275
276        let res = Self {
277            realm,
278            nonce,
279            algorithm_param,
280            algorithm,
281            qops,
282            opaque,
283        };
284
285        Ok(res)
286    }
287}
288
289impl From<DigestChallenge> for HeaderFieldValue {
290    fn from(challenge: DigestChallenge) -> Self {
291        HeaderFieldValue::from(challenge.to_string())
292    }
293}