digest_auth/
digest.rs

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
11/// slash quoting for digest strings
12trait 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
34/// Join a Vec of Display items using a separator
35fn 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
58/// Helper func that parses the key-value string received from server
59fn 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); // consume the value here
140        }
141        ParserState::P_WHITE => {}
142        _ => return Err(InvalidHeaderSyntax(input.into())),
143    }
144
145    Ok(parsed)
146}
147
148/// Login attempt context
149///
150/// All fields are borrowed to reduce runtime overhead; this struct should not be stored anywhere,
151/// it is normally meaningful only for the one request.
152#[derive(Debug)]
153pub struct AuthContext<'a> {
154    /// Login username
155    pub username: Cow<'a, str>,
156    /// Login password (plain)
157    pub password: Cow<'a, str>,
158    /// Requested URI (not a domain! should start with a slash)
159    pub uri: Cow<'a, str>,
160    /// Request payload body - used for auth-int (auth with integrity check)
161    /// May be left out if not using auth-int
162    pub body: Option<Cow<'a, [u8]>>,
163    /// HTTP method used (defaults to GET)
164    pub method: HttpMethod<'a>,
165    /// Spoofed client nonce (use only for tests; a random nonce is generated automatically)
166    pub cnonce: Option<Cow<'a, str>>,
167}
168
169impl<'a> AuthContext<'a> {
170    /// Construct a new context with the GET verb and no payload body.
171    /// See the other constructors if this does not fit your situation.
172    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    /// Construct a new context with the POST verb and a payload body (may be None).
188    /// See the other constructors if this does not fit your situation.
189    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    /// Construct a new context with arbitrary verb and, optionally, a payload body
200    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    /// Set cnonce to the given value
224    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/// WWW-Authenticate header parsed from HTTP header value
233#[derive(Debug, PartialEq, Clone)]
234pub struct WwwAuthenticateHeader {
235    /// Domain is a list of URIs that will accept the same digest. None if not given (i.e applies to all)
236    pub domain: Option<Vec<String>>,
237    /// Authorization realm (i.e. hostname, serial number...)
238    pub realm: String,
239    /// Server nonce
240    pub nonce: String,
241    /// Server opaque string
242    pub opaque: Option<String>,
243    /// True if the server nonce expired.
244    /// This is sent in response to an auth attempt with an older digest.
245    /// The response should contain a new WWW-Authenticate header.
246    pub stale: bool,
247    /// Hashing algo
248    pub algorithm: Algorithm,
249    /// Digest algorithm variant
250    pub qop: Option<Vec<Qop>>,
251    /// Flag that the server supports user-hashes
252    pub userhash: bool,
253    /// Server-supported charset
254    pub charset: Charset,
255    /// nc - not part of the received header, but kept here for convenience and incremented each time
256    /// a response is composed with the same nonce.
257    pub nc: u32,
258}
259
260impl FromStr for WwwAuthenticateHeader {
261    type Err = crate::Error;
262
263    /// Parse HTTP header
264    fn from_str(input: &str) -> Result<Self> {
265        Self::parse(input)
266    }
267}
268
269impl WwwAuthenticateHeader {
270    /// Generate an [`AuthorizationHeader`](struct.AuthorizationHeader.html) to be sent to the server in a new request.
271    /// The [`self.nc`](struct.AuthorizationHeader.html#structfield.nc) field is incremented.
272    pub fn respond(&mut self, secrets: &AuthContext) -> Result<AuthorizationHeader> {
273        AuthorizationHeader::from_prompt(self, secrets)
274    }
275
276    /// Construct from the `WWW-Authenticate` header string
277    ///
278    /// # Errors
279    /// If the header is malformed (e.g. missing 'realm', missing a closing quote, unknown algorithm etc.)
280    pub fn parse(input: &str) -> Result<Self> {
281        let mut input = input.trim();
282
283        // Remove leading "Digest"
284        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/// Header sent back to the server, including password hashes.
383///
384/// This can be obtained by calling [`AuthorizationHeader::from_prompt()`](#method.from_prompt),
385/// or from the [`WwwAuthenticateHeader`](struct.WwwAuthenticateHeader.html) prompt struct
386/// with [`.respond()`](struct.WwwAuthenticateHeader.html#method.respond)
387#[derive(Debug, PartialEq, Clone)]
388pub struct AuthorizationHeader {
389    /// Authorization realm
390    pub realm: String,
391    /// Server nonce
392    pub nonce: String,
393    /// Server opaque
394    pub opaque: Option<String>,
395    /// Flag that userhash was used
396    pub userhash: bool,
397    /// Hash algorithm
398    pub algorithm: Algorithm,
399    /// Computed digest
400    pub response: String,
401    /// Username or hash (owned because of the computed hash)
402    pub username: String,
403    /// Requested URI
404    pub uri: String,
405    /// QOP chosen from the list offered by server, if any
406    /// None in legacy compat mode (RFC 2069)
407    pub qop: Option<Qop>,
408    /// Client nonce
409    /// None in legacy compat mode (RFC 2069)
410    pub cnonce: Option<String>,
411    /// How many requests have been signed with this server nonce
412    /// Not used in legacy compat mode (RFC 2069) - it's still incremented though
413    pub nc: u32,
414}
415
416impl AuthorizationHeader {
417    /// Construct using a parsed prompt header and an auth context, selecting suitable algorithm
418    /// options. The [`WwwAuthenticateHeader`](struct.WwwAuthenticateHeader.html) struct contains a
419    /// [`nc`](struct.WwwAuthenticateHeader.html#structfield.nc) field that is incremented by this
420    /// method.
421    ///
422    /// For subsequent requests, simply reuse the same [`WwwAuthenticateHeader`](struct.WwwAuthenticateHeader.html)
423    /// and - if the server supports nonce reuse - it will work automatically.
424    ///
425    /// # Errors
426    ///
427    /// Fails if the source header is malformed so much that we can't figure out
428    /// a proper response (e.g. given but invalid QOP options)
429    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                // this is at least RFC2617, qop was given
437                if vec.contains(&Qop::AUTH_INT) {
438                    Some(Qop::AUTH_INT)
439                } else if vec.contains(&Qop::AUTH) {
440                    // "auth" is the second best after "auth-int"
441                    Some(Qop::AUTH)
442                } else {
443                    // parser bug - prompt.qop should have been None
444                    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), // Will be generated if needed, if build_hash is set and this is None
466            nc: prompt.nc,
467        };
468
469        hdr.digest(context);
470
471        Ok(hdr)
472    }
473
474    /// Build the response digest from Auth Context.
475    ///
476    /// This function is used by client to fill the Authorization header.
477    /// It can be used by server using a known password to replicate the hash
478    /// and then compare "response".
479    ///
480    /// This function sets cnonce if it was None before, or reuses it.
481    ///
482    /// Fields updated in the Authorization header:
483    /// - qop (if it was auth-int before but no body was given in context)
484    /// - cnonce (if it was None before)
485    /// - username copied from context
486    /// - response
487    pub fn digest(&mut self, context: &AuthContext) {
488        // figure out which QOP option to use
489        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                    // fallback
496                    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        // a1 value for the hash algo. cnonce is generated if needed
517        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        // a2 value for the hash algo
539        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        // hashed or unhashed username - always hash if server wants it
552        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    /// Produce a header string (also accessible through the Display trait)
600    pub fn to_header_string(&self) -> String {
601        self.to_string()
602    }
603
604    /// Construct from the `Authorization` header string
605    ///
606    /// # Errors
607    /// If the header is malformed (e.g. missing mandatory fields)
608    pub fn parse(input: &str) -> Result<Self> {
609        let mut input = input.trim();
610
611        // Remove leading "Digest"
612        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            // cnonce must not be set if qop is not given, clear it.
662            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        // algorithm can be omitted if it is the default value (or in legacy compat mode)
699        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    /// Parse HTTP header
725    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        // most things are parsed here...
797        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        // verify some defaults
897        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        // check that it correctly ignores leading Digest
926        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        // The spec has a wrong hash in the example, see errata
962        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        // Try round trip
977        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        //println!("{}", str);
1001
1002        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        // Try round trip
1020        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        // Try round trip
1061        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        //
1079        //    let secrets = AuthSecrets {
1080        //        username: "Mufasa".to_string(),
1081        //        password: "Circle of Life".to_string(),
1082        //        uri: "/dir/index.html".to_string(),
1083        //        body: None,
1084        //        method: HttpMethod::GET,
1085        //        nc: 1,
1086        //        cnonce: Some("f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ".to_string()),
1087        //    };
1088
1089        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        //println!("{}", str);
1094
1095        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        // Try round trip
1113        let parsed = AuthorizationHeader::parse(&s).unwrap();
1114        assert_eq!(answer, parsed);
1115    }
1116}