ttpkit_auth/digest/
challenge.rs1use 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
14pub 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 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 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 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 #[inline]
67 pub fn algorithm(mut self, algorithm: DigestAlgorithm) -> Self {
68 self.algorithm = algorithm;
69 self
70 }
71
72 #[inline]
76 pub fn emit_md5(mut self, emit_md5: bool) -> Self {
77 self.emit_md5 = emit_md5;
78 self
79 }
80
81 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 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#[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 pub fn builder<T>(realm: T) -> DigestChallengeBuilder
127 where
128 T: Into<String>,
129 {
130 DigestChallengeBuilder::new(realm)
131 }
132
133 #[inline]
135 pub fn realm(&self) -> &str {
136 &self.realm
137 }
138
139 #[inline]
141 pub fn nonce(&self) -> &str {
142 &self.nonce
143 }
144
145 #[inline]
147 pub fn algorithm_param(&self) -> Option<&str> {
148 self.algorithm_param.as_deref()
149 }
150
151 #[inline]
153 pub fn algorithm(&self) -> DigestAlgorithm {
154 self.algorithm
155 }
156
157 #[inline]
159 pub fn qops(&self) -> &[QualityOfProtection] {
160 &self.qops
161 }
162
163 #[inline]
165 pub fn opaque(&self) -> Option<&str> {
166 self.opaque.as_deref()
167 }
168
169 #[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 fn pick_algorithm(algorithm: &str) -> Result<(&str, DigestAlgorithm), Error> {
217 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}