code cleaning with clippy, improved API ergonomics, add missing pub

master
Ondřej Hruška 5 years ago
parent c25e559b53
commit 18c2b2a107
Signed by untrusted user: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 3
      Cargo.toml
  2. 203
      src/digest.rs
  3. 7
      src/lib.rs

@ -1,6 +1,6 @@
[package] [package]
name = "digest_auth" name = "digest_auth"
version = "0.1.0" version = "0.1.1"
authors = ["Ondřej Hruška <ondra@ondrovo.com>"] authors = ["Ondřej Hruška <ondra@ondrovo.com>"]
edition = "2018" edition = "2018"
description = "Implementation of the Digest Auth algorithm as defined in IETF RFC 2069, 2617, and 7616, intended for HTTP clients" 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" "web-programming::http-client"
] ]
license = "MIT" license = "MIT"
maintenance = { status = "actively-developed" }
[dependencies] [dependencies]
rust-crypto = "0.2" rust-crypto = "0.2"

@ -212,6 +212,8 @@ impl Display for HttpMethod {
//endregion //endregion
//region AuthContext
/// Login attempt context /// Login attempt context
/// ///
/// All fields are borrowed to reduce runtime overhead; this struct should not be stored anywhere, /// 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> { impl<'a> AuthContext<'a> {
/// Construct a new context with the GET verb and no payload body. /// Construct a new context with the GET verb and no payload body.
/// See the other constructors if this does not fit your situation. /// 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) 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). /// 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. /// 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, username : &'n str,
password : &'p str, password : &'p str,
uri : &'u str, uri : &'u str,
@ -252,7 +254,7 @@ impl<'a> AuthContext<'a> {
} }
/// Construct a new context with arbitrary verb and, optionally, a payload body /// 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, username : &'n str,
password : &'p str, password : &'p str,
uri : &'u str, uri : &'u str,
@ -279,8 +281,6 @@ impl<'a> AuthContext<'a> {
//region WwwAuthenticateHeader //region WwwAuthenticateHeader
/// WWW-Authenticate header parsed from HTTP header value /// 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)] #[derive(Debug, PartialEq)]
pub struct WwwAuthenticateHeader { pub struct WwwAuthenticateHeader {
/// Domain is a list of URIs that will accept the same digest. None if not given (i.e applies to all) /// 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, pub userhash: bool,
/// Server-supported charset /// Server-supported charset
pub charset: 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. /// a response is composed with the same nonce.
pub nc: u32, pub nc: u32,
} }
impl 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) -> Fallible<AuthorizationHeader<'re>> { pub fn respond<'re, 'a:'re, 'c:'re>(&'a mut self, secrets : &'c AuthContext) -> Fallible<AuthorizationHeader<'re>> {
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<Self> {
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 /// Helper func that parses the key-value string received from server
/// pub fn parse_header_map(input: &str) -> Fallible<HashMap<String, String>> {
/// # Panics
/// if the input is malformed
pub fn parse_header_map(input: &str) -> HashMap<String, String> {
#[derive(Debug)] #[derive(Debug)]
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]
enum ParserState { enum ParserState {
@ -401,86 +462,28 @@ pub fn parse_header_map(input: &str) -> HashMap<String, String> {
parsed.insert(current_token.unwrap().to_string(), current_value); // consume the value here parsed.insert(current_token.unwrap().to_string(), current_value); // consume the value here
} }
ParserState::P_WHITE => {} 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 { impl FromStr for WwwAuthenticateHeader {
type Err = Error; type Err = Error;
/// Parse HTTP header /// Parse HTTP header
fn from_str(input: &str) -> Fallible<Self> { fn from_str(input: &str) -> Fallible<Self> {
let mut input = input.trim(); Self::parse(input)
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
})
} }
} }
//endregion //endregion
//region Authentication //region AuthorizationHeader
/// Header sent back to the server, including password hashes /// Header sent back to the server, including password hashes.
/// Always create it using ::new(), the hash calculation is done in the constructor.
/// ///
/// This can also be obtained from the WwwAuthentication header with `WwwAuthenticateHeader::respond()` /// 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)
///
/// Use .to_string() to generate the Authenticate HTTP header value
#[derive(Debug)] #[derive(Debug)]
pub struct AuthorizationHeader<'ctx> { pub struct AuthorizationHeader<'ctx> {
/// The server header that triggered the authentication flow; used to retrieve some additional /// 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> { impl<'a> AuthorizationHeader<'a> {
/// Create from a prompt header and auth context, selecting suitable algorithm options. /// Construct using a parsed prompt header and an auth context, selecting suitable algorithm
/// The header contains a 'nc' field that is incremented by this method. /// 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 /// # Errors
/// server supports nonce reuse - it will work automatically
/// ///
/// Returns Error if the source header is malformed so much that we can't figure out /// Fails if the source header is malformed so much that we can't figure out
/// a proper response. /// a proper response (e.g. given but invalid QOP options)
pub fn new<'p:'a, 's:'a>( pub fn from_prompt<'p:'a, 's:'a>(
prompt: &'p mut WwwAuthenticateHeader, context: &'s AuthContext prompt: &'p mut WwwAuthenticateHeader, context: &'s AuthContext
) -> Fallible<AuthorizationHeader<'a>> { ) -> Fallible<AuthorizationHeader<'a>> {
// figure out which QOP option to use // 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 // hashed or unhashed username - always hash if server wants it
let username = match prompt.userhash { let username = if prompt.userhash {
true => h.hash( h.hash(
format!( format!(
"{username}:{realm}", "{username}:{realm}",
username = context.username, username = context.username,
realm = prompt.realm realm = prompt.realm
) ).as_bytes()
.as_bytes(), )
), } else {
false => context.username.to_owned(), context.username.to_owned()
}; };
let qop : Option<Qop> = qop_algo.into(); let qop : Option<Qop> = qop_algo.into();
@ -609,7 +616,7 @@ impl<'a> AuthorizationHeader<'a> {
let ha2 = h.hash_str(&a2); let ha2 = h.hash_str(&a2);
// Increment nonce counter // Increment nonce counter
prompt.nc = prompt.nc + 1; prompt.nc += 1;
// Compute the response // Compute the response
let response = match &qop { let response = match &qop {
@ -676,7 +683,7 @@ impl<'a> Display for AuthorizationHeader<'a> {
f.write_fmt(format_args!(", uri=\"{}\"", self.uri))?; 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!( f.write_fmt(format_args!(
", qop={qop}, nc={nc:08x}, cnonce=\"{cnonce}\"", ", qop={qop}, nc={nc:08x}, cnonce=\"{cnonce}\"",
qop = self.qop.as_ref().unwrap(), qop = self.qop.as_ref().unwrap(),
@ -736,7 +743,7 @@ mod tests {
userhash=true 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("realm").unwrap(), "api@example.org");
assert_eq!(map.get("qop").unwrap(), "auth"); assert_eq!(map.get("qop").unwrap(), "auth");
@ -755,19 +762,19 @@ mod tests {
{ {
let src = r#"realm="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"); assert_eq!(map.get("realm").unwrap(), "api@example.org");
} }
{ {
let src = r#"realm=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"); assert_eq!(map.get("realm").unwrap(), "api@example.org");
} }
{ {
let src = ""; let src = "";
let map = parse_header_map(src); let map = parse_header_map(src).unwrap();
assert_eq!(map.is_empty(), true); assert_eq!(map.is_empty(), true);
} }
} }
@ -874,7 +881,7 @@ mod tests {
let context = AuthContext::new("Mufasa", "CircleOfLife", "/dir/index.html"); let context = AuthContext::new("Mufasa", "CircleOfLife", "/dir/index.html");
let mut prompt = WwwAuthenticateHeader::from_str(src).unwrap(); 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 // The spec has a wrong hash in the example, see errata
let str = answer.to_string().replace(", ", ",\n "); let str = answer.to_string().replace(", ", ",\n ");
@ -906,7 +913,7 @@ Digest username="Mufasa",
context.set_custom_cnonce("0a4f113b"); context.set_custom_cnonce("0a4f113b");
let mut prompt = WwwAuthenticateHeader::from_str(src).unwrap(); 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 "); let str = answer.to_string().replace(", ", ",\n ");
//println!("{}", str); //println!("{}", str);
@ -944,7 +951,7 @@ Digest username="Mufasa",
context.set_custom_cnonce("f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ"); context.set_custom_cnonce("f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ");
let mut prompt = WwwAuthenticateHeader::from_str(src).unwrap(); 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 "); let str = answer.to_string().replace(", ", ",\n ");
@ -991,7 +998,7 @@ Digest username="Mufasa",
// }; // };
let mut prompt = WwwAuthenticateHeader::from_str(src).unwrap(); 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 "); let str = answer.to_string().replace(", ", ",\n ");
//println!("{}", str); //println!("{}", str);

@ -41,7 +41,6 @@
//! // notice how the 'response' field changed - the 'nc' counter is included in the hash //! // 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"#); //! 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; #[macro_use] extern crate failure;
use failure::Fallible; use failure::Fallible;
@ -49,16 +48,14 @@ use failure::Fallible;
mod digest; mod digest;
mod utils; mod utils;
use std::str::FromStr;
pub use crate::digest::{ pub use crate::digest::{
Algorithm, AuthContext, AuthorizationHeader, HttpMethod, Qop, WwwAuthenticateHeader, Algorithm, AuthContext, AuthorizationHeader, HttpMethod, Qop, WwwAuthenticateHeader,
}; };
/// Parse the WWW-Authorization header value. /// 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> { pub fn parse(www_authorize : &str) -> Fallible<WwwAuthenticateHeader> {
WwwAuthenticateHeader::from_str(www_authorize) WwwAuthenticateHeader::parse(www_authorize)
} }
#[test] #[test]

Loading…
Cancel
Save