1use std::{convert::TryFrom, fmt::Write as _, io::Write as _};
8
9use digest::Digest;
10
11use crate::{
12 char_classes, ChallengeRef, ParamValue, PasswordParams, C_ATTR, C_ESCAPABLE, C_QDTEXT,
13};
14
15#[derive(Copy, Clone, Debug)]
19#[repr(u8)]
20#[non_exhaustive]
21pub enum Qop {
22 Auth = 1,
24
25 AuthInt = 2,
29}
30
31impl Qop {
32 fn as_str(self) -> &'static str {
34 match self {
35 Qop::Auth => "auth",
36 Qop::AuthInt => "auth-int",
37 }
38 }
39}
40
41#[derive(Copy, Clone, PartialEq, Eq)]
43pub struct QopSet(u8);
44
45impl std::fmt::Debug for QopSet {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 let mut l = f.debug_set();
48 if (self.0 & Qop::Auth as u8) != 0 {
49 l.entry(&"auth");
50 }
51 if (self.0 & Qop::AuthInt as u8) != 0 {
52 l.entry(&"auth-int");
53 }
54 l.finish()
55 }
56}
57
58impl std::ops::BitAnd<Qop> for QopSet {
59 type Output = bool;
60
61 fn bitand(self, rhs: Qop) -> Self::Output {
62 (self.0 & (rhs as u8)) != 0
63 }
64}
65
66#[derive(Eq, PartialEq)]
119pub struct DigestClient {
120 buf: Box<str>,
131
132 domain_start: u16,
136 opaque_start: u16,
137 nonce_start: u16,
138
139 algorithm: Algorithm,
141 session: bool,
142 stale: bool,
143 rfc2069_compat: bool,
144 userhash: bool,
145 qop: QopSet,
146 nc: u32,
147}
148
149impl DigestClient {
150 #[inline]
160 pub fn realm(&self) -> &str {
161 &self.buf[..self.domain_start as usize]
162 }
163
164 #[inline]
170 pub fn domain(&self) -> &str {
171 &self.buf[self.domain_start as usize..self.opaque_start as usize]
172 }
173
174 #[inline]
177 pub fn nonce(&self) -> &str {
178 &self.buf[self.nonce_start as usize..]
179 }
180
181 #[inline]
187 pub fn opaque(&self) -> Option<&str> {
188 if self.opaque_start == self.nonce_start {
189 None
190 } else {
191 Some(&self.buf[self.opaque_start as usize..self.nonce_start as usize])
192 }
193 }
194
195 #[inline]
198 pub fn stale(&self) -> bool {
199 self.stale
200 }
201
202 #[inline]
208 pub fn rfc2069_compat(&self) -> bool {
209 self.rfc2069_compat
210 }
211
212 #[inline]
214 pub fn algorithm(&self) -> Algorithm {
215 self.algorithm
216 }
217
218 #[inline]
220 pub fn session(&self) -> bool {
221 self.session
222 }
223
224 #[inline]
226 pub fn qop(&self) -> QopSet {
227 self.qop
228 }
229
230 #[inline]
233 pub fn nonce_count(&self) -> u32 {
234 self.nc
235 }
236
237 #[inline]
242 pub fn respond(&mut self, p: &PasswordParams) -> Result<String, String> {
243 self.respond_inner(p, &new_random_cnonce())
244 }
245
246 #[inline]
251 pub fn respond_with_testing_cnonce(
252 &mut self,
253 p: &PasswordParams,
254 cnonce: &str,
255 ) -> Result<String, String> {
256 self.respond_inner(p, cnonce)
257 }
258
259 fn respond_inner(&mut self, p: &PasswordParams, cnonce: &str) -> Result<String, String> {
265 let realm = self.realm();
266 let mut h_a1 = self.algorithm.h(&[
267 p.username.as_bytes(),
268 b":",
269 realm.as_bytes(),
270 b":",
271 p.password.as_bytes(),
272 ]);
273 if self.session {
274 h_a1 = self.algorithm.h(&[
275 h_a1.as_bytes(),
276 b":",
277 self.nonce().as_bytes(),
278 b":",
279 cnonce.as_bytes(),
280 ]);
281 }
282
283 let (h_a2, qop);
286 if let (Some(body), true) = (p.body, self.qop & Qop::AuthInt) {
287 h_a2 = self
288 .algorithm
289 .h(&[p.method.as_bytes(), b":", p.uri.as_bytes(), b":", body]);
290 qop = Qop::AuthInt;
291 } else if self.qop & Qop::Auth {
292 h_a2 = self
293 .algorithm
294 .h(&[p.method.as_bytes(), b":", p.uri.as_bytes()]);
295 qop = Qop::Auth;
296 } else {
297 return Err("no supported/available qop".into());
298 }
299
300 let nc = self.nc.checked_add(1).ok_or("nonce count exhausted")?;
301 let mut hex_nc = [0u8; 8];
302 let _ = write!(&mut hex_nc[..], "{:08x}", nc);
303 let str_hex_nc = match std::str::from_utf8(&hex_nc[..]) {
304 Ok(h) => h,
305 Err(_) => unreachable!(),
306 };
307
308 let response = if self.rfc2069_compat {
310 self.algorithm.h(&[
311 h_a1.as_bytes(),
312 b":",
313 self.nonce().as_bytes(),
314 b":",
315 h_a2.as_bytes(),
316 ])
317 } else {
318 self.algorithm.h(&[
319 h_a1.as_bytes(),
320 b":",
321 self.nonce().as_bytes(),
322 b":",
323 &hex_nc[..],
324 b":",
325 cnonce.as_bytes(),
326 b":",
327 qop.as_str().as_bytes(),
328 b":",
329 h_a2.as_bytes(),
330 ])
331 };
332
333 let mut out = String::with_capacity(128);
334 out.push_str("Digest ");
335 if self.userhash {
336 let hashed = self
337 .algorithm
338 .h(&[p.username.as_bytes(), b":", realm.as_bytes()]);
339 append_quoted_key_value(&mut out, "username", &hashed)?;
340 append_unquoted_key_value(&mut out, "userhash", "true");
341 } else if is_valid_quoted_value(p.username) {
342 append_quoted_key_value(&mut out, "username", p.username)?;
343 } else {
344 append_extended_key_value(&mut out, "username", p.username);
345 }
346 append_quoted_key_value(&mut out, "realm", self.realm())?;
347 append_quoted_key_value(&mut out, "uri", p.uri)?;
348 append_quoted_key_value(&mut out, "nonce", self.nonce())?;
349 if !self.rfc2069_compat {
350 append_unquoted_key_value(&mut out, "algorithm", self.algorithm.as_str(self.session));
351 append_unquoted_key_value(&mut out, "nc", str_hex_nc);
352 append_quoted_key_value(&mut out, "cnonce", cnonce)?;
353 append_unquoted_key_value(&mut out, "qop", qop.as_str());
354 }
355 append_quoted_key_value(&mut out, "response", &response)?;
356 if let Some(o) = self.opaque() {
357 append_quoted_key_value(&mut out, "opaque", o)?;
358 }
359 out.truncate(out.len() - 2); self.nc = nc;
361 Ok(out)
362 }
363}
364
365impl TryFrom<&ChallengeRef<'_>> for DigestClient {
366 type Error = String;
367
368 fn try_from(value: &ChallengeRef<'_>) -> Result<Self, Self::Error> {
369 if !value.scheme.eq_ignore_ascii_case("Digest") {
370 return Err(format!(
371 "DigestClientContext doesn't support challenge scheme {:?}",
372 value.scheme
373 ));
374 }
375 let mut buf_len = 0;
376 let mut unused_len = 0;
377 let mut realm = None;
378 let mut domain = None;
379 let mut nonce = None;
380 let mut opaque = None;
381 let mut stale = false;
382 let mut algorithm_and_session = None;
383 let mut qop_str = None;
384 let mut userhash_str = None;
385
386 for (k, v) in &value.params {
389 if store_param(k, v, "realm", &mut realm, &mut buf_len)?
394 || store_param(k, v, "domain", &mut domain, &mut buf_len)?
395 || store_param(k, v, "nonce", &mut nonce, &mut buf_len)?
396 || store_param(k, v, "opaque", &mut opaque, &mut buf_len)?
397 || store_param(k, v, "qop", &mut qop_str, &mut unused_len)?
398 || store_param(k, v, "userhash", &mut userhash_str, &mut unused_len)?
399 {
400 } else if k.eq_ignore_ascii_case("stale") {
402 stale = v.escaped.eq_ignore_ascii_case("true");
403 } else if k.eq_ignore_ascii_case("algorithm") {
404 algorithm_and_session = Some(Algorithm::parse(v.escaped)?);
405 }
406 }
407 let realm = realm.ok_or("missing required parameter realm")?;
408 let nonce = nonce.ok_or("missing required parameter nonce")?;
409 if buf_len > u16::MAX as usize {
410 return Err(format!(
412 "Unescaped parameters' length {} exceeds u16::MAX!",
413 buf_len
414 ));
415 }
416
417 let algorithm_and_session = algorithm_and_session.unwrap_or((Algorithm::Md5, false));
418
419 let mut buf = String::with_capacity(buf_len);
420 let mut qop = QopSet(0);
421 let rfc2069_compat = if let Some(qop_str) = qop_str {
422 let qop_str = qop_str.unescaped_with_scratch(&mut buf);
423 for v in qop_str.split(',') {
424 let v = v.trim();
425 if v.eq_ignore_ascii_case("auth") {
426 qop.0 |= Qop::Auth as u8;
427 } else if v.eq_ignore_ascii_case("auth-int") {
428 qop.0 |= Qop::AuthInt as u8;
429 }
430 }
431 if qop.0 == 0 {
432 return Err(format!("no supported qop in {:?}", qop_str));
433 }
434 buf.clear();
435 false
436 } else {
437 qop.0 |= Qop::Auth as u8;
440 true
441 };
442 let userhash;
443 if let Some(userhash_str) = userhash_str {
444 let userhash_str = userhash_str.unescaped_with_scratch(&mut buf);
445 userhash = userhash_str.eq_ignore_ascii_case("true");
446 buf.clear();
447 } else {
448 userhash = false;
449 };
450 realm.append_unescaped(&mut buf);
451 let domain_start = buf.len();
452 if let Some(d) = domain {
453 d.append_unescaped(&mut buf);
454 }
455 let opaque_start = buf.len();
456 if let Some(o) = opaque {
457 o.append_unescaped(&mut buf);
458 }
459 let nonce_start = buf.len();
460 nonce.append_unescaped(&mut buf);
461 Ok(DigestClient {
462 buf: buf.into_boxed_str(),
463 domain_start: domain_start as u16,
464 opaque_start: opaque_start as u16,
465 nonce_start: nonce_start as u16,
466 algorithm: algorithm_and_session.0,
467 session: algorithm_and_session.1,
468 stale,
469 rfc2069_compat,
470 userhash,
471 qop,
472 nc: 0,
473 })
474 }
475}
476
477impl std::fmt::Debug for DigestClient {
478 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
479 f.debug_struct("DigestClient")
480 .field("realm", &self.realm())
481 .field("domain", &self.domain())
482 .field("opaque", &self.opaque())
483 .field("nonce", &self.nonce())
484 .field("algorithm", &self.algorithm.as_str(self.session))
485 .field("stale", &self.stale)
486 .field("qop", &self.qop)
487 .field("rfc2069_compat", &self.rfc2069_compat)
488 .field("userhash", &self.userhash)
489 .field("nc", &self.nc)
490 .finish()
491 }
492}
493
494#[inline(never)]
496fn store_param<'v, 'tmp>(
497 k: &'tmp str,
498 v: &'v ParamValue<'v>,
499 expected_k: &'tmp str,
500 set_v: &'tmp mut Option<&'v ParamValue<'v>>,
501 add_len: &'tmp mut usize,
502) -> Result<bool, String> {
503 if !k.eq_ignore_ascii_case(expected_k) {
504 return Ok(false);
505 }
506 if set_v.is_some() {
507 return Err(format!("duplicate parameter {:?}", k));
508 }
509 *add_len += v.unescaped_len();
510 *set_v = Some(v);
511 Ok(true)
512}
513
514fn is_valid_quoted_value(s: &str) -> bool {
515 for &b in s.as_bytes() {
516 if char_classes(b) & (C_QDTEXT | C_ESCAPABLE) == 0 {
517 return false;
518 }
519 }
520 true
521}
522
523fn append_extended_key_value(out: &mut String, key: &str, value: &str) {
524 out.push_str(key);
525 out.push_str("*=UTF-8''");
526 for &b in value.as_bytes() {
527 if (char_classes(b) & C_ATTR) != 0 {
528 out.push(char::from(b));
529 } else {
530 let _ = write!(out, "%{:02X}", b);
531 }
532 }
533 out.push_str(", ");
534}
535
536#[inline(never)]
537fn append_unquoted_key_value(out: &mut String, key: &str, value: &str) {
538 out.push_str(key);
539 out.push('=');
540 out.push_str(value);
541 out.push_str(", ");
542}
543
544#[inline(never)]
545fn append_quoted_key_value(out: &mut String, key: &str, value: &str) -> Result<(), String> {
546 out.push_str(key);
547 out.push_str("=\"");
548 let mut first_unwritten = 0;
549 let bytes = value.as_bytes();
550 for (i, &b) in bytes.iter().enumerate() {
551 let class = char_classes(b);
554 if (class & C_QDTEXT) != 0 {
555 } else if (class & C_ESCAPABLE) != 0 {
557 out.push_str(&value[first_unwritten..i]);
558 out.push('\\');
559 out.push(char::from(b));
560 first_unwritten = i + 1;
561 } else {
562 return Err(format!("invalid {} value {:?}", key, value));
563 }
564 }
565 out.push_str(&value[first_unwritten..]);
566 out.push_str("\", ");
567 Ok(())
568}
569
570#[derive(Copy, Clone, Debug, Eq, PartialEq)]
576#[non_exhaustive]
577pub enum Algorithm {
578 Md5,
579 Sha256,
580 Sha512Trunc256,
581}
582
583impl Algorithm {
584 fn parse(s: &str) -> Result<(Self, bool), String> {
587 Ok(match s {
588 "MD5" => (Algorithm::Md5, false),
589 "MD5-sess" => (Algorithm::Md5, true),
590 "SHA-256" => (Algorithm::Sha256, false),
591 "SHA-256-sess" => (Algorithm::Sha256, true),
592 "SHA-512-256" => (Algorithm::Sha512Trunc256, false),
593 "SHA-512-256-sess" => (Algorithm::Sha512Trunc256, true),
594 _ => return Err(format!("unknown algorithm {:?}", s)),
595 })
596 }
597
598 #[inline(never)]
599 fn as_str(&self, session: bool) -> &'static str {
600 match (self, session) {
601 (Algorithm::Md5, false) => "MD5",
602 (Algorithm::Md5, true) => "MD5-sess",
603 (Algorithm::Sha256, false) => "SHA-256",
604 (Algorithm::Sha256, true) => "SHA-256-sess",
605 (Algorithm::Sha512Trunc256, false) => "SHA-512-256",
606 (Algorithm::Sha512Trunc256, true) => "SHA-512-256-sess",
607 }
608 }
609
610 #[inline(never)]
611 fn h(&self, items: &[&[u8]]) -> String {
612 match self {
613 Algorithm::Md5 => h(md5::Md5::new(), items),
614 Algorithm::Sha256 => h(sha2::Sha256::new(), items),
615 Algorithm::Sha512Trunc256 => h(sha2::Sha512_256::new(), items),
616 }
617 }
618}
619
620fn h<D: Digest>(mut d: D, items: &[&[u8]]) -> String {
621 for i in items {
622 d.update(i);
623 }
624 hex::encode(d.finalize())
625}
626
627fn new_random_cnonce() -> String {
628 let raw: [u8; 16] = rand::random();
629 hex::encode(&raw[..])
630}
631
632#[cfg(test)]
633mod tests {
634 use super::*;
635 use pretty_assertions::assert_eq;
636
637 #[test]
640 fn sha256_and_md5() {
641 let www_authenticate = "\
642 Digest \
643 realm=\"http-auth@example.org\", \
644 qop=\"auth, auth-int\", \
645 algorithm=SHA-256, \
646 nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\", \
647 opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\", \
648 Digest \
649 realm=\"http-auth@example.org\", \
650 qop=\"auth, auth-int\", \
651 algorithm=MD5, \
652 nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\", \
653 opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\"";
654 let challenges = dbg!(crate::parse_challenges(www_authenticate).unwrap());
655 assert_eq!(challenges.len(), 2);
656 let ctxs: Result<Vec<_>, _> = challenges.iter().map(DigestClient::try_from).collect();
657 let mut ctxs = dbg!(ctxs.unwrap());
658 assert_eq!(ctxs[1].realm(), "http-auth@example.org");
659 assert_eq!(ctxs[1].domain(), "");
660 assert_eq!(
661 ctxs[1].nonce(),
662 "7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v"
663 );
664 assert_eq!(
665 ctxs[1].opaque(),
666 Some("FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS")
667 );
668 assert_eq!(ctxs[1].stale(), false);
669 assert_eq!(ctxs[1].algorithm(), Algorithm::Md5);
670 assert_eq!(ctxs[1].qop().0, (Qop::Auth as u8) | (Qop::AuthInt as u8));
671 assert_eq!(ctxs[1].nonce_count(), 0);
672 let params = crate::PasswordParams {
673 username: "Mufasa",
674 password: "Circle of Life",
675 uri: "/dir/index.html",
676 body: None,
677 method: "GET",
678 };
679 assert_eq!(
680 &mut ctxs[0]
681 .respond_with_testing_cnonce(
682 ¶ms,
683 "f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ"
684 )
685 .unwrap(),
686 "Digest username=\"Mufasa\", \
687 realm=\"http-auth@example.org\", \
688 uri=\"/dir/index.html\", \
689 nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\", \
690 algorithm=SHA-256, \
691 nc=00000001, \
692 cnonce=\"f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ\", \
693 qop=auth, \
694 response=\"753927fa0e85d155564e2e272a28d1802ca10daf4496794697cf8db5856cb6c1\", \
695 opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\""
696 );
697 assert_eq!(ctxs[0].nc, 1);
698 assert_eq!(
699 &mut ctxs[1]
700 .respond_with_testing_cnonce(
701 ¶ms,
702 "f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ"
703 )
704 .unwrap(),
705 "Digest username=\"Mufasa\", \
706 realm=\"http-auth@example.org\", \
707 uri=\"/dir/index.html\", \
708 nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\", \
709 algorithm=MD5, \
710 nc=00000001, \
711 cnonce=\"f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ\", \
712 qop=auth, \
713 response=\"8ca523f5e9506fed4657c9700eebdbec\", \
714 opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\""
715 );
716 assert_eq!(ctxs[1].nc, 1);
717 }
718
719 #[test]
724 fn md5_sess() {
725 let www_authenticate = "\
726 Digest \
727 realm=\"http-auth@example.org\", \
728 qop=\"auth, auth-int\", \
729 algorithm=MD5-sess, \
730 nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\", \
731 opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\"";
732 let challenges = dbg!(crate::parse_challenges(www_authenticate).unwrap());
733 assert_eq!(challenges.len(), 1);
734 let ctxs: Result<Vec<_>, _> = challenges.iter().map(DigestClient::try_from).collect();
735 let mut ctxs = dbg!(ctxs.unwrap());
736 assert_eq!(ctxs[0].realm(), "http-auth@example.org");
737 assert_eq!(ctxs[0].domain(), "");
738 assert_eq!(
739 ctxs[0].nonce(),
740 "7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v"
741 );
742 assert_eq!(
743 ctxs[0].opaque(),
744 Some("FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS")
745 );
746 assert_eq!(ctxs[0].stale(), false);
747 assert_eq!(ctxs[0].algorithm(), Algorithm::Md5);
748 assert_eq!(ctxs[0].session(), true);
749 assert_eq!(ctxs[0].qop().0, (Qop::Auth as u8) | (Qop::AuthInt as u8));
750 assert_eq!(ctxs[0].nonce_count(), 0);
751 let params = crate::PasswordParams {
752 username: "Mufasa",
753 password: "Circle of Life",
754 uri: "/dir/index.html",
755 body: None,
756 method: "GET",
757 };
758 assert_eq!(
759 &mut ctxs[0]
760 .respond_with_testing_cnonce(
761 ¶ms,
762 "f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ"
763 )
764 .unwrap(),
765 "Digest username=\"Mufasa\", \
766 realm=\"http-auth@example.org\", \
767 uri=\"/dir/index.html\", \
768 nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\", \
769 algorithm=MD5-sess, \
770 nc=00000001, \
771 cnonce=\"f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ\", \
772 qop=auth, \
773 response=\"e783283f46242139c486a698fec7211d\", \
774 opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\""
775 );
776 assert_eq!(ctxs[0].nc, 1);
777 }
778
779 #[test]
782 fn sha512_256_charset() {
783 let www_authenticate = "\
784 Digest \
785 realm=\"api@example.org\", \
786 qop=\"auth\", \
787 algorithm=SHA-512-256, \
788 nonce=\"5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK\", \
789 opaque=\"HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS\", \
790 charset=UTF-8, \
791 userhash=true";
792 let challenges = dbg!(crate::parse_challenges(www_authenticate).unwrap());
793 assert_eq!(challenges.len(), 1);
794 let ctxs: Result<Vec<_>, _> = challenges.iter().map(DigestClient::try_from).collect();
795 let mut ctxs = dbg!(ctxs.unwrap());
796 assert_eq!(ctxs.len(), 1);
797 assert_eq!(ctxs[0].realm(), "api@example.org");
798 assert_eq!(ctxs[0].domain(), "");
799 assert_eq!(
800 ctxs[0].nonce(),
801 "5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK"
802 );
803 assert_eq!(
804 ctxs[0].opaque(),
805 Some("HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS")
806 );
807 assert_eq!(ctxs[0].stale, false);
808 assert_eq!(ctxs[0].userhash, true);
809 assert_eq!(ctxs[0].algorithm, Algorithm::Sha512Trunc256);
810 assert_eq!(ctxs[0].qop.0, Qop::Auth as u8);
811 assert_eq!(ctxs[0].nc, 0);
812 let params = crate::PasswordParams {
813 username: "J\u{E4}s\u{F8}n Doe",
814 password: "Secret, or not?",
815 uri: "/doe.json",
816 body: None,
817 method: "GET",
818 };
819
820 assert_eq!(
823 &mut ctxs[0]
824 .respond_with_testing_cnonce(
825 ¶ms,
826 "NTg6RKcb9boFIAS3KrFK9BGeh+iDa/sm6jUMp2wds69v"
827 )
828 .unwrap(),
829 "\
830 Digest \
831 username=\"793263caabb707a56211940d90411ea4a575adeccb7e360aeb624ed06ece9b0b\", \
832 userhash=true, \
833 realm=\"api@example.org\", \
834 uri=\"/doe.json\", \
835 nonce=\"5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK\", \
836 algorithm=SHA-512-256, \
837 nc=00000001, \
838 cnonce=\"NTg6RKcb9boFIAS3KrFK9BGeh+iDa/sm6jUMp2wds69v\", \
839 qop=auth, \
840 response=\"3798d4131c277846293534c3edc11bd8a5e4cdcbff78b05db9d95eeb1cec68a5\", \
841 opaque=\"HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS\""
842 );
843 assert_eq!(ctxs[0].nc, 1);
844 ctxs[0].userhash = false;
845 ctxs[0].nc = 0;
846 assert_eq!(
847 &mut ctxs[0]
848 .respond_with_testing_cnonce(
849 ¶ms,
850 "NTg6RKcb9boFIAS3KrFK9BGeh+iDa/sm6jUMp2wds69v"
851 )
852 .unwrap(),
853 "\
854 Digest \
855 username*=UTF-8''J%C3%A4s%C3%B8n%20Doe, \
856 realm=\"api@example.org\", \
857 uri=\"/doe.json\", \
858 nonce=\"5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK\", \
859 algorithm=SHA-512-256, \
860 nc=00000001, \
861 cnonce=\"NTg6RKcb9boFIAS3KrFK9BGeh+iDa/sm6jUMp2wds69v\", \
862 qop=auth, \
863 response=\"3798d4131c277846293534c3edc11bd8a5e4cdcbff78b05db9d95eeb1cec68a5\", \
864 opaque=\"HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS\""
865 );
866 assert_eq!(ctxs[0].nc, 1);
867 }
868
869 #[test]
870 fn rfc2069() {
871 let www_authenticate = "\
874 Digest \
875 realm=\"testrealm@host.com\", \
876 nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", \
877 opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"";
878 let challenges = dbg!(crate::parse_challenges(www_authenticate).unwrap());
879 assert_eq!(challenges.len(), 1);
880 let ctxs: Result<Vec<_>, _> = challenges.iter().map(DigestClient::try_from).collect();
881 let mut ctxs = dbg!(ctxs.unwrap());
882 assert_eq!(ctxs.len(), 1);
883 assert_eq!(ctxs[0].qop.0, Qop::Auth as u8);
884 assert_eq!(ctxs[0].rfc2069_compat, true);
885 let params = crate::PasswordParams {
886 username: "Mufasa",
887 password: "CircleOfLife",
888 uri: "/dir/index.html",
889 body: None,
890 method: "GET",
891 };
892 assert_eq!(
893 &mut ctxs[0]
894 .respond_with_testing_cnonce(¶ms, "unused")
895 .unwrap(),
896 "\
897 Digest \
898 username=\"Mufasa\", \
899 realm=\"testrealm@host.com\", \
900 uri=\"/dir/index.html\", \
901 nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", \
902 response=\"1949323746fe6a43ef61f9606e7febea\", \
903 opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"",
904 );
905 assert_eq!(ctxs[0].nc, 1);
906 }
907
908 #[test]
910 fn size() {
911 assert_eq!(
913 dbg!(std::mem::size_of::<DigestClient>()),
914 dbg!(std::mem::size_of::<Option<DigestClient>>()),
915 )
916 }
917}