Compare commits

...

7 Commits

Author SHA1 Message Date
Ondřej Hruška fa3503e19a Bump version 1 year ago
Wolfgang Silbermayr 49d214bfd6 Upgrade dependencies 1 year ago
Ondřej Hruška 2827c9bbe5
optimize Method 3 years ago
Ondřej Hruška bae4c13597
improvements around HttpMethod, more tests 3 years ago
Ondřej Hruška a1f3c6ebfb Merge branch 'pr-method-lifetime' of slamb/digest_auth_rs into master 3 years ago
Scott Lamb 2e61615da0 allow non-'static HTTP method names 3 years ago
Ondřej Hruška 750294cd97 add changelog 3 years ago
  1. 15
      CHANGELOG.md
  2. 15
      Cargo.toml
  3. 2
      README.md
  4. 4
      src/digest.rs
  5. 296
      src/enums.rs
  6. 2
      src/error.rs
  7. 39
      src/lib.rs

@ -0,0 +1,15 @@
# 0.3.1
- Update deps
# 0.3.0
- Added lifetime parameter to `HttpMethod`
- Changed `HttpMethod` to `HttpMethod<'a>(Cow<'a, str>)`
- Added unit tests
- Added support for the `http` crate's `Method` struct (optional feature)
# 0.2.4
- Update dependencies

@ -1,6 +1,6 @@
[package] [package]
name = "digest_auth" name = "digest_auth"
version = "0.2.4" version = "0.3.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"
@ -17,6 +17,13 @@ license = "MIT"
[dependencies] [dependencies]
rand = "0.8" rand = "0.8"
hex = "0.4" hex = "0.4"
sha2 = "0.9" sha2 = "0.10"
md-5 = "0.9" md-5 = "0.10"
digest = "0.9" digest = "0.10"
[dependencies.http]
version = "0.2.4"
optional = true
[features]
default = []

@ -2,3 +2,5 @@ Rust implementation of Digest Auth hashing algorithms,
as defined in IETF RFC 2069, 2617, and 7616. as defined in IETF RFC 2069, 2617, and 7616.
This crate provides the authentication header parsing and generation code. This crate provides the authentication header parsing and generation code.
Please see the docs and tests for examples.

@ -161,7 +161,7 @@ pub struct AuthContext<'a> {
/// May be left out if not using auth-int /// May be left out if not using auth-int
pub body: Option<Cow<'a, [u8]>>, pub body: Option<Cow<'a, [u8]>>,
/// HTTP method used (defaults to GET) /// HTTP method used (defaults to GET)
pub method: HttpMethod, pub method: HttpMethod<'a>,
/// Spoofed client nonce (use only for tests; a random nonce is generated automatically) /// Spoofed client nonce (use only for tests; a random nonce is generated automatically)
pub cnonce: Option<Cow<'a, str>>, pub cnonce: Option<Cow<'a, str>>,
} }
@ -202,7 +202,7 @@ impl<'a> AuthContext<'a> {
password: PW, password: PW,
uri: UR, uri: UR,
body: Option<BD>, body: Option<BD>,
method: HttpMethod, method: HttpMethod<'a>,
) -> Self ) -> Self
where where
UN: Into<Cow<'a, str>>, UN: Into<Cow<'a, str>>,

@ -1,3 +1,5 @@
#![allow(clippy::upper_case_acronyms)]
use crate::{Error, Error::*, Result}; use crate::{Error, Error::*, Result};
use std::fmt; use std::fmt;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
@ -5,7 +7,8 @@ use std::str::FromStr;
use digest::{Digest, DynDigest}; use digest::{Digest, DynDigest};
use md5::Md5; use md5::Md5;
use sha2::{Sha256, Sha512Trunc256}; use sha2::{Sha256, Sha512_256};
use std::borrow::Cow;
/// Algorithm type /// Algorithm type
#[derive(Debug, PartialEq, Clone, Copy)] #[derive(Debug, PartialEq, Clone, Copy)]
@ -34,7 +37,7 @@ impl Algorithm {
let mut hash: Box<dyn DynDigest> = match self.algo { let mut hash: Box<dyn DynDigest> = match self.algo {
AlgorithmType::MD5 => Box::new(Md5::new()), AlgorithmType::MD5 => Box::new(Md5::new()),
AlgorithmType::SHA2_256 => Box::new(Sha256::new()), AlgorithmType::SHA2_256 => Box::new(Sha256::new()),
AlgorithmType::SHA2_512_256 => Box::new(Sha512Trunc256::new()), AlgorithmType::SHA2_512_256 => Box::new(Sha512_256::new()),
}; };
hash.update(bytes); hash.update(bytes);
@ -127,10 +130,9 @@ pub enum QopAlgo<'a> {
} }
// casting back... // casting back...
impl<'a> Into<Option<Qop>> for QopAlgo<'a> { impl<'a> From<QopAlgo<'a>> for Option<Qop> {
/// Convert to ?Qop fn from(algo: QopAlgo<'a>) -> Self {
fn into(self) -> Option<Qop> { match algo {
match self {
QopAlgo::NONE => None, QopAlgo::NONE => None,
QopAlgo::AUTH => Some(Qop::AUTH), QopAlgo::AUTH => Some(Qop::AUTH),
QopAlgo::AUTH_INT(_) => Some(Qop::AUTH_INT), QopAlgo::AUTH_INT(_) => Some(Qop::AUTH_INT),
@ -167,28 +169,282 @@ impl Display for Charset {
} }
/// HTTP method (used when generating the response hash for some Qop options) /// HTTP method (used when generating the response hash for some Qop options)
#[derive(Debug)] #[derive(Debug, PartialEq, Clone)]
pub enum HttpMethod { pub struct HttpMethod<'a>(pub Cow<'a, str>);
GET,
POST, // Well-known methods are provided as convenient associated constants
HEAD, impl<'a> HttpMethod<'a> {
OTHER(&'static str), pub const GET : Self = HttpMethod(Cow::Borrowed("GET"));
pub const POST : Self = HttpMethod(Cow::Borrowed("POST"));
pub const PUT : Self = HttpMethod(Cow::Borrowed("PUT"));
pub const DELETE : Self = HttpMethod(Cow::Borrowed("DELETE"));
pub const HEAD : Self = HttpMethod(Cow::Borrowed("HEAD"));
pub const OPTIONS : Self = HttpMethod(Cow::Borrowed("OPTIONS"));
pub const CONNECT : Self = HttpMethod(Cow::Borrowed("CONNECT"));
pub const PATCH : Self = HttpMethod(Cow::Borrowed("PATCH"));
pub const TRACE : Self = HttpMethod(Cow::Borrowed("TRACE"));
} }
impl Default for HttpMethod { impl<'a> Default for HttpMethod<'a> {
fn default() -> Self { fn default() -> Self {
HttpMethod::GET HttpMethod::GET
} }
} }
impl Display for HttpMethod { impl<'a> Display for HttpMethod<'a> {
/// Convert to uppercase string /// Convert to uppercase string
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str(match self { f.write_str(&self.0)
HttpMethod::GET => "GET", }
HttpMethod::POST => "POST", }
HttpMethod::HEAD => "HEAD",
HttpMethod::OTHER(s) => s, impl<'a> From<&'a str> for HttpMethod<'a> {
}) fn from(s: &'a str) -> Self {
Self(s.into())
}
}
impl<'a> From<&'a [u8]> for HttpMethod<'a> {
fn from(s: &'a [u8]) -> Self {
Self(String::from_utf8_lossy(s).into())
}
}
impl<'a> From<String> for HttpMethod<'a> {
fn from(s: String) -> Self {
Self(s.into())
}
}
impl<'a> From<Cow<'a, str>> for HttpMethod<'a> {
fn from(s: Cow<'a, str>) -> Self {
Self(s)
}
}
#[cfg(feature = "http")]
impl From<http::Method> for HttpMethod<'static> {
fn from(method: http::Method) -> Self {
match method.as_str() {
// Avoid cloning when possible
"GET" => Self::GET,
"POST" => Self::POST,
"PUT" => Self::PUT,
"DELETE" => Self::DELETE,
"HEAD" => Self::HEAD,
"OPTIONS" => Self::OPTIONS,
"CONNECT" => Self::CONNECT,
"PATCH" => Self::PATCH,
"TRACE" => Self::TRACE,
// Clone custom strings. This is inefficient, but the inner string is private
other => Self(other.to_owned().into())
}
}
}
#[cfg(feature = "http")]
impl<'a> From<&'a http::Method> for HttpMethod<'a> {
fn from(method: &'a http::Method) -> HttpMethod<'a> {
Self(method.as_str().into())
}
}
#[cfg(test)]
mod test {
use crate::error::Error::{BadCharset, BadQop, UnknownAlgorithm};
use crate::{Algorithm, AlgorithmType, Charset, HttpMethod, Qop, QopAlgo};
use std::borrow::Cow;
use std::str::FromStr;
#[test]
fn test_algorithm_type() {
// String parsing
assert_eq!(
Ok(Algorithm::new(AlgorithmType::MD5, false)),
Algorithm::from_str("MD5")
);
assert_eq!(
Ok(Algorithm::new(AlgorithmType::MD5, true)),
Algorithm::from_str("MD5-sess")
);
assert_eq!(
Ok(Algorithm::new(AlgorithmType::SHA2_256, false)),
Algorithm::from_str("SHA-256")
);
assert_eq!(
Ok(Algorithm::new(AlgorithmType::SHA2_256, true)),
Algorithm::from_str("SHA-256-sess")
);
assert_eq!(
Ok(Algorithm::new(AlgorithmType::SHA2_512_256, false)),
Algorithm::from_str("SHA-512-256")
);
assert_eq!(
Ok(Algorithm::new(AlgorithmType::SHA2_512_256, true)),
Algorithm::from_str("SHA-512-256-sess")
);
assert_eq!(
Err(UnknownAlgorithm("OTHER_ALGORITHM".to_string())),
Algorithm::from_str("OTHER_ALGORITHM")
);
// String building
assert_eq!(
"MD5".to_string(),
Algorithm::new(AlgorithmType::MD5, false).to_string()
);
assert_eq!(
"MD5-sess".to_string(),
Algorithm::new(AlgorithmType::MD5, true).to_string()
);
assert_eq!(
"SHA-256".to_string(),
Algorithm::new(AlgorithmType::SHA2_256, false).to_string()
);
assert_eq!(
"SHA-256-sess".to_string(),
Algorithm::new(AlgorithmType::SHA2_256, true).to_string()
);
assert_eq!(
"SHA-512-256".to_string(),
Algorithm::new(AlgorithmType::SHA2_512_256, false).to_string()
);
assert_eq!(
"SHA-512-256-sess".to_string(),
Algorithm::new(AlgorithmType::SHA2_512_256, true).to_string()
);
// Default
assert_eq!(
Algorithm::new(AlgorithmType::MD5, false),
Default::default()
);
// Hash calculation
assert_eq!(
"e2fc714c4727ee9395f324cd2e7f331f".to_string(),
Algorithm::new(AlgorithmType::MD5, false).hash("abcd".as_bytes())
);
assert_eq!(
"e2fc714c4727ee9395f324cd2e7f331f".to_string(),
Algorithm::new(AlgorithmType::MD5, false).hash_str("abcd")
);
assert_eq!(
"88d4266fd4e6338d13b845fcf289579d209c897823b9217da3e161936f031589".to_string(),
Algorithm::new(AlgorithmType::SHA2_256, false).hash("abcd".as_bytes())
);
assert_eq!(
"d2891c7978be0e24948f37caa415b87cb5cbe2b26b7bad9dc6391b8a6f6ddcc9".to_string(),
Algorithm::new(AlgorithmType::SHA2_512_256, false).hash("abcd".as_bytes())
);
}
#[test]
fn test_qop() {
assert_eq!(Ok(Qop::AUTH), Qop::from_str("auth"));
assert_eq!(Ok(Qop::AUTH_INT), Qop::from_str("auth-int"));
assert_eq!(Err(BadQop("banana".to_string())), Qop::from_str("banana"));
assert_eq!("auth".to_string(), Qop::AUTH.to_string());
assert_eq!("auth-int".to_string(), Qop::AUTH_INT.to_string());
}
#[test]
fn test_qop_algo() {
assert_eq!(Option::<Qop>::None, QopAlgo::NONE.into());
assert_eq!(Some(Qop::AUTH), QopAlgo::AUTH.into());
assert_eq!(
Some(Qop::AUTH_INT),
QopAlgo::AUTH_INT("foo".as_bytes()).into()
);
}
#[test]
fn test_charset() {
assert_eq!(Ok(Charset::UTF8), Charset::from_str("UTF-8"));
assert_eq!(Err(BadCharset("ASCII".into())), Charset::from_str("ASCII"));
assert_eq!("UTF-8".to_string(), Charset::UTF8.to_string());
assert_eq!("ASCII".to_string(), Charset::ASCII.to_string());
}
#[test]
fn test_http_method() {
// Well known 'static
assert_eq!(HttpMethod::GET, "GET".into());
assert_eq!(HttpMethod::POST, "POST".into());
assert_eq!(HttpMethod::PUT, "PUT".into());
assert_eq!(HttpMethod::DELETE, "DELETE".into());
assert_eq!(HttpMethod::HEAD, "HEAD".into());
assert_eq!(HttpMethod::OPTIONS, "OPTIONS".into());
assert_eq!(HttpMethod::CONNECT, "CONNECT".into());
assert_eq!(HttpMethod::PATCH, "PATCH".into());
assert_eq!(HttpMethod::TRACE, "TRACE".into());
// As bytes
assert_eq!(HttpMethod::GET, "GET".as_bytes().into());
assert_eq!(
HttpMethod(Cow::Borrowed("ěščř")),
"ěščř".as_bytes().into()
);
assert_eq!(
HttpMethod(Cow::Owned("AB<EFBFBD>".to_string())), // Lossy conversion
(&[65u8, 66, 156][..]).into()
);
// Well known String
assert_eq!(HttpMethod::GET, String::from("GET").into());
// Custom String
assert_eq!(
HttpMethod(Cow::Borrowed("NonsenseMethod")),
"NonsenseMethod".into()
);
assert_eq!(
HttpMethod(Cow::Owned("NonsenseMethod".to_string())),
"NonsenseMethod".to_string().into()
);
// Custom Cow
assert_eq!(HttpMethod::HEAD, Cow::Borrowed("HEAD").into());
assert_eq!(
HttpMethod(Cow::Borrowed("NonsenseMethod")),
Cow::Borrowed("NonsenseMethod").into()
);
// to string
assert_eq!("GET".to_string(), HttpMethod::GET.to_string());
assert_eq!("POST".to_string(), HttpMethod::POST.to_string());
assert_eq!("PUT".to_string(), HttpMethod::PUT.to_string());
assert_eq!("DELETE".to_string(), HttpMethod::DELETE.to_string());
assert_eq!("HEAD".to_string(), HttpMethod::HEAD.to_string());
assert_eq!("OPTIONS".to_string(), HttpMethod::OPTIONS.to_string());
assert_eq!("CONNECT".to_string(), HttpMethod::CONNECT.to_string());
assert_eq!("PATCH".to_string(), HttpMethod::PATCH.to_string());
assert_eq!("TRACE".to_string(), HttpMethod::TRACE.to_string());
assert_eq!(
"NonsenseMethod".to_string(),
HttpMethod(Cow::Borrowed("NonsenseMethod")).to_string()
);
assert_eq!(
"NonsenseMethod".to_string(),
HttpMethod(Cow::Owned("NonsenseMethod".to_string())).to_string()
);
}
#[cfg(feature = "http")]
#[test]
fn test_http_crate() {
assert_eq!(HttpMethod::GET, http::Method::GET.clone().into());
assert_eq!(
HttpMethod(Cow::Owned("BANANA".to_string())),
http::Method::from_str("BANANA").unwrap().into()
);
assert_eq!(HttpMethod::GET, (&http::Method::GET).into());
let x = http::Method::from_str("BANANA").unwrap();
assert_eq!(
HttpMethod(Cow::Borrowed("BANANA")),
(&x).into()
);
} }
} }

@ -1,7 +1,7 @@
use std::fmt::{self, Display, Formatter}; use std::fmt::{self, Display, Formatter};
use std::result; use std::result;
#[derive(Debug)] #[derive(Debug, PartialEq)]
pub enum Error { pub enum Error {
BadCharset(String), BadCharset(String),
UnknownAlgorithm(String), UnknownAlgorithm(String),

@ -62,9 +62,13 @@ pub fn parse(www_authorize: &str) -> Result<WwwAuthenticateHeader> {
WwwAuthenticateHeader::parse(www_authorize) WwwAuthenticateHeader::parse(www_authorize)
} }
#[test] #[cfg(test)]
fn test_parse_respond() { mod test {
let src = r#" use crate::{AuthContext, Error};
#[test]
fn test_parse_respond() {
let src = r#"
Digest Digest
realm="http-auth@example.org", realm="http-auth@example.org",
qop="auth, auth-int", qop="auth, auth-int",
@ -73,17 +77,17 @@ fn test_parse_respond() {
opaque="FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS" opaque="FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS"
"#; "#;
let mut context = AuthContext::new("Mufasa", "Circle of Life", "/dir/index.html"); let mut context = AuthContext::new("Mufasa", "Circle of Life", "/dir/index.html");
context.set_custom_cnonce("f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ"); context.set_custom_cnonce("f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ");
let mut prompt = crate::parse(src).unwrap(); let mut prompt = crate::parse(src).unwrap();
let answer = prompt.respond(&context).unwrap(); let answer = prompt.respond(&context).unwrap();
let str = answer.to_string().replace(", ", ",\n "); let str = answer.to_string().replace(", ", ",\n "); // This is only for easier reading
assert_eq!( assert_eq!(
str, str,
r#" r#"
Digest username="Mufasa", Digest username="Mufasa",
realm="http-auth@example.org", realm="http-auth@example.org",
nonce="7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v", nonce="7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v",
@ -95,11 +99,12 @@ Digest username="Mufasa",
opaque="FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS", opaque="FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS",
algorithm=MD5 algorithm=MD5
"# "#
.trim() .trim()
); );
} }
#[test] #[test]
fn test_cast_error() { fn test_cast_error() {
let _m: Box<dyn std::error::Error> = Error::UnknownAlgorithm("Uhhh".into()).into(); let _m: Box<dyn std::error::Error> = Error::UnknownAlgorithm("Uhhh".into()).into();
}
} }

Loading…
Cancel
Save