From 1947affd7574422af919f93e084839f2e66b17ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Mon, 7 Oct 2019 01:04:37 +0200 Subject: [PATCH] add parsing support to AuthorizationHeader --- src/digest.rs | 576 +++++++++++++++++++++++++++++++------------------- src/enums.rs | 8 +- src/error.rs | 14 +- 3 files changed, 375 insertions(+), 223 deletions(-) diff --git a/src/digest.rs b/src/digest.rs index f80197e..cf4d544 100644 --- a/src/digest.rs +++ b/src/digest.rs @@ -57,6 +57,98 @@ impl Display for NamedTag<'_> { } } + +/// Helper func that parses the key-value string received from server +fn parse_header_map(input: &str) -> Result> { + #[derive(Debug)] + #[allow(non_camel_case_types)] + enum ParserState { + P_WHITE, + P_NAME(usize), + P_VALUE_BEGIN, + P_VALUE_QUOTED, + P_VALUE_QUOTED_NEXTLITERAL, + P_VALUE_PLAIN, + } + + let mut state = ParserState::P_WHITE; + + let mut parsed = HashMap::::new(); + let mut current_token = None; + let mut current_value = String::new(); + + for (char_n, c) in input.chars().enumerate() { + match state { + ParserState::P_WHITE => { + if c.is_alphabetic() { + state = ParserState::P_NAME(char_n); + } + } + ParserState::P_NAME(name_start) => { + if c == '=' { + current_token = Some(&input[name_start..char_n]); + state = ParserState::P_VALUE_BEGIN; + } + } + ParserState::P_VALUE_BEGIN => { + current_value.clear(); + state = match c { + '"' => ParserState::P_VALUE_QUOTED, + _ => { + current_value.push(c); + ParserState::P_VALUE_PLAIN + } + }; + } + ParserState::P_VALUE_QUOTED => { + match c { + '"' => { + parsed.insert(current_token.unwrap().to_string(), current_value.clone()); + + current_token = None; + current_value.clear(); + + state = ParserState::P_WHITE; + } + '\\' => { + state = ParserState::P_VALUE_QUOTED_NEXTLITERAL; + } + _ => { + current_value.push(c); + } + }; + } + ParserState::P_VALUE_PLAIN => { + if c == ',' || c.is_ascii_whitespace() { + parsed.insert(current_token.unwrap().to_string(), current_value.clone()); + + current_token = None; + current_value.clear(); + + state = ParserState::P_WHITE; + } else { + current_value.push(c); + } + } + ParserState::P_VALUE_QUOTED_NEXTLITERAL => { + current_value.push(c); + state = ParserState::P_VALUE_QUOTED + } + } + } + + match state { + ParserState::P_VALUE_PLAIN => { + parsed.insert(current_token.unwrap().to_string(), current_value); // consume the value here + } + ParserState::P_WHITE => {} + _ => return Err(InvalidHeaderSyntax(input.into())), + } + + Ok(parsed) +} + + /// Login attempt context /// /// All fields are borrowed to reduce runtime overhead; this struct should not be stored anywhere, @@ -64,64 +156,81 @@ impl Display for NamedTag<'_> { #[derive(Debug)] pub struct AuthContext<'a> { /// Login username - pub username: &'a str, + pub username: Cow<'a, str>, /// Login password (plain) - pub password: &'a str, + pub password: Cow<'a, str>, /// Requested URI (not a domain! should start with a slash) - pub uri: &'a str, + pub uri: Cow<'a, str>, /// Request payload body - used for auth-int (auth with integrity check) /// May be left out if not using auth-int - pub body: Option<&'a [u8]>, + pub body: Option>, /// HTTP method used (defaults to GET) pub method: HttpMethod, /// Spoofed client nonce (use only for tests; a random nonce is generated automatically) - pub cnonce: Option<&'a str>, + pub cnonce: Option>, } impl<'a> AuthContext<'a> { /// Construct a new context with the GET verb and no payload body. /// See the other constructors if this does not fit your situation. - pub fn new<'n: 'a, 'p: 'a, 'u: 'a>(username: &'n str, password: &'p str, uri: &'u str) -> Self { - Self::new_with_method(username, password, uri, None, HttpMethod::GET) + pub fn new(username: UN, password: PW, uri: UR) -> Self + where UN: Into>, + PW: Into>, + UR: Into> + { + Self::new_with_method(username, password, uri, Option::<&'a[u8]>::None, HttpMethod::GET) } /// Construct a new context with the POST verb and a payload body (may be None). /// See the other constructors if this does not fit your situation. - pub fn new_post<'n: 'a, 'p: 'a, 'u: 'a, 'b: 'a>( - username: &'n str, - password: &'p str, - uri: &'u str, - body: Option<&'b [u8]>, - ) -> Self { + pub fn new_post( + username: UN, + password: PW, + uri: UR, + body: Option, + ) -> Self + where UN: Into>, + PW: Into>, + UR: Into>, + BD: Into>, + { Self::new_with_method(username, password, uri, body, HttpMethod::POST) } /// Construct a new context with arbitrary verb and, optionally, a payload body - pub fn new_with_method<'n: 'a, 'p: 'a, 'u: 'a, 'b: 'a>( - username: &'n str, - password: &'p str, - uri: &'u str, - body: Option<&'b [u8]>, + pub fn new_with_method( + username: UN, + password: PW, + uri: UR, + body: Option, method: HttpMethod, - ) -> Self { + ) -> Self + where UN: Into>, + PW: Into>, + UR: Into>, + BD: Into> + { Self { - username, - password, - uri, - body, + username : username.into(), + password : password.into(), + uri : uri.into(), + body : body.map(Into::into), method, cnonce: None, } } /// Set cnonce to the given value - pub fn set_custom_cnonce<'x: 'a>(&mut self, cnonce: &'x str) { - self.cnonce = Some(cnonce); + pub fn set_custom_cnonce(&mut self, cnonce: CN) + where + CN: Into> + { + self.cnonce = Some(cnonce.into()); } } /// WWW-Authenticate header parsed from HTTP header value -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub struct WwwAuthenticateHeader { /// Domain is a list of URIs that will accept the same digest. None if not given (i.e applies to all) pub domain: Option>, @@ -160,10 +269,10 @@ impl FromStr for WwwAuthenticateHeader { impl WwwAuthenticateHeader { /// Generate an [`AuthorizationHeader`](struct.AuthorizationHeader.html) to be sent to the server in a new request. /// The [`self.nc`](struct.AuthorizationHeader.html#structfield.nc) field is incremented. - pub fn respond<'re, 'a: 're, 'c: 're>( - &'a mut self, - secrets: &'c AuthContext, - ) -> Result> { + pub fn respond( + &mut self, + secrets: &AuthContext, + ) -> Result { AuthorizationHeader::from_prompt(self, secrets) } @@ -181,13 +290,6 @@ impl WwwAuthenticateHeader { let mut kv = parse_header_map(input)?; - //println!("Parsed map: {:#?}", kv); - - let algo = match kv.get("algorithm") { - Some(a) => Algorithm::from_str(&a)?, - _ => Algorithm::default(), - }; - Ok(Self { domain: if let Some(domains) = kv.get("domain") { let domains: Vec<&str> = domains.split(' ').collect(); @@ -197,11 +299,11 @@ impl WwwAuthenticateHeader { }, realm: match kv.remove("realm") { Some(v) => v, - None => return Err(MissingRealm(input.into())), + None => return Err(MissingRequired("realm", input.into())), }, nonce: match kv.remove("nonce") { Some(v) => v, - None => return Err(MissingNonce(input.into())), + None => return Err(MissingRequired("nonce", input.into())), }, opaque: kv.remove("opaque"), stale: match kv.get("stale") { @@ -212,7 +314,10 @@ impl WwwAuthenticateHeader { Some(v) => Charset::from_str(v)?, None => Charset::ASCII, }, - algorithm: algo, + algorithm: match kv.get("algorithm") { + Some(a) => Algorithm::from_str(&a)?, + _ => Algorithm::default(), + }, qop: if let Some(domains) = kv.get("qop") { let domains: Vec<&str> = domains.split(',').collect(); let mut qops = vec![]; @@ -264,9 +369,7 @@ impl Display for WwwAuthenticateHeader { } for (i, e) in entries.iter().enumerate() { - if i > 0 { - f.write_str(", ")?; - } + if i > 0 { f.write_str(", ")?; } f.write_str(&e.to_string())?; } @@ -274,112 +377,29 @@ impl Display for WwwAuthenticateHeader { } } -/// Helper func that parses the key-value string received from server -fn parse_header_map(input: &str) -> Result> { - #[derive(Debug)] - #[allow(non_camel_case_types)] - enum ParserState { - P_WHITE, - P_NAME(usize), - P_VALUE_BEGIN, - P_VALUE_QUOTED, - P_VALUE_QUOTED_NEXTLITERAL, - P_VALUE_PLAIN, - } - - let mut state = ParserState::P_WHITE; - - let mut parsed = HashMap::::new(); - let mut current_token = None; - let mut current_value = String::new(); - - for (char_n, c) in input.chars().enumerate() { - match state { - ParserState::P_WHITE => { - if c.is_alphabetic() { - state = ParserState::P_NAME(char_n); - } - } - ParserState::P_NAME(name_start) => { - if c == '=' { - current_token = Some(&input[name_start..char_n]); - state = ParserState::P_VALUE_BEGIN; - } - } - ParserState::P_VALUE_BEGIN => { - current_value.clear(); - state = match c { - '"' => ParserState::P_VALUE_QUOTED, - _ => { - current_value.push(c); - ParserState::P_VALUE_PLAIN - } - }; - } - ParserState::P_VALUE_QUOTED => { - match c { - '"' => { - parsed.insert(current_token.unwrap().to_string(), current_value.clone()); - - current_token = None; - current_value.clear(); - - state = ParserState::P_WHITE; - } - '\\' => { - state = ParserState::P_VALUE_QUOTED_NEXTLITERAL; - } - _ => { - current_value.push(c); - } - }; - } - ParserState::P_VALUE_PLAIN => { - if c == ',' || c.is_ascii_whitespace() { - parsed.insert(current_token.unwrap().to_string(), current_value.clone()); - - current_token = None; - current_value.clear(); - - state = ParserState::P_WHITE; - } else { - current_value.push(c); - } - } - ParserState::P_VALUE_QUOTED_NEXTLITERAL => { - current_value.push(c); - state = ParserState::P_VALUE_QUOTED - } - } - } - - match state { - ParserState::P_VALUE_PLAIN => { - parsed.insert(current_token.unwrap().to_string(), current_value); // consume the value here - } - ParserState::P_WHITE => {} - _ => return Err(InvalidHeaderSyntax(input.into())), - } - - Ok(parsed) -} - /// Header sent back to the server, including password hashes. /// /// This can be obtained by calling [`AuthorizationHeader::from_prompt()`](#method.from_prompt), /// or from the [`WwwAuthenticateHeader`](struct.WwwAuthenticateHeader.html) prompt struct /// with [`.respond()`](struct.WwwAuthenticateHeader.html#method.respond) -#[derive(Debug)] -pub struct AuthorizationHeader<'ctx> { - /// The server header that triggered the authentication flow; used to retrieve some additional - /// fields when serializing to the header string - pub prompt: &'ctx WwwAuthenticateHeader, +#[derive(Debug, PartialEq, Clone)] +pub struct AuthorizationHeader { + /// Authorization realm + pub realm : String, + /// Server nonce + pub nonce: String, + /// Server opaque + pub opaque: Option, + /// Flag that userhash was used + pub userhash : bool, + /// Hash algorithm + pub algorithm : Algorithm, /// Computed digest pub response: String, /// Username or hash (owned because of the computed hash) pub username: String, /// Requested URI - pub uri: &'ctx str, + pub uri: String, /// QOP chosen from the list offered by server, if any /// None in legacy compat mode (RFC 2069) pub qop: Option, @@ -391,7 +411,7 @@ pub struct AuthorizationHeader<'ctx> { pub nc: u32, } -impl<'a> AuthorizationHeader<'a> { +impl AuthorizationHeader { /// Construct using a parsed prompt header and an auth context, selecting suitable algorithm /// options. The [`WwwAuthenticateHeader`](struct.WwwAuthenticateHeader.html) struct contains a /// [`nc`](struct.WwwAuthenticateHeader.html#structfield.nc) field that is incremented by this @@ -404,30 +424,20 @@ impl<'a> AuthorizationHeader<'a> { /// /// Fails if the source header is malformed so much that we can't figure out /// a proper response (e.g. given but invalid QOP options) - pub fn from_prompt<'p: 'a, 's: 'a>( - prompt: &'p mut WwwAuthenticateHeader, - context: &'s AuthContext, - ) -> Result> { - // figure out which QOP option to use - let empty_vec = vec![]; - let qop_algo = match &prompt.qop { - None => QopAlgo::NONE, + pub fn from_prompt( + prompt: &mut WwwAuthenticateHeader, + context: &AuthContext, + ) -> Result { + + let qop = match &prompt.qop { + None => None, Some(vec) => { // this is at least RFC2617, qop was given if vec.contains(&Qop::AUTH_INT) { - if let Some(b) = context.body { - QopAlgo::AUTH_INT(b) - } else { - // we have no body. Fall back to regular auth if possible, or use empty - if vec.contains(&Qop::AUTH) { - QopAlgo::AUTH - } else { - QopAlgo::AUTH_INT(&empty_vec) - } - } + Some(Qop::AUTH_INT) } else if vec.contains(&Qop::AUTH) { // "auth" is the second best after "auth-int" - QopAlgo::AUTH + Some(Qop::AUTH) } else { // parser bug - prompt.qop should have been None return Err(BadQopOptions(join_vec(vec, ", "))); @@ -435,15 +445,68 @@ impl<'a> AuthorizationHeader<'a> { } }; - let h = &prompt.algorithm; + prompt.nc += 1; + + let mut hdr = AuthorizationHeader { + realm: prompt.realm.clone(), + nonce: prompt.nonce.clone(), + opaque: prompt.opaque.clone(), + userhash: prompt.userhash, + algorithm: prompt.algorithm, + response : String::default(), + username : String::default(), + uri: context.uri.as_ref().into(), + qop, + cnonce: context.cnonce.as_ref().map(AsRef::as_ref).map(ToOwned::to_owned), // Will be generated if needed, if build_hash is set and this is None + nc: prompt.nc, + }; + + hdr.build_hash(context); + + Ok(hdr) + } + + /// Build the response digest from Auth Context. + /// + /// This function is used by client to fill the Authorization header. + /// It can be used by server using a known password to replicate the hash + /// and then compare "response". + /// + /// This function sets cnonce if it was None before, or reuses it. + /// + /// Fields updated in the Authorization header: + /// - qop (if it was auth-int before but no body was given in context) + /// - cnonce (if it was None before) + /// - username copied from context + /// - response + pub fn build_hash(&mut self, context : &AuthContext) + { + // figure out which QOP option to use + let qop_algo = match self.qop { + None => QopAlgo::NONE, + Some(Qop::AUTH_INT) => { + if let Some(b) = &context.body { + QopAlgo::AUTH_INT(b.as_ref()) + } else { + // fallback + QopAlgo::AUTH + } + } + Some(Qop::AUTH) => { + QopAlgo::AUTH + } + }; + + let h = &self.algorithm; let cnonce = { - match context.cnonce { + match &self.cnonce { Some(cnonce) => cnonce.to_owned(), None => { let mut rng = rand::thread_rng(); let nonce_bytes: [u8; 16] = rng.gen(); - hex::encode(nonce_bytes) + let cnonce = hex::encode(nonce_bytes); + cnonce } } }; @@ -453,16 +516,16 @@ impl<'a> AuthorizationHeader<'a> { let a = format!( "{name}:{realm}:{pw}", name = context.username, - realm = prompt.realm, + realm = self.realm, pw = context.password ); - let sess = prompt.algorithm.sess; + let sess = self.algorithm.sess; if sess { format!( "{hash}:{nonce}:{cnonce}", hash = h.hash(a.as_bytes()), - nonce = prompt.nonce, + nonce = self.nonce, cnonce = cnonce ) } else { @@ -484,17 +547,17 @@ impl<'a> AuthorizationHeader<'a> { }; // hashed or unhashed username - always hash if server wants it - let username = if prompt.userhash { + let username = if self.userhash { h.hash( format!( "{username}:{realm}", username = context.username, - realm = prompt.realm + realm = self.realm ) .as_bytes(), ) } else { - context.username.to_owned() + context.username.as_ref().to_owned() }; let qop: Option = qop_algo.into(); @@ -502,17 +565,13 @@ impl<'a> AuthorizationHeader<'a> { let ha1 = h.hash_str(&a1); let ha2 = h.hash_str(&a2); - // Increment nonce counter - prompt.nc += 1; - - // Compute the response - let response = match &qop { + self.response = match &qop { Some(q) => { let tmp = format!( "{ha1}:{nonce}:{nc:08x}:{cnonce}:{qop}:{ha2}", ha1 = ha1, - nonce = prompt.nonce, - nc = prompt.nc, + nonce = self.nonce, + nc = self.nc, cnonce = cnonce, qop = q, ha2 = ha2 @@ -523,69 +582,138 @@ impl<'a> AuthorizationHeader<'a> { let tmp = format!( "{ha1}:{nonce}:{ha2}", ha1 = ha1, - nonce = prompt.nonce, + nonce = self.nonce, ha2 = ha2 ); h.hash(tmp.as_bytes()) } }; - Ok(AuthorizationHeader { - prompt, - response, - username, - uri: context.uri, - qop, - cnonce: Some(cnonce), - nc: prompt.nc, - }) + self.qop = qop; + self.username = username; + self.cnonce = qop.map(|_| cnonce); } /// Produce a header string (also accessible through the Display trait) pub fn to_header_string(&self) -> String { - // TODO move impl from Display here & clean it up self.to_string() } + + /// Construct from the `Authorization` header string + /// + /// # Errors + /// If the header is malformed (e.g. missing mandatory fields) + pub fn parse(input: &str) -> Result { + let mut input = input.trim(); + + // Remove leading "Digest" + if input.starts_with("Digest") { + input = &input["Digest".len()..]; + } + + let mut kv = parse_header_map(input)?; + + let mut auth = Self { + username: match kv.remove("username") { + Some(v) => v, + None => return Err(MissingRequired("username", input.into())), + }, + realm: match kv.remove("realm") { + Some(v) => v, + None => return Err(MissingRequired("realm", input.into())), + }, + nonce: match kv.remove("nonce") { + Some(v) => v, + None => return Err(MissingRequired("nonce", input.into())), + }, + uri: match kv.remove("uri") { + Some(v) => v, + None => return Err(MissingRequired("uri", input.into())), + }, + response: match kv.remove("response") { + Some(v) => v, + None => return Err(MissingRequired("response", input.into())), + }, + qop: kv.remove("qop").map(|s| Qop::from_str(&s)).transpose()?, + nc: match kv.remove("nc") { + Some(v) => u32::from_str_radix(&v, 16)?, + None => 1, + }, + cnonce: kv.remove("cnonce"), + opaque: kv.remove("opaque"), + algorithm: match kv.get("algorithm") { + Some(a) => Algorithm::from_str(&a)?, + _ => Algorithm::default(), + }, + userhash: match kv.get("userhash") { + Some(v) => &v.to_ascii_lowercase() == "true", + None => false, + }, + }; + + if auth.qop.is_some() { + if !auth.cnonce.is_some() { + return Err(MissingRequired("cnonce", input.into())) + } + } else { + // cnonce must not be set if qop is not given, clear it. + auth.cnonce = None; + } + + Ok(auth) + } } -impl<'a> Display for AuthorizationHeader<'a> { +impl Display for AuthorizationHeader { fn fmt(&self, f: &mut Formatter) -> fmt::Result { + let mut entries = Vec::::new(); + f.write_str("Digest ")?; - //TODO charset shenanigans with username* (UTF-8 charset) - 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() { - 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(), - )?; + entries.push(NamedTag::Quoted("username", (&self.username).into())); + entries.push(NamedTag::Quoted("realm", (&self.realm).into())); + entries.push(NamedTag::Quoted("nonce", (&self.nonce).into())); + entries.push(NamedTag::Quoted("uri", (&self.uri).into())); + + if self.qop.is_some() && self.cnonce.is_some() { + entries.push(NamedTag::Plain("qop", self.qop.as_ref().unwrap().to_string().into())); + entries.push(NamedTag::Plain("nc", format!("{:08x}", self.nc).into())); + entries.push(NamedTag::Quoted("cnonce", self.cnonce.as_ref().unwrap().into())); } - write!(f, ", response=\"{}\"", self.response.quote_for_digest())?; + entries.push(NamedTag::Quoted("response", (&self.response).into())); - if let Some(opaque) = &self.prompt.opaque { - write!(f, ", opaque=\"{}\"", opaque.quote_for_digest())?; + if let Some(opaque) = &self.opaque { + entries.push(NamedTag::Quoted("opaque", opaque.into())); } // 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 { - write!(f, ", algorithm={}", self.prompt.algorithm)?; + if self.qop.is_some() || self.algorithm.algo != AlgorithmType::MD5 { + entries.push(NamedTag::Plain("algorithm", self.algorithm.to_string().into())); + } + + if self.userhash { + entries.push(NamedTag::Plain("userhash", "true".into())); } - if self.prompt.userhash { - f.write_str(", userhash=true")?; + for (i, e) in entries.iter().enumerate() { + if i > 0 { f.write_str(", ")?; } + f.write_str(&e.to_string())?; } Ok(()) } } +impl FromStr for AuthorizationHeader { + type Err = crate::Error; + + /// Parse HTTP header + fn from_str(input: &str) -> Result { + Self::parse(input) + } +} + #[cfg(test)] mod tests { use super::parse_header_map; @@ -810,9 +938,9 @@ r#"Digest realm="api@example.org", let answer = AuthorizationHeader::from_prompt(&mut prompt, &context).unwrap(); // The spec has a wrong hash in the example, see errata - let str = answer.to_string().replace(", ", ",\n "); + let s = answer.to_string().replace(", ", ",\n "); assert_eq!( - str, + s, r#" Digest username="Mufasa", realm="testrealm@host.com", @@ -823,6 +951,10 @@ Digest username="Mufasa", "# .trim() ); + + // Try round trip + let parsed = AuthorizationHeader::parse(&s).unwrap(); + assert_eq!(answer, parsed); } #[test] @@ -838,14 +970,16 @@ Digest username="Mufasa", let mut context = AuthContext::new("Mufasa", "Circle Of Life", "/dir/index.html"); context.set_custom_cnonce("0a4f113b"); + assert_eq!(context.body, None); + let mut prompt = WwwAuthenticateHeader::from_str(src).unwrap(); let answer = AuthorizationHeader::from_prompt(&mut prompt, &context).unwrap(); - let str = answer.to_string().replace(", ", ",\n "); + let s = answer.to_string().replace(", ", ",\n "); //println!("{}", str); assert_eq!( - str, + s, r#" Digest username="Mufasa", realm="testrealm@host.com", @@ -860,6 +994,10 @@ Digest username="Mufasa", "# .trim() ); + + // Try round trip + let parsed = AuthorizationHeader::parse(&s).unwrap(); + assert_eq!(answer, parsed); } #[test] @@ -879,10 +1017,10 @@ Digest username="Mufasa", let mut prompt = WwwAuthenticateHeader::from_str(src).unwrap(); let answer = AuthorizationHeader::from_prompt(&mut prompt, &context).unwrap(); - let str = answer.to_string().replace(", ", ",\n "); + let s = answer.to_string().replace(", ", ",\n "); assert_eq!( - str, + s, r#" Digest username="Mufasa", realm="http-auth@example.org", @@ -897,6 +1035,10 @@ Digest username="Mufasa", "# .trim() ); + + // Try round trip + let parsed = AuthorizationHeader::parse(&s).unwrap(); + assert_eq!(answer, parsed); } #[test] @@ -926,11 +1068,11 @@ Digest username="Mufasa", let mut prompt = WwwAuthenticateHeader::from_str(src).unwrap(); let answer = AuthorizationHeader::from_prompt(&mut prompt, &context).unwrap(); - let str = answer.to_string().replace(", ", ",\n "); + let s = answer.to_string().replace(", ", ",\n "); //println!("{}", str); assert_eq!( - str, + s, r#" Digest username="Mufasa", realm="http-auth@example.org", @@ -945,5 +1087,9 @@ Digest username="Mufasa", "# .trim() ); + + // Try round trip + let parsed = AuthorizationHeader::parse(&s).unwrap(); + assert_eq!(answer, parsed); } } diff --git a/src/enums.rs b/src/enums.rs index 5d6cd91..39331e6 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -5,7 +5,7 @@ use std::fmt::{Display, Formatter}; use std::str::FromStr; /// Algorithm type -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone, Copy)] #[allow(non_camel_case_types)] pub enum AlgorithmType { MD5, @@ -14,7 +14,7 @@ pub enum AlgorithmType { } /// Algorithm and the -sess flag pair -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone, Copy)] pub struct Algorithm { pub algo: AlgorithmType, pub sess: bool, @@ -86,7 +86,7 @@ impl Display for Algorithm { } /// QOP field values -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone, Copy)] #[allow(non_camel_case_types)] pub enum Qop { AUTH, @@ -136,7 +136,7 @@ impl<'a> Into> for QopAlgo<'a> { } /// Charset field value as specified by the server -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub enum Charset { ASCII, UTF8, diff --git a/src/error.rs b/src/error.rs index fe182bd..df8cc50 100644 --- a/src/error.rs +++ b/src/error.rs @@ -6,10 +6,10 @@ pub enum Error { BadCharset(String), UnknownAlgorithm(String), BadQop(String), - MissingRealm(String), - MissingNonce(String), + MissingRequired(&'static str, String), InvalidHeaderSyntax(String), BadQopOptions(String), + NumParseError, } pub type Result = result::Result; @@ -22,10 +22,16 @@ impl Display for Error { BadCharset(ctx) => write!(f, "Bad charset: {}", ctx), UnknownAlgorithm(ctx) => write!(f, "Unknown algorithm: {}", ctx), BadQop(ctx) => write!(f, "Bad Qop option: {}", ctx), - MissingRealm(ctx) => write!(f, "Missing 'realm' in WWW-Authenticate: {}", ctx), - MissingNonce(ctx) => write!(f, "Missing 'nonce' in WWW-Authenticate: {}", ctx), + MissingRequired(what, ctx) => write!(f, "Missing \"{}\" in header: {}", what, ctx), InvalidHeaderSyntax(ctx) => write!(f, "Invalid header syntax: {}", ctx), BadQopOptions(ctx) => write!(f, "Illegal Qop in prompt: {}", ctx), + NumParseError => write!(f, "Error parsing a number."), } } } + +impl From for Error { + fn from(_: std::num::ParseIntError) -> Self { + NumParseError + } +}