From f2346416ccf4a946efc30f73a041b004c56dff9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sat, 5 Oct 2019 23:18:46 +0200 Subject: [PATCH] implement ToString for WwwAuthenticateHeader for server-side use --- README.md | 3 +- src/digest.rs | 393 ++++++++++++++++++++++++++++++++------------------ src/enums.rs | 9 ++ 3 files changed, 264 insertions(+), 141 deletions(-) diff --git a/README.md b/README.md index 698c0df..786396c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ -Rust implementation of Digest Auth hashing algorithms, as defined in IETF RFC 2069, 2617, and 7616. +Rust implementation of Digest Auth hashing algorithms, +as defined in IETF RFC 2069, 2617, and 7616. This crate provides the authentication header parsing and generation code. diff --git a/src/digest.rs b/src/digest.rs index 50790b5..f80197e 100644 --- a/src/digest.rs +++ b/src/digest.rs @@ -6,6 +6,7 @@ use std::str::FromStr; use crate::enums::{Algorithm, AlgorithmType, Charset, HttpMethod, Qop, QopAlgo}; use crate::{Error::*, Result}; +use std::borrow::Cow; /// slash quoting for digest strings trait QuoteForDigest { @@ -18,12 +19,44 @@ impl QuoteForDigest for &str { } } +impl<'a> QuoteForDigest for Cow<'a, str> { + fn quote_for_digest(&self) -> String { + self.as_ref().quote_for_digest() + } +} + impl QuoteForDigest for String { fn quote_for_digest(&self) -> String { self.replace("\\", "\\\\").replace("\"", "\\\"") } } +/// Join a Vec of Display items using a separator +fn join_vec(vec : &Vec, sep : &str) -> String { + vec.iter() + .map(ToString::to_string) + .collect::>() + .join(sep) +} + +enum NamedTag<'a> { + Quoted(&'a str, Cow<'a, str>), + Plain(&'a str, Cow<'a, str>) +} + +impl Display for NamedTag<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + NamedTag::Quoted(name, content) => { + write!(f, "{}=\"{}\"", name, content.quote_for_digest()) + } + NamedTag::Plain(name, content) => { + write!(f, "{}={}", name, content) + } + } + } +} + /// Login attempt context /// /// All fields are borrowed to reduce runtime overhead; this struct should not be stored anywhere, @@ -199,6 +232,48 @@ impl WwwAuthenticateHeader { } } +impl Display for WwwAuthenticateHeader { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let mut entries = Vec::::new(); + + f.write_str("Digest ")?; + + entries.push(NamedTag::Quoted("realm", (&self.realm).into())); + + if let Some(ref qops) = self.qop { + entries.push(NamedTag::Quoted("qop", join_vec(qops, ", ").into())); + } + + if let Some(ref domains) = self.domain { + entries.push(NamedTag::Quoted("domain", join_vec(domains, " ").into())); + } + + if self.stale { + entries.push(NamedTag::Plain("stale", "true".into())); + } + + entries.push(NamedTag::Plain("algorithm", self.algorithm.to_string().into())); + entries.push(NamedTag::Quoted("nonce", (&self.nonce).into())); + if let Some(ref opaque) = self.opaque { + entries.push(NamedTag::Quoted("opaque", (opaque).into())); + } + entries.push(NamedTag::Plain("charset", self.charset.to_string().into())); + + if self.userhash { + entries.push(NamedTag::Plain("userhash", "true".into())); + } + + for (i, e) in entries.iter().enumerate() { + if i > 0 { + f.write_str(", ")?; + } + f.write_str(&e.to_string())?; + } + + Ok(()) + } +} + /// Helper func that parses the key-value string received from server fn parse_header_map(input: &str) -> Result> { #[derive(Debug)] @@ -355,12 +430,7 @@ impl<'a> AuthorizationHeader<'a> { QopAlgo::AUTH } else { // parser bug - prompt.qop should have been None - return Err(BadQopOptions( - vec.iter() - .map(ToString::to_string) - .collect::>() - .join(","), - )); + return Err(BadQopOptions(join_vec(vec, ", "))); } } }; @@ -421,7 +491,7 @@ impl<'a> AuthorizationHeader<'a> { username = context.username, realm = prompt.realm ) - .as_bytes(), + .as_bytes(), ) } else { context.username.to_owned() @@ -483,44 +553,29 @@ impl<'a> Display for AuthorizationHeader<'a> { f.write_str("Digest ")?; //TODO charset shenanigans with username* (UTF-8 charset) - f.write_fmt(format_args!( - "username=\"{}\"", - self.username.quote_for_digest() - ))?; - - f.write_fmt(format_args!( - ", realm=\"{}\"", - self.prompt.realm.quote_for_digest() - ))?; - - f.write_fmt(format_args!( - ", nonce=\"{}\"", - self.prompt.nonce.quote_for_digest() - ))?; - - f.write_fmt(format_args!(", uri=\"{}\"", self.uri))?; + write!(f, "username=\"{uname}\", realm=\"{realm}\", nonce=\"{nonce}\", uri=\"{uri}\"", + uname = self.username.quote_for_digest(), + realm = self.prompt.realm.quote_for_digest(), + nonce = self.prompt.nonce.quote_for_digest(), + uri = self.uri)?; if self.prompt.qop.is_some() && self.cnonce.is_some() { - f.write_fmt(format_args!( - ", qop={qop}, nc={nc:08x}, cnonce=\"{cnonce}\"", - qop = self.qop.as_ref().unwrap(), - cnonce = self.cnonce.as_ref().unwrap().quote_for_digest(), - nc = self.nc - ))?; + write!(f, ", qop={qop}, nc={nc:08x}, cnonce=\"{cnonce}\"", + qop = self.qop.as_ref().unwrap(), + nc = self.nc, + cnonce = self.cnonce.as_ref().unwrap().quote_for_digest(), + )?; } - f.write_fmt(format_args!( - ", response=\"{}\"", - self.response.quote_for_digest() - ))?; + write!(f, ", response=\"{}\"", self.response.quote_for_digest())?; if let Some(opaque) = &self.prompt.opaque { - f.write_fmt(format_args!(", opaque=\"{}\"", opaque.quote_for_digest()))?; + write!(f, ", opaque=\"{}\"", opaque.quote_for_digest())?; } // algorithm can be omitted if it is the default value (or in legacy compat mode) if self.qop.is_some() || self.prompt.algorithm.algo != AlgorithmType::MD5 { - f.write_fmt(format_args!(", algorithm={}", self.prompt.algorithm))?; + write!(f, ", algorithm={}", self.prompt.algorithm)?; } if self.prompt.userhash { @@ -545,8 +600,7 @@ mod tests { #[test] fn test_parse_header_map() { - { - let src = r#" + let src = r#" realm="api@example.org", qop="auth", algorithm=SHA-512-256, @@ -556,35 +610,40 @@ mod tests { userhash=true "#; - let map = parse_header_map(src).unwrap(); + let map = parse_header_map(src).unwrap(); - assert_eq!(map.get("realm").unwrap(), "api@example.org"); - assert_eq!(map.get("qop").unwrap(), "auth"); - assert_eq!(map.get("algorithm").unwrap(), "SHA-512-256"); - assert_eq!( - map.get("nonce").unwrap(), - "5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK" - ); - assert_eq!( - map.get("opaque").unwrap(), - "HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS" - ); - assert_eq!(map.get("charset").unwrap(), "UTF-8"); - assert_eq!(map.get("userhash").unwrap(), "true"); - } + assert_eq!(map.get("realm").unwrap(), "api@example.org"); + assert_eq!(map.get("qop").unwrap(), "auth"); + assert_eq!(map.get("algorithm").unwrap(), "SHA-512-256"); + assert_eq!( + map.get("nonce").unwrap(), + "5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK" + ); + assert_eq!( + map.get("opaque").unwrap(), + "HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS" + ); + assert_eq!(map.get("charset").unwrap(), "UTF-8"); + assert_eq!(map.get("userhash").unwrap(), "true"); + } - { - let src = r#"realm="api@example.org""#; - let map = parse_header_map(src).unwrap(); - assert_eq!(map.get("realm").unwrap(), "api@example.org"); - } + #[test] + fn test_parse_header_map2() + { + let src = r#"realm="api@example.org""#; + let map = parse_header_map(src).unwrap(); + assert_eq!(map.get("realm").unwrap(), "api@example.org"); + } - { - let src = r#"realm=api@example.org"#; - let map = parse_header_map(src).unwrap(); - assert_eq!(map.get("realm").unwrap(), "api@example.org"); - } + #[test] + fn test_parse_header_map3() { + let src = r#"realm=api@example.org"#; + let map = parse_header_map(src).unwrap(); + assert_eq!(map.get("realm").unwrap(), "api@example.org"); + } + #[test] + fn test_parse_header_map4() { { let src = ""; let map = parse_header_map(src).unwrap(); @@ -594,12 +653,11 @@ mod tests { #[test] fn test_www_hdr_parse() { - { - // most things are parsed here... - let src = r#" + // most things are parsed here... + let src = r#" realm="api@example.org", qop="auth", - domain="/my/nice/url /login /logout" + domain="/my/nice/url /login /logout", algorithm=SHA-512-256, nonce="5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK", opaque="HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS", @@ -607,79 +665,134 @@ mod tests { userhash=true "#; - let parsed = WwwAuthenticateHeader::from_str(src).unwrap(); - - assert_eq!( - parsed, - WwwAuthenticateHeader { - domain: Some(vec![ - "/my/nice/url".to_string(), - "/login".to_string(), - "/logout".to_string(), - ]), - realm: "api@example.org".to_string(), - nonce: "5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK".to_string(), - opaque: Some("HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS".to_string()), - stale: false, - algorithm: Algorithm::new(AlgorithmType::SHA2_512_256, false), - qop: Some(vec![Qop::AUTH]), - userhash: true, - charset: Charset::UTF8, - nc: 0, - } - ) - } + let parsed = WwwAuthenticateHeader::from_str(src).unwrap(); - { - // verify some defaults - let src = r#" - realm="a long realm with\\, weird \" characters", - qop="auth-int", - nonce="bla bla nonce aaaaa", - stale=TRUE - "#; + assert_eq!( + parsed, + WwwAuthenticateHeader { + domain: Some(vec![ + "/my/nice/url".to_string(), + "/login".to_string(), + "/logout".to_string(), + ]), + realm: "api@example.org".to_string(), + nonce: "5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK".to_string(), + opaque: Some("HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS".to_string()), + stale: false, + algorithm: Algorithm::new(AlgorithmType::SHA2_512_256, false), + qop: Some(vec![Qop::AUTH]), + userhash: true, + charset: Charset::UTF8, + nc: 0, + } + ) + } - let parsed = WwwAuthenticateHeader::from_str(src).unwrap(); - - assert_eq!( - parsed, - WwwAuthenticateHeader { - domain: None, - realm: "a long realm with\\, weird \" characters".to_string(), - nonce: "bla bla nonce aaaaa".to_string(), - opaque: None, - stale: true, - algorithm: Algorithm::default(), - qop: Some(vec![Qop::AUTH_INT]), - userhash: false, - charset: Charset::ASCII, - nc: 0, - } - ) - } + #[test] + fn test_www_hdr_tostring() { + let mut hdr = WwwAuthenticateHeader { + domain: Some(vec![ + "/my/nice/url".to_string(), + "/login".to_string(), + "/logout".to_string(), + ]), + realm: "api@example.org".to_string(), + nonce: "5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK".to_string(), + opaque: Some("HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS".to_string()), + stale: false, + algorithm: Algorithm::new(AlgorithmType::SHA2_512_256, false), + qop: Some(vec![Qop::AUTH]), + userhash: true, + charset: Charset::UTF8, + nc: 0, + }; - { - // check that it correctly ignores leading Digest - let src = r#"Digest realm="aaa", nonce="bbb""#; - - let parsed = WwwAuthenticateHeader::from_str(src).unwrap(); - - assert_eq!( - parsed, - WwwAuthenticateHeader { - domain: None, - realm: "aaa".to_string(), - nonce: "bbb".to_string(), - opaque: None, - stale: false, - algorithm: Algorithm::default(), - qop: None, - userhash: false, - charset: Charset::ASCII, - nc: 0, - } - ) - } + assert_eq!( +r#"Digest realm="api@example.org", + qop="auth", + domain="/my/nice/url /login /logout", + algorithm=SHA-512-256, + nonce="5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK", + opaque="HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS", + charset=UTF-8, + userhash=true"#.replace(",\n ", ", "), hdr.to_string()); + + hdr.stale=true; + hdr.userhash=false; + hdr.opaque = None; + hdr.qop = None; + + assert_eq!( +r#"Digest realm="api@example.org", + domain="/my/nice/url /login /logout", + stale=true, + algorithm=SHA-512-256, + nonce="5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK", + charset=UTF-8"#.replace(",\n ", ", "), hdr.to_string()); + + hdr.qop = Some(vec![Qop::AUTH, Qop::AUTH_INT]); + + assert_eq!( +r#"Digest realm="api@example.org", + qop="auth, auth-int", + domain="/my/nice/url /login /logout", + stale=true, + algorithm=SHA-512-256, + nonce="5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK", + charset=UTF-8"#.replace(",\n ", ", "), hdr.to_string()); + } + + #[test] + fn test_www_hdr_parse2() { + // verify some defaults + let src = r#" + realm="a long realm with\\, weird \" characters", + qop="auth-int", + nonce="bla bla nonce aaaaa", + stale=TRUE + "#; + + let parsed = WwwAuthenticateHeader::from_str(src).unwrap(); + + assert_eq!( + parsed, + WwwAuthenticateHeader { + domain: None, + realm: "a long realm with\\, weird \" characters".to_string(), + nonce: "bla bla nonce aaaaa".to_string(), + opaque: None, + stale: true, + algorithm: Algorithm::default(), + qop: Some(vec![Qop::AUTH_INT]), + userhash: false, + charset: Charset::ASCII, + nc: 0, + } + ) + } + + #[test] + fn test_www_hdr_parse3() { + // check that it correctly ignores leading Digest + let src = r#"Digest realm="aaa", nonce="bbb""#; + + let parsed = WwwAuthenticateHeader::from_str(src).unwrap(); + + assert_eq!( + parsed, + WwwAuthenticateHeader { + domain: None, + realm: "aaa".to_string(), + nonce: "bbb".to_string(), + opaque: None, + stale: false, + algorithm: Algorithm::default(), + qop: None, + userhash: false, + charset: Charset::ASCII, + nc: 0, + } + ) } #[test] @@ -708,7 +821,7 @@ Digest username="Mufasa", response="1949323746fe6a43ef61f9606e7febea", opaque="5ccc069c403ebaf9f0171e9517f40e41" "# - .trim() + .trim() ); } @@ -745,7 +858,7 @@ Digest username="Mufasa", opaque="5ccc069c403ebaf9f0171e9517f40e41", algorithm=MD5 "# - .trim() + .trim() ); } @@ -782,7 +895,7 @@ Digest username="Mufasa", opaque="FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS", algorithm=MD5 "# - .trim() + .trim() ); } @@ -830,7 +943,7 @@ Digest username="Mufasa", opaque="FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS", algorithm=SHA-256 "# - .trim() + .trim() ); } } diff --git a/src/enums.rs b/src/enums.rs index 7aab45c..5d6cd91 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -154,6 +154,15 @@ impl FromStr for Charset { } } +impl Display for Charset { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Charset::ASCII => "ASCII", + Charset::UTF8 => "UTF-8", + }) + } +} + /// HTTP method (used when generating the response hash for some Qop options) #[derive(Debug)] pub enum HttpMethod {