From 18c2b2a1074d8ab9a89f7d928db85eebf78cd7d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Wed, 27 Feb 2019 14:04:43 +0100 Subject: [PATCH] code cleaning with clippy, improved API ergonomics, add missing pub --- Cargo.toml | 3 +- src/digest.rs | 203 ++++++++++++++++++++++++++------------------------ src/lib.rs | 7 +- 3 files changed, 108 insertions(+), 105 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 39b7c4d..7d10528 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "digest_auth" -version = "0.1.0" +version = "0.1.1" authors = ["Ondřej Hruška "] edition = "2018" description = "Implementation of the Digest Auth algorithm as defined in IETF RFC 2069, 2617, and 7616, intended for HTTP clients" @@ -13,7 +13,6 @@ categories = [ "web-programming::http-client" ] license = "MIT" -maintenance = { status = "actively-developed" } [dependencies] rust-crypto = "0.2" diff --git a/src/digest.rs b/src/digest.rs index f0bde20..aa9463c 100644 --- a/src/digest.rs +++ b/src/digest.rs @@ -212,6 +212,8 @@ impl Display for HttpMethod { //endregion +//region AuthContext + /// Login attempt context /// /// All fields are borrowed to reduce runtime overhead; this struct should not be stored anywhere, @@ -236,13 +238,13 @@ pub struct AuthContext<'a> { 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, 's:'a, 'u:'a>(username : &'n str, password : &'p str, uri : &'u str) -> Self { + 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) } /// 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, 's:'a, 'u:'a, 'b:'a>( + pub fn new_post<'n:'a, 'p:'a, 'u:'a, 'b:'a>( username : &'n str, password : &'p str, uri : &'u str, @@ -252,7 +254,7 @@ impl<'a> AuthContext<'a> { } /// Construct a new context with arbitrary verb and, optionally, a payload body - pub fn new_with_method<'n:'a, 'p:'a, 's:'a, 'u:'a, 'b:'a>( + pub fn new_with_method<'n:'a, 'p:'a, 'u:'a, 'b:'a>( username : &'n str, password : &'p str, uri : &'u str, @@ -279,8 +281,6 @@ impl<'a> AuthContext<'a> { //region WwwAuthenticateHeader /// WWW-Authenticate header parsed from HTTP header value -/// -/// Use .from_str() to create it from data in a WWW-Authenticate HTTP header #[derive(Debug, PartialEq)] pub struct WwwAuthenticateHeader { /// Domain is a list of URIs that will accept the same digest. None if not given (i.e applies to all) @@ -303,22 +303,83 @@ pub struct WwwAuthenticateHeader { pub userhash: bool, /// Server-supported charset pub charset: Charset, - /// NC - not part of the received header, but kept here for convenience and incremented each time + /// nc - not part of the received header, but kept here for convenience and incremented each time /// a response is composed with the same nonce. pub nc: u32, } 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) -> Fallible> { - AuthorizationHeader::new(self, secrets) + AuthorizationHeader::from_prompt(self, secrets) + } + + /// Construct from the `WWW-Authenticate` header string + /// + /// # Errors + /// If the header is malformed (e.g. missing 'realm', missing a closing quote, unknown algorithm etc.) + pub fn parse(input: &str) -> Fallible { + let mut input = input.trim(); + if input.starts_with("Digest") { + input = &input["Digest".len()..]; + } + + 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(); + Some(domains.iter().map(|x| x.trim().to_string()).collect()) + } else { + None + }, + realm: match kv.remove("realm") { + Some(v) => v, + None => bail!("realm not given"), + }, + nonce: match kv.remove("nonce") { + Some(v) => v, + None => bail!("nonce not given"), + }, + opaque: kv.remove("opaque"), + stale: match kv.get("stale") { + Some(v) => v.to_ascii_lowercase() == "true", + None => false, + }, + charset: match kv.get("charset") { + Some(v) => Charset::from_str(v)?, + None => Charset::ASCII, + }, + algorithm: algo, + qop: if let Some(domains) = kv.get("qop") { + let domains: Vec<&str> = domains.split(',').collect(); + let mut qops = vec![]; + for d in domains { + qops.push(Qop::from_str(d.trim())?); + } + Some(qops) + } else { + None + }, + userhash: match kv.get("userhash") { + Some(v) => v.to_ascii_lowercase() == "true", + None => false, + }, + nc : 0 + }) } } /// Helper func that parses the key-value string received from server -/// -/// # Panics -/// if the input is malformed -pub fn parse_header_map(input: &str) -> HashMap { +pub fn parse_header_map(input: &str) -> Fallible> { #[derive(Debug)] #[allow(non_camel_case_types)] enum ParserState { @@ -401,86 +462,28 @@ pub fn parse_header_map(input: &str) -> HashMap { parsed.insert(current_token.unwrap().to_string(), current_value); // consume the value here } ParserState::P_WHITE => {} - _ => panic!("Unexpected end state {:?}", state), + _ => bail!("Unexpected end state {:?}", state), } - parsed + Ok(parsed) } -/// Parse the WWW-Authenticate header value into a struct impl FromStr for WwwAuthenticateHeader { type Err = Error; /// Parse HTTP header fn from_str(input: &str) -> Fallible { - let mut input = input.trim(); - if input.starts_with("Digest") { - input = &input["Digest".len()..]; - } - - 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(); - Some(domains.iter().map(|x| x.trim().to_string()).collect()) - } else { - None - }, - realm: match kv.remove("realm") { - Some(v) => v, - None => bail!("realm not given"), - }, - nonce: match kv.remove("nonce") { - Some(v) => v, - None => bail!("nonce not given"), - }, - opaque: kv.remove("opaque"), - stale: match kv.get("stale") { - Some(v) => v.to_ascii_lowercase() == "true", - None => false, - }, - charset: match kv.get("charset") { - Some(v) => Charset::from_str(v)?, - None => Charset::ASCII, - }, - algorithm: algo, - qop: if let Some(domains) = kv.get("qop") { - let domains: Vec<&str> = domains.split(",").collect(); - let mut qops = vec![]; - for d in domains { - qops.push(Qop::from_str(d.trim())?); - } - Some(qops) - } else { - None - }, - userhash: match kv.get("userhash") { - Some(v) => v.to_ascii_lowercase() == "true", - None => false, - }, - nc : 0 - }) + Self::parse(input) } } //endregion -//region Authentication +//region AuthorizationHeader -/// Header sent back to the server, including password hashes -/// Always create it using ::new(), the hash calculation is done in the constructor. +/// Header sent back to the server, including password hashes. /// -/// This can also be obtained from the WwwAuthentication header with `WwwAuthenticateHeader::respond()` -/// -/// Use .to_string() to generate the Authenticate HTTP header value +/// 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 @@ -504,15 +507,19 @@ pub struct AuthorizationHeader<'ctx> { } impl<'a> AuthorizationHeader<'a> { - /// Create from a prompt header and auth context, selecting suitable algorithm options. - /// The header contains a 'nc' field that is incremented by this method. + /// 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 + /// method. + /// + /// For subsequent requests, simply reuse the same [`WwwAuthenticateHeader`](struct.WwwAuthenticateHeader.html) + /// and - if the server supports nonce reuse - it will work automatically. /// - /// For subsequent requests, simply reuse the same parsed WwwAuthenticateHeader, and - if the - /// server supports nonce reuse - it will work automatically + /// # Errors /// - /// Returns Error if the source header is malformed so much that we can't figure out - /// a proper response. - pub fn new<'p:'a, 's:'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 ) -> Fallible> { // figure out which QOP option to use @@ -591,16 +598,16 @@ impl<'a> AuthorizationHeader<'a> { }; // hashed or unhashed username - always hash if server wants it - let username = match prompt.userhash { - true => h.hash( + let username = if prompt.userhash { + h.hash( format!( "{username}:{realm}", username = context.username, realm = prompt.realm - ) - .as_bytes(), - ), - false => context.username.to_owned(), + ).as_bytes() + ) + } else { + context.username.to_owned() }; let qop : Option = qop_algo.into(); @@ -609,7 +616,7 @@ impl<'a> AuthorizationHeader<'a> { let ha2 = h.hash_str(&a2); // Increment nonce counter - prompt.nc = prompt.nc + 1; + prompt.nc += 1; // Compute the response let response = match &qop { @@ -676,7 +683,7 @@ impl<'a> Display for AuthorizationHeader<'a> { f.write_fmt(format_args!(", uri=\"{}\"", self.uri))?; - if self.prompt.qop.is_some() { + 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(), @@ -736,7 +743,7 @@ mod tests { userhash=true "#; - let map = parse_header_map(src); + let map = parse_header_map(src).unwrap(); assert_eq!(map.get("realm").unwrap(), "api@example.org"); assert_eq!(map.get("qop").unwrap(), "auth"); @@ -755,19 +762,19 @@ mod tests { { let src = r#"realm="api@example.org""#; - let map = parse_header_map(src); + 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); + let map = parse_header_map(src).unwrap(); assert_eq!(map.get("realm").unwrap(), "api@example.org"); } { let src = ""; - let map = parse_header_map(src); + let map = parse_header_map(src).unwrap(); assert_eq!(map.is_empty(), true); } } @@ -874,7 +881,7 @@ mod tests { let context = AuthContext::new("Mufasa", "CircleOfLife", "/dir/index.html"); let mut prompt = WwwAuthenticateHeader::from_str(src).unwrap(); - let answer = AuthorizationHeader::new(&mut prompt, &context).unwrap(); + 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 "); @@ -906,7 +913,7 @@ Digest username="Mufasa", context.set_custom_cnonce("0a4f113b"); let mut prompt = WwwAuthenticateHeader::from_str(src).unwrap(); - let answer = AuthorizationHeader::new(&mut prompt, &context).unwrap(); + let answer = AuthorizationHeader::from_prompt(&mut prompt, &context).unwrap(); let str = answer.to_string().replace(", ", ",\n "); //println!("{}", str); @@ -944,7 +951,7 @@ Digest username="Mufasa", context.set_custom_cnonce("f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ"); let mut prompt = WwwAuthenticateHeader::from_str(src).unwrap(); - let answer = AuthorizationHeader::new(&mut prompt, &context).unwrap(); + let answer = AuthorizationHeader::from_prompt(&mut prompt, &context).unwrap(); let str = answer.to_string().replace(", ", ",\n "); @@ -991,7 +998,7 @@ Digest username="Mufasa", // }; let mut prompt = WwwAuthenticateHeader::from_str(src).unwrap(); - let answer = AuthorizationHeader::new(&mut prompt, &context).unwrap(); + let answer = AuthorizationHeader::from_prompt(&mut prompt, &context).unwrap(); let str = answer.to_string().replace(", ", ",\n "); //println!("{}", str); diff --git a/src/lib.rs b/src/lib.rs index f78a5f4..723fc00 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,7 +41,6 @@ //! // notice how the 'response' field changed - the 'nc' counter is included in the hash //! assert_eq!(answer2, r#"Digest username="Mufasa", realm="http-auth@example.org", nonce="7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v", uri="/dir/index.html", qop=auth, nc=00000002, cnonce="f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ", response="4b5d595ecf2db9df612ea5b45cd97101", opaque="FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS", algorithm=MD5"#); //! ``` -//! #[macro_use] extern crate failure; use failure::Fallible; @@ -49,16 +48,14 @@ use failure::Fallible; mod digest; mod utils; -use std::str::FromStr; - pub use crate::digest::{ Algorithm, AuthContext, AuthorizationHeader, HttpMethod, Qop, WwwAuthenticateHeader, }; /// Parse the WWW-Authorization header value. -/// It's just a convenience method to call `WwwAuthenticateHeader::from_str()`. +/// It's just a convenience method to call [`WwwAuthenticateHeader::parse()`](struct.WwwAuthenticateHeader.html#method.parse). pub fn parse(www_authorize : &str) -> Fallible { - WwwAuthenticateHeader::from_str(www_authorize) + WwwAuthenticateHeader::parse(www_authorize) } #[test]