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

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

@ -1,6 +1,6 @@
[package]
name = "digest_auth"
version = "0.1.0"
version = "0.1.1"
authors = ["Ondřej Hruška <ondra@ondrovo.com>"]
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"

@ -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<'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
///
/// # Panics
/// if the input is malformed
pub fn parse_header_map(input: &str) -> HashMap<String, String> {
pub fn parse_header_map(input: &str) -> Fallible<HashMap<String, String>> {
#[derive(Debug)]
#[allow(non_camel_case_types)]
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
}
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<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
})
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<AuthorizationHeader<'a>> {
// 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> = 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);

@ -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> {
WwwAuthenticateHeader::from_str(www_authorize)
WwwAuthenticateHeader::parse(www_authorize)
}
#[test]

Loading…
Cancel
Save