1use rand::Rng;
2use std::collections::HashMap;
3use std::fmt::{self, Display, Formatter};
4use std::str::FromStr;
5
6use crate::enums::{Algorithm, AlgorithmType, Charset, HttpMethod, Qop, QopAlgo};
7
8use crate::{Error::*, Result};
9use std::borrow::Cow;
10
11trait QuoteForDigest {
13 fn quote_for_digest(&self) -> String;
14}
15
16impl QuoteForDigest for &str {
17 fn quote_for_digest(&self) -> String {
18 self.to_string().quote_for_digest()
19 }
20}
21
22impl<'a> QuoteForDigest for Cow<'a, str> {
23 fn quote_for_digest(&self) -> String {
24 self.as_ref().quote_for_digest()
25 }
26}
27
28impl QuoteForDigest for String {
29 fn quote_for_digest(&self) -> String {
30 self.replace("\\", "\\\\").replace("\"", "\\\"")
31 }
32}
33
34fn join_vec<T: ToString>(vec: &[T], sep: &str) -> String {
36 vec.iter()
37 .map(ToString::to_string)
38 .collect::<Vec<_>>()
39 .join(sep)
40}
41
42enum NamedTag<'a> {
43 Quoted(&'a str, Cow<'a, str>),
44 Plain(&'a str, Cow<'a, str>),
45}
46
47impl Display for NamedTag<'_> {
48 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
49 match self {
50 NamedTag::Quoted(name, content) => {
51 write!(f, "{}=\"{}\"", name, content.quote_for_digest())
52 }
53 NamedTag::Plain(name, content) => write!(f, "{}={}", name, content),
54 }
55 }
56}
57
58fn parse_header_map(input: &str) -> Result<HashMap<String, String>> {
60 #[derive(Debug)]
61 #[allow(non_camel_case_types)]
62 enum ParserState {
63 P_WHITE,
64 P_NAME(usize),
65 P_VALUE_BEGIN,
66 P_VALUE_QUOTED,
67 P_VALUE_QUOTED_NEXTLITERAL,
68 P_VALUE_PLAIN,
69 }
70
71 let mut state = ParserState::P_WHITE;
72
73 let mut parsed = HashMap::<String, String>::new();
74 let mut current_token = None;
75 let mut current_value = String::new();
76
77 for (char_n, c) in input.chars().enumerate() {
78 match state {
79 ParserState::P_WHITE => {
80 if c.is_alphabetic() {
81 state = ParserState::P_NAME(char_n);
82 }
83 }
84 ParserState::P_NAME(name_start) => {
85 if c == '=' {
86 current_token = Some(&input[name_start..char_n]);
87 state = ParserState::P_VALUE_BEGIN;
88 }
89 }
90 ParserState::P_VALUE_BEGIN => {
91 current_value.clear();
92 state = match c {
93 '"' => ParserState::P_VALUE_QUOTED,
94 _ => {
95 current_value.push(c);
96 ParserState::P_VALUE_PLAIN
97 }
98 };
99 }
100 ParserState::P_VALUE_QUOTED => {
101 match c {
102 '"' => {
103 parsed.insert(current_token.unwrap().to_string(), current_value.clone());
104
105 current_token = None;
106 current_value.clear();
107
108 state = ParserState::P_WHITE;
109 }
110 '\\' => {
111 state = ParserState::P_VALUE_QUOTED_NEXTLITERAL;
112 }
113 _ => {
114 current_value.push(c);
115 }
116 };
117 }
118 ParserState::P_VALUE_PLAIN => {
119 if c == ',' || c.is_ascii_whitespace() {
120 parsed.insert(current_token.unwrap().to_string(), current_value.clone());
121
122 current_token = None;
123 current_value.clear();
124
125 state = ParserState::P_WHITE;
126 } else {
127 current_value.push(c);
128 }
129 }
130 ParserState::P_VALUE_QUOTED_NEXTLITERAL => {
131 current_value.push(c);
132 state = ParserState::P_VALUE_QUOTED
133 }
134 }
135 }
136
137 match state {
138 ParserState::P_VALUE_PLAIN => {
139 parsed.insert(current_token.unwrap().to_string(), current_value); }
141 ParserState::P_WHITE => {}
142 _ => return Err(InvalidHeaderSyntax(input.into())),
143 }
144
145 Ok(parsed)
146}
147
148#[derive(Debug)]
153pub struct AuthContext<'a> {
154 pub username: Cow<'a, str>,
156 pub password: Cow<'a, str>,
158 pub uri: Cow<'a, str>,
160 pub body: Option<Cow<'a, [u8]>>,
163 pub method: HttpMethod<'a>,
165 pub cnonce: Option<Cow<'a, str>>,
167}
168
169impl<'a> AuthContext<'a> {
170 pub fn new<UN, PW, UR>(username: UN, password: PW, uri: UR) -> Self
173 where
174 UN: Into<Cow<'a, str>>,
175 PW: Into<Cow<'a, str>>,
176 UR: Into<Cow<'a, str>>,
177 {
178 Self::new_with_method(
179 username,
180 password,
181 uri,
182 Option::<&'a [u8]>::None,
183 HttpMethod::GET,
184 )
185 }
186
187 pub fn new_post<UN, PW, UR, BD>(username: UN, password: PW, uri: UR, body: Option<BD>) -> Self
190 where
191 UN: Into<Cow<'a, str>>,
192 PW: Into<Cow<'a, str>>,
193 UR: Into<Cow<'a, str>>,
194 BD: Into<Cow<'a, [u8]>>,
195 {
196 Self::new_with_method(username, password, uri, body, HttpMethod::POST)
197 }
198
199 pub fn new_with_method<UN, PW, UR, BD>(
201 username: UN,
202 password: PW,
203 uri: UR,
204 body: Option<BD>,
205 method: HttpMethod<'a>,
206 ) -> Self
207 where
208 UN: Into<Cow<'a, str>>,
209 PW: Into<Cow<'a, str>>,
210 UR: Into<Cow<'a, str>>,
211 BD: Into<Cow<'a, [u8]>>,
212 {
213 Self {
214 username: username.into(),
215 password: password.into(),
216 uri: uri.into(),
217 body: body.map(Into::into),
218 method,
219 cnonce: None,
220 }
221 }
222
223 pub fn set_custom_cnonce<CN>(&mut self, cnonce: CN)
225 where
226 CN: Into<Cow<'a, str>>,
227 {
228 self.cnonce = Some(cnonce.into());
229 }
230}
231
232#[derive(Debug, PartialEq, Clone)]
234pub struct WwwAuthenticateHeader {
235 pub domain: Option<Vec<String>>,
237 pub realm: String,
239 pub nonce: String,
241 pub opaque: Option<String>,
243 pub stale: bool,
247 pub algorithm: Algorithm,
249 pub qop: Option<Vec<Qop>>,
251 pub userhash: bool,
253 pub charset: Charset,
255 pub nc: u32,
258}
259
260impl FromStr for WwwAuthenticateHeader {
261 type Err = crate::Error;
262
263 fn from_str(input: &str) -> Result<Self> {
265 Self::parse(input)
266 }
267}
268
269impl WwwAuthenticateHeader {
270 pub fn respond(&mut self, secrets: &AuthContext) -> Result<AuthorizationHeader> {
273 AuthorizationHeader::from_prompt(self, secrets)
274 }
275
276 pub fn parse(input: &str) -> Result<Self> {
281 let mut input = input.trim();
282
283 if input.starts_with("Digest") {
285 input = &input["Digest".len()..];
286 }
287
288 let mut kv = parse_header_map(input)?;
289
290 Ok(Self {
291 domain: if let Some(domains) = kv.get("domain") {
292 let domains: Vec<&str> = domains.split(' ').collect();
293 Some(domains.iter().map(|x| x.trim().to_string()).collect())
294 } else {
295 None
296 },
297 realm: match kv.remove("realm") {
298 Some(v) => v,
299 None => return Err(MissingRequired("realm", input.into())),
300 },
301 nonce: match kv.remove("nonce") {
302 Some(v) => v,
303 None => return Err(MissingRequired("nonce", input.into())),
304 },
305 opaque: kv.remove("opaque"),
306 stale: match kv.get("stale") {
307 Some(v) => &v.to_ascii_lowercase() == "true",
308 None => false,
309 },
310 charset: match kv.get("charset") {
311 Some(v) => Charset::from_str(v)?,
312 None => Charset::ASCII,
313 },
314 algorithm: match kv.get("algorithm") {
315 Some(a) => Algorithm::from_str(&a)?,
316 _ => Algorithm::default(),
317 },
318 qop: if let Some(domains) = kv.get("qop") {
319 let domains: Vec<&str> = domains.split(',').collect();
320 let mut qops = vec![];
321 for d in domains {
322 qops.push(Qop::from_str(d.trim())?);
323 }
324 Some(qops)
325 } else {
326 None
327 },
328 userhash: match kv.get("userhash") {
329 Some(v) => &v.to_ascii_lowercase() == "true",
330 None => false,
331 },
332 nc: 0,
333 })
334 }
335}
336
337impl Display for WwwAuthenticateHeader {
338 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
339 let mut entries = Vec::<NamedTag>::new();
340
341 f.write_str("Digest ")?;
342
343 entries.push(NamedTag::Quoted("realm", (&self.realm).into()));
344
345 if let Some(ref qops) = self.qop {
346 entries.push(NamedTag::Quoted("qop", join_vec(qops, ", ").into()));
347 }
348
349 if let Some(ref domains) = self.domain {
350 entries.push(NamedTag::Quoted("domain", join_vec(domains, " ").into()));
351 }
352
353 if self.stale {
354 entries.push(NamedTag::Plain("stale", "true".into()));
355 }
356
357 entries.push(NamedTag::Plain(
358 "algorithm",
359 self.algorithm.to_string().into(),
360 ));
361 entries.push(NamedTag::Quoted("nonce", (&self.nonce).into()));
362 if let Some(ref opaque) = self.opaque {
363 entries.push(NamedTag::Quoted("opaque", (opaque).into()));
364 }
365 entries.push(NamedTag::Plain("charset", self.charset.to_string().into()));
366
367 if self.userhash {
368 entries.push(NamedTag::Plain("userhash", "true".into()));
369 }
370
371 for (i, e) in entries.iter().enumerate() {
372 if i > 0 {
373 f.write_str(", ")?;
374 }
375 f.write_str(&e.to_string())?;
376 }
377
378 Ok(())
379 }
380}
381
382#[derive(Debug, PartialEq, Clone)]
388pub struct AuthorizationHeader {
389 pub realm: String,
391 pub nonce: String,
393 pub opaque: Option<String>,
395 pub userhash: bool,
397 pub algorithm: Algorithm,
399 pub response: String,
401 pub username: String,
403 pub uri: String,
405 pub qop: Option<Qop>,
408 pub cnonce: Option<String>,
411 pub nc: u32,
414}
415
416impl AuthorizationHeader {
417 pub fn from_prompt(
430 prompt: &mut WwwAuthenticateHeader,
431 context: &AuthContext,
432 ) -> Result<AuthorizationHeader> {
433 let qop = match &prompt.qop {
434 None => None,
435 Some(vec) => {
436 if vec.contains(&Qop::AUTH_INT) {
438 Some(Qop::AUTH_INT)
439 } else if vec.contains(&Qop::AUTH) {
440 Some(Qop::AUTH)
442 } else {
443 return Err(BadQopOptions(join_vec(vec, ", ")));
445 }
446 }
447 };
448
449 prompt.nc += 1;
450
451 let mut hdr = AuthorizationHeader {
452 realm: prompt.realm.clone(),
453 nonce: prompt.nonce.clone(),
454 opaque: prompt.opaque.clone(),
455 userhash: prompt.userhash,
456 algorithm: prompt.algorithm,
457 response: String::default(),
458 username: String::default(),
459 uri: context.uri.as_ref().into(),
460 qop,
461 cnonce: context
462 .cnonce
463 .as_ref()
464 .map(AsRef::as_ref)
465 .map(ToOwned::to_owned), nc: prompt.nc,
467 };
468
469 hdr.digest(context);
470
471 Ok(hdr)
472 }
473
474 pub fn digest(&mut self, context: &AuthContext) {
488 let qop_algo = match self.qop {
490 None => QopAlgo::NONE,
491 Some(Qop::AUTH_INT) => {
492 if let Some(b) = &context.body {
493 QopAlgo::AUTH_INT(b.as_ref())
494 } else {
495 QopAlgo::AUTH
497 }
498 }
499 Some(Qop::AUTH) => QopAlgo::AUTH,
500 };
501
502 let h = &self.algorithm;
503
504 let cnonce = {
505 match &self.cnonce {
506 Some(cnonce) => cnonce.to_owned(),
507 None => {
508 let mut rng = rand::thread_rng();
509 let nonce_bytes: [u8; 16] = rng.gen();
510
511 hex::encode(nonce_bytes)
512 }
513 }
514 };
515
516 let a1 = {
518 let a = format!(
519 "{name}:{realm}:{pw}",
520 name = context.username,
521 realm = self.realm,
522 pw = context.password
523 );
524
525 let sess = self.algorithm.sess;
526 if sess {
527 format!(
528 "{hash}:{nonce}:{cnonce}",
529 hash = h.hash(a.as_bytes()),
530 nonce = self.nonce,
531 cnonce = cnonce
532 )
533 } else {
534 a
535 }
536 };
537
538 let a2 = match qop_algo {
540 QopAlgo::AUTH | QopAlgo::NONE => {
541 format!("{method}:{uri}", method = context.method, uri = context.uri)
542 }
543 QopAlgo::AUTH_INT(body) => format!(
544 "{method}:{uri}:{bodyhash}",
545 method = context.method,
546 uri = context.uri,
547 bodyhash = h.hash(body)
548 ),
549 };
550
551 let username = if self.userhash {
553 h.hash(
554 format!(
555 "{username}:{realm}",
556 username = context.username,
557 realm = self.realm
558 )
559 .as_bytes(),
560 )
561 } else {
562 context.username.as_ref().to_owned()
563 };
564
565 let qop: Option<Qop> = qop_algo.into();
566
567 let ha1 = h.hash_str(&a1);
568 let ha2 = h.hash_str(&a2);
569
570 self.response = match &qop {
571 Some(q) => {
572 let tmp = format!(
573 "{ha1}:{nonce}:{nc:08x}:{cnonce}:{qop}:{ha2}",
574 ha1 = ha1,
575 nonce = self.nonce,
576 nc = self.nc,
577 cnonce = cnonce,
578 qop = q,
579 ha2 = ha2
580 );
581 h.hash(tmp.as_bytes())
582 }
583 None => {
584 let tmp = format!(
585 "{ha1}:{nonce}:{ha2}",
586 ha1 = ha1,
587 nonce = self.nonce,
588 ha2 = ha2
589 );
590 h.hash(tmp.as_bytes())
591 }
592 };
593
594 self.qop = qop;
595 self.username = username;
596 self.cnonce = qop.map(|_| cnonce);
597 }
598
599 pub fn to_header_string(&self) -> String {
601 self.to_string()
602 }
603
604 pub fn parse(input: &str) -> Result<Self> {
609 let mut input = input.trim();
610
611 if input.starts_with("Digest") {
613 input = &input["Digest".len()..];
614 }
615
616 let mut kv = parse_header_map(input)?;
617
618 let mut auth = Self {
619 username: match kv.remove("username") {
620 Some(v) => v,
621 None => return Err(MissingRequired("username", input.into())),
622 },
623 realm: match kv.remove("realm") {
624 Some(v) => v,
625 None => return Err(MissingRequired("realm", input.into())),
626 },
627 nonce: match kv.remove("nonce") {
628 Some(v) => v,
629 None => return Err(MissingRequired("nonce", input.into())),
630 },
631 uri: match kv.remove("uri") {
632 Some(v) => v,
633 None => return Err(MissingRequired("uri", input.into())),
634 },
635 response: match kv.remove("response") {
636 Some(v) => v,
637 None => return Err(MissingRequired("response", input.into())),
638 },
639 qop: kv.remove("qop").map(|s| Qop::from_str(&s)).transpose()?,
640 nc: match kv.remove("nc") {
641 Some(v) => u32::from_str_radix(&v, 16)?,
642 None => 1,
643 },
644 cnonce: kv.remove("cnonce"),
645 opaque: kv.remove("opaque"),
646 algorithm: match kv.get("algorithm") {
647 Some(a) => Algorithm::from_str(&a)?,
648 _ => Algorithm::default(),
649 },
650 userhash: match kv.get("userhash") {
651 Some(v) => &v.to_ascii_lowercase() == "true",
652 None => false,
653 },
654 };
655
656 if auth.qop.is_some() {
657 if auth.cnonce.is_none() {
658 return Err(MissingRequired("cnonce", input.into()));
659 }
660 } else {
661 auth.cnonce = None;
663 }
664
665 Ok(auth)
666 }
667}
668
669impl Display for AuthorizationHeader {
670 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
671 let mut entries = Vec::<NamedTag>::new();
672
673 f.write_str("Digest ")?;
674
675 entries.push(NamedTag::Quoted("username", (&self.username).into()));
676 entries.push(NamedTag::Quoted("realm", (&self.realm).into()));
677 entries.push(NamedTag::Quoted("nonce", (&self.nonce).into()));
678 entries.push(NamedTag::Quoted("uri", (&self.uri).into()));
679
680 if self.qop.is_some() && self.cnonce.is_some() {
681 entries.push(NamedTag::Plain(
682 "qop",
683 self.qop.as_ref().unwrap().to_string().into(),
684 ));
685 entries.push(NamedTag::Plain("nc", format!("{:08x}", self.nc).into()));
686 entries.push(NamedTag::Quoted(
687 "cnonce",
688 self.cnonce.as_ref().unwrap().into(),
689 ));
690 }
691
692 entries.push(NamedTag::Quoted("response", (&self.response).into()));
693
694 if let Some(opaque) = &self.opaque {
695 entries.push(NamedTag::Quoted("opaque", opaque.into()));
696 }
697
698 if self.qop.is_some() || self.algorithm.algo != AlgorithmType::MD5 {
700 entries.push(NamedTag::Plain(
701 "algorithm",
702 self.algorithm.to_string().into(),
703 ));
704 }
705
706 if self.userhash {
707 entries.push(NamedTag::Plain("userhash", "true".into()));
708 }
709
710 for (i, e) in entries.iter().enumerate() {
711 if i > 0 {
712 f.write_str(", ")?;
713 }
714 f.write_str(&e.to_string())?;
715 }
716
717 Ok(())
718 }
719}
720
721impl FromStr for AuthorizationHeader {
722 type Err = crate::Error;
723
724 fn from_str(input: &str) -> Result<Self> {
726 Self::parse(input)
727 }
728}
729
730#[cfg(test)]
731mod tests {
732 use super::parse_header_map;
733 use super::Algorithm;
734 use super::AlgorithmType;
735 use super::AuthorizationHeader;
736 use super::Charset;
737 use super::Qop;
738 use super::WwwAuthenticateHeader;
739 use crate::digest::AuthContext;
740 use std::str::FromStr;
741
742 #[test]
743 fn test_parse_header_map() {
744 let src = r#"
745 realm="api@example.org",
746 qop="auth",
747 algorithm=SHA-512-256,
748 nonce="5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK",
749 opaque="HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS",
750 charset=UTF-8,
751 userhash=true
752 "#;
753
754 let map = parse_header_map(src).unwrap();
755
756 assert_eq!(map.get("realm").unwrap(), "api@example.org");
757 assert_eq!(map.get("qop").unwrap(), "auth");
758 assert_eq!(map.get("algorithm").unwrap(), "SHA-512-256");
759 assert_eq!(
760 map.get("nonce").unwrap(),
761 "5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK"
762 );
763 assert_eq!(
764 map.get("opaque").unwrap(),
765 "HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS"
766 );
767 assert_eq!(map.get("charset").unwrap(), "UTF-8");
768 assert_eq!(map.get("userhash").unwrap(), "true");
769 }
770
771 #[test]
772 fn test_parse_header_map2() {
773 let src = r#"realm="api@example.org""#;
774 let map = parse_header_map(src).unwrap();
775 assert_eq!(map.get("realm").unwrap(), "api@example.org");
776 }
777
778 #[test]
779 fn test_parse_header_map3() {
780 let src = r#"realm=api@example.org"#;
781 let map = parse_header_map(src).unwrap();
782 assert_eq!(map.get("realm").unwrap(), "api@example.org");
783 }
784
785 #[test]
786 fn test_parse_header_map4() {
787 {
788 let src = "";
789 let map = parse_header_map(src).unwrap();
790 assert_eq!(map.is_empty(), true);
791 }
792 }
793
794 #[test]
795 fn test_www_hdr_parse() {
796 let src = r#"
798 realm="api@example.org",
799 qop="auth",
800 domain="/my/nice/url /login /logout",
801 algorithm=SHA-512-256,
802 nonce="5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK",
803 opaque="HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS",
804 charset=UTF-8,
805 userhash=true
806 "#;
807
808 let parsed = WwwAuthenticateHeader::from_str(src).unwrap();
809
810 assert_eq!(
811 parsed,
812 WwwAuthenticateHeader {
813 domain: Some(vec![
814 "/my/nice/url".to_string(),
815 "/login".to_string(),
816 "/logout".to_string(),
817 ]),
818 realm: "api@example.org".to_string(),
819 nonce: "5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK".to_string(),
820 opaque: Some("HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS".to_string()),
821 stale: false,
822 algorithm: Algorithm::new(AlgorithmType::SHA2_512_256, false),
823 qop: Some(vec![Qop::AUTH]),
824 userhash: true,
825 charset: Charset::UTF8,
826 nc: 0,
827 }
828 )
829 }
830
831 #[test]
832 fn test_www_hdr_tostring() {
833 let mut hdr = WwwAuthenticateHeader {
834 domain: Some(vec![
835 "/my/nice/url".to_string(),
836 "/login".to_string(),
837 "/logout".to_string(),
838 ]),
839 realm: "api@example.org".to_string(),
840 nonce: "5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK".to_string(),
841 opaque: Some("HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS".to_string()),
842 stale: false,
843 algorithm: Algorithm::new(AlgorithmType::SHA2_512_256, false),
844 qop: Some(vec![Qop::AUTH]),
845 userhash: true,
846 charset: Charset::UTF8,
847 nc: 0,
848 };
849
850 assert_eq!(
851 r#"Digest realm="api@example.org",
852 qop="auth",
853 domain="/my/nice/url /login /logout",
854 algorithm=SHA-512-256,
855 nonce="5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK",
856 opaque="HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS",
857 charset=UTF-8,
858 userhash=true"#
859 .replace(",\n ", ", "),
860 hdr.to_string()
861 );
862
863 hdr.stale = true;
864 hdr.userhash = false;
865 hdr.opaque = None;
866 hdr.qop = None;
867
868 assert_eq!(
869 r#"Digest realm="api@example.org",
870 domain="/my/nice/url /login /logout",
871 stale=true,
872 algorithm=SHA-512-256,
873 nonce="5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK",
874 charset=UTF-8"#
875 .replace(",\n ", ", "),
876 hdr.to_string()
877 );
878
879 hdr.qop = Some(vec![Qop::AUTH, Qop::AUTH_INT]);
880
881 assert_eq!(
882 r#"Digest realm="api@example.org",
883 qop="auth, auth-int",
884 domain="/my/nice/url /login /logout",
885 stale=true,
886 algorithm=SHA-512-256,
887 nonce="5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK",
888 charset=UTF-8"#
889 .replace(",\n ", ", "),
890 hdr.to_string()
891 );
892 }
893
894 #[test]
895 fn test_www_hdr_parse2() {
896 let src = r#"
898 realm="a long realm with\\, weird \" characters",
899 qop="auth-int",
900 nonce="bla bla nonce aaaaa",
901 stale=TRUE
902 "#;
903
904 let parsed = WwwAuthenticateHeader::from_str(src).unwrap();
905
906 assert_eq!(
907 parsed,
908 WwwAuthenticateHeader {
909 domain: None,
910 realm: "a long realm with\\, weird \" characters".to_string(),
911 nonce: "bla bla nonce aaaaa".to_string(),
912 opaque: None,
913 stale: true,
914 algorithm: Algorithm::default(),
915 qop: Some(vec![Qop::AUTH_INT]),
916 userhash: false,
917 charset: Charset::ASCII,
918 nc: 0,
919 }
920 )
921 }
922
923 #[test]
924 fn test_www_hdr_parse3() {
925 let src = r#"Digest realm="aaa", nonce="bbb""#;
927
928 let parsed = WwwAuthenticateHeader::from_str(src).unwrap();
929
930 assert_eq!(
931 parsed,
932 WwwAuthenticateHeader {
933 domain: None,
934 realm: "aaa".to_string(),
935 nonce: "bbb".to_string(),
936 opaque: None,
937 stale: false,
938 algorithm: Algorithm::default(),
939 qop: None,
940 userhash: false,
941 charset: Charset::ASCII,
942 nc: 0,
943 }
944 )
945 }
946
947 #[test]
948 fn test_rfc2069() {
949 let src = r#"
950 Digest
951 realm="testrealm@host.com",
952 nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
953 opaque="5ccc069c403ebaf9f0171e9517f40e41"
954 "#;
955
956 let context = AuthContext::new("Mufasa", "CircleOfLife", "/dir/index.html");
957
958 let mut prompt = WwwAuthenticateHeader::from_str(src).unwrap();
959 let answer = AuthorizationHeader::from_prompt(&mut prompt, &context).unwrap();
960
961 let s = answer.to_string().replace(", ", ",\n ");
963 assert_eq!(
964 s,
965 r#"
966Digest username="Mufasa",
967 realm="testrealm@host.com",
968 nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
969 uri="/dir/index.html",
970 response="1949323746fe6a43ef61f9606e7febea",
971 opaque="5ccc069c403ebaf9f0171e9517f40e41"
972"#
973 .trim()
974 );
975
976 let parsed = AuthorizationHeader::parse(&s).unwrap();
978 assert_eq!(answer, parsed);
979 }
980
981 #[test]
982 fn test_rfc2617() {
983 let src = r#"
984 Digest
985 realm="testrealm@host.com",
986 qop="auth,auth-int",
987 nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
988 opaque="5ccc069c403ebaf9f0171e9517f40e41"
989 "#;
990
991 let mut context = AuthContext::new("Mufasa", "Circle Of Life", "/dir/index.html");
992 context.set_custom_cnonce("0a4f113b");
993
994 assert_eq!(context.body, None);
995
996 let mut prompt = WwwAuthenticateHeader::from_str(src).unwrap();
997 let answer = AuthorizationHeader::from_prompt(&mut prompt, &context).unwrap();
998
999 let s = answer.to_string().replace(", ", ",\n ");
1000 assert_eq!(
1003 s,
1004 r#"
1005Digest username="Mufasa",
1006 realm="testrealm@host.com",
1007 nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
1008 uri="/dir/index.html",
1009 qop=auth,
1010 nc=00000001,
1011 cnonce="0a4f113b",
1012 response="6629fae49393a05397450978507c4ef1",
1013 opaque="5ccc069c403ebaf9f0171e9517f40e41",
1014 algorithm=MD5
1015"#
1016 .trim()
1017 );
1018
1019 let parsed = AuthorizationHeader::parse(&s).unwrap();
1021 assert_eq!(answer, parsed);
1022 }
1023
1024 #[test]
1025 fn test_rfc7616_md5() {
1026 let src = r#"
1027 Digest
1028 realm="http-auth@example.org",
1029 qop="auth, auth-int",
1030 algorithm=MD5,
1031 nonce="7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v",
1032 opaque="FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS"
1033 "#;
1034
1035 let mut context = AuthContext::new("Mufasa", "Circle of Life", "/dir/index.html");
1036 context.set_custom_cnonce("f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ");
1037
1038 let mut prompt = WwwAuthenticateHeader::from_str(src).unwrap();
1039 let answer = AuthorizationHeader::from_prompt(&mut prompt, &context).unwrap();
1040
1041 let s = answer.to_string().replace(", ", ",\n ");
1042
1043 assert_eq!(
1044 s,
1045 r#"
1046Digest username="Mufasa",
1047 realm="http-auth@example.org",
1048 nonce="7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v",
1049 uri="/dir/index.html",
1050 qop=auth,
1051 nc=00000001,
1052 cnonce="f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ",
1053 response="8ca523f5e9506fed4657c9700eebdbec",
1054 opaque="FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS",
1055 algorithm=MD5
1056"#
1057 .trim()
1058 );
1059
1060 let parsed = AuthorizationHeader::parse(&s).unwrap();
1062 assert_eq!(answer, parsed);
1063 }
1064
1065 #[test]
1066 fn test_rfc7616_sha256() {
1067 let src = r#"
1068 Digest
1069 realm="http-auth@example.org",
1070 qop="auth, auth-int",
1071 algorithm=SHA-256,
1072 nonce="7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v",
1073 opaque="FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS"
1074 "#;
1075
1076 let mut context = AuthContext::new("Mufasa", "Circle of Life", "/dir/index.html");
1077 context.set_custom_cnonce("f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ");
1078 let mut prompt = WwwAuthenticateHeader::from_str(src).unwrap();
1090 let answer = AuthorizationHeader::from_prompt(&mut prompt, &context).unwrap();
1091
1092 let s = answer.to_string().replace(", ", ",\n ");
1093 assert_eq!(
1096 s,
1097 r#"
1098Digest username="Mufasa",
1099 realm="http-auth@example.org",
1100 nonce="7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v",
1101 uri="/dir/index.html",
1102 qop=auth,
1103 nc=00000001,
1104 cnonce="f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ",
1105 response="753927fa0e85d155564e2e272a28d1802ca10daf4496794697cf8db5856cb6c1",
1106 opaque="FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS",
1107 algorithm=SHA-256
1108"#
1109 .trim()
1110 );
1111
1112 let parsed = AuthorizationHeader::parse(&s).unwrap();
1114 assert_eq!(answer, parsed);
1115 }
1116}