implement ToString for WwwAuthenticateHeader for server-side use
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
Rust implementation of Digest Auth hashing algorithms, as defined in IETF RFC 2069, 2617, and 7616.
|
||||
Rust implementation of Digest Auth hashing algorithms,
|
||||
as defined in IETF RFC 2069, 2617, and 7616.
|
||||
|
||||
This crate provides the authentication header parsing and generation code.
|
||||
|
||||
+249
-136
@@ -6,6 +6,7 @@ use std::str::FromStr;
|
||||
use crate::enums::{Algorithm, AlgorithmType, Charset, HttpMethod, Qop, QopAlgo};
|
||||
|
||||
use crate::{Error::*, Result};
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// slash quoting for digest strings
|
||||
trait QuoteForDigest {
|
||||
@@ -18,12 +19,44 @@ impl QuoteForDigest for &str {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> QuoteForDigest for Cow<'a, str> {
|
||||
fn quote_for_digest(&self) -> String {
|
||||
self.as_ref().quote_for_digest()
|
||||
}
|
||||
}
|
||||
|
||||
impl QuoteForDigest for String {
|
||||
fn quote_for_digest(&self) -> String {
|
||||
self.replace("\\", "\\\\").replace("\"", "\\\"")
|
||||
}
|
||||
}
|
||||
|
||||
/// Join a Vec of Display items using a separator
|
||||
fn join_vec<T : ToString>(vec : &Vec<T>, sep : &str) -> String {
|
||||
vec.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join(sep)
|
||||
}
|
||||
|
||||
enum NamedTag<'a> {
|
||||
Quoted(&'a str, Cow<'a, str>),
|
||||
Plain(&'a str, Cow<'a, str>)
|
||||
}
|
||||
|
||||
impl Display for NamedTag<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
NamedTag::Quoted(name, content) => {
|
||||
write!(f, "{}=\"{}\"", name, content.quote_for_digest())
|
||||
}
|
||||
NamedTag::Plain(name, content) => {
|
||||
write!(f, "{}={}", name, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Login attempt context
|
||||
///
|
||||
/// All fields are borrowed to reduce runtime overhead; this struct should not be stored anywhere,
|
||||
@@ -199,6 +232,48 @@ impl WwwAuthenticateHeader {
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for WwwAuthenticateHeader {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let mut entries = Vec::<NamedTag>::new();
|
||||
|
||||
f.write_str("Digest ")?;
|
||||
|
||||
entries.push(NamedTag::Quoted("realm", (&self.realm).into()));
|
||||
|
||||
if let Some(ref qops) = self.qop {
|
||||
entries.push(NamedTag::Quoted("qop", join_vec(qops, ", ").into()));
|
||||
}
|
||||
|
||||
if let Some(ref domains) = self.domain {
|
||||
entries.push(NamedTag::Quoted("domain", join_vec(domains, " ").into()));
|
||||
}
|
||||
|
||||
if self.stale {
|
||||
entries.push(NamedTag::Plain("stale", "true".into()));
|
||||
}
|
||||
|
||||
entries.push(NamedTag::Plain("algorithm", self.algorithm.to_string().into()));
|
||||
entries.push(NamedTag::Quoted("nonce", (&self.nonce).into()));
|
||||
if let Some(ref opaque) = self.opaque {
|
||||
entries.push(NamedTag::Quoted("opaque", (opaque).into()));
|
||||
}
|
||||
entries.push(NamedTag::Plain("charset", self.charset.to_string().into()));
|
||||
|
||||
if self.userhash {
|
||||
entries.push(NamedTag::Plain("userhash", "true".into()));
|
||||
}
|
||||
|
||||
for (i, e) in entries.iter().enumerate() {
|
||||
if i > 0 {
|
||||
f.write_str(", ")?;
|
||||
}
|
||||
f.write_str(&e.to_string())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper func that parses the key-value string received from server
|
||||
fn parse_header_map(input: &str) -> Result<HashMap<String, String>> {
|
||||
#[derive(Debug)]
|
||||
@@ -355,12 +430,7 @@ impl<'a> AuthorizationHeader<'a> {
|
||||
QopAlgo::AUTH
|
||||
} else {
|
||||
// parser bug - prompt.qop should have been None
|
||||
return Err(BadQopOptions(
|
||||
vec.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<String>>()
|
||||
.join(","),
|
||||
));
|
||||
return Err(BadQopOptions(join_vec(vec, ", ")));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -421,7 +491,7 @@ impl<'a> AuthorizationHeader<'a> {
|
||||
username = context.username,
|
||||
realm = prompt.realm
|
||||
)
|
||||
.as_bytes(),
|
||||
.as_bytes(),
|
||||
)
|
||||
} else {
|
||||
context.username.to_owned()
|
||||
@@ -483,44 +553,29 @@ impl<'a> Display for AuthorizationHeader<'a> {
|
||||
f.write_str("Digest ")?;
|
||||
|
||||
//TODO charset shenanigans with username* (UTF-8 charset)
|
||||
f.write_fmt(format_args!(
|
||||
"username=\"{}\"",
|
||||
self.username.quote_for_digest()
|
||||
))?;
|
||||
|
||||
f.write_fmt(format_args!(
|
||||
", realm=\"{}\"",
|
||||
self.prompt.realm.quote_for_digest()
|
||||
))?;
|
||||
|
||||
f.write_fmt(format_args!(
|
||||
", nonce=\"{}\"",
|
||||
self.prompt.nonce.quote_for_digest()
|
||||
))?;
|
||||
|
||||
f.write_fmt(format_args!(", uri=\"{}\"", self.uri))?;
|
||||
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() {
|
||||
f.write_fmt(format_args!(
|
||||
", qop={qop}, nc={nc:08x}, cnonce=\"{cnonce}\"",
|
||||
qop = self.qop.as_ref().unwrap(),
|
||||
cnonce = self.cnonce.as_ref().unwrap().quote_for_digest(),
|
||||
nc = self.nc
|
||||
))?;
|
||||
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(),
|
||||
)?;
|
||||
}
|
||||
|
||||
f.write_fmt(format_args!(
|
||||
", response=\"{}\"",
|
||||
self.response.quote_for_digest()
|
||||
))?;
|
||||
write!(f, ", response=\"{}\"", self.response.quote_for_digest())?;
|
||||
|
||||
if let Some(opaque) = &self.prompt.opaque {
|
||||
f.write_fmt(format_args!(", opaque=\"{}\"", opaque.quote_for_digest()))?;
|
||||
write!(f, ", opaque=\"{}\"", opaque.quote_for_digest())?;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
f.write_fmt(format_args!(", algorithm={}", self.prompt.algorithm))?;
|
||||
write!(f, ", algorithm={}", self.prompt.algorithm)?;
|
||||
}
|
||||
|
||||
if self.prompt.userhash {
|
||||
@@ -545,8 +600,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_parse_header_map() {
|
||||
{
|
||||
let src = r#"
|
||||
let src = r#"
|
||||
realm="api@example.org",
|
||||
qop="auth",
|
||||
algorithm=SHA-512-256,
|
||||
@@ -556,35 +610,40 @@ mod tests {
|
||||
userhash=true
|
||||
"#;
|
||||
|
||||
let map = parse_header_map(src).unwrap();
|
||||
let map = parse_header_map(src).unwrap();
|
||||
|
||||
assert_eq!(map.get("realm").unwrap(), "api@example.org");
|
||||
assert_eq!(map.get("qop").unwrap(), "auth");
|
||||
assert_eq!(map.get("algorithm").unwrap(), "SHA-512-256");
|
||||
assert_eq!(
|
||||
map.get("nonce").unwrap(),
|
||||
"5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK"
|
||||
);
|
||||
assert_eq!(
|
||||
map.get("opaque").unwrap(),
|
||||
"HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS"
|
||||
);
|
||||
assert_eq!(map.get("charset").unwrap(), "UTF-8");
|
||||
assert_eq!(map.get("userhash").unwrap(), "true");
|
||||
}
|
||||
assert_eq!(map.get("realm").unwrap(), "api@example.org");
|
||||
assert_eq!(map.get("qop").unwrap(), "auth");
|
||||
assert_eq!(map.get("algorithm").unwrap(), "SHA-512-256");
|
||||
assert_eq!(
|
||||
map.get("nonce").unwrap(),
|
||||
"5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK"
|
||||
);
|
||||
assert_eq!(
|
||||
map.get("opaque").unwrap(),
|
||||
"HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS"
|
||||
);
|
||||
assert_eq!(map.get("charset").unwrap(), "UTF-8");
|
||||
assert_eq!(map.get("userhash").unwrap(), "true");
|
||||
}
|
||||
|
||||
{
|
||||
let src = r#"realm="api@example.org""#;
|
||||
let map = parse_header_map(src).unwrap();
|
||||
assert_eq!(map.get("realm").unwrap(), "api@example.org");
|
||||
}
|
||||
#[test]
|
||||
fn test_parse_header_map2()
|
||||
{
|
||||
let src = r#"realm="api@example.org""#;
|
||||
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).unwrap();
|
||||
assert_eq!(map.get("realm").unwrap(), "api@example.org");
|
||||
}
|
||||
#[test]
|
||||
fn test_parse_header_map3() {
|
||||
let src = r#"realm=api@example.org"#;
|
||||
let map = parse_header_map(src).unwrap();
|
||||
assert_eq!(map.get("realm").unwrap(), "api@example.org");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_header_map4() {
|
||||
{
|
||||
let src = "";
|
||||
let map = parse_header_map(src).unwrap();
|
||||
@@ -594,12 +653,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_www_hdr_parse() {
|
||||
{
|
||||
// most things are parsed here...
|
||||
let src = r#"
|
||||
// most things are parsed here...
|
||||
let src = r#"
|
||||
realm="api@example.org",
|
||||
qop="auth",
|
||||
domain="/my/nice/url /login /logout"
|
||||
domain="/my/nice/url /login /logout",
|
||||
algorithm=SHA-512-256,
|
||||
nonce="5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK",
|
||||
opaque="HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS",
|
||||
@@ -607,79 +665,134 @@ mod tests {
|
||||
userhash=true
|
||||
"#;
|
||||
|
||||
let parsed = WwwAuthenticateHeader::from_str(src).unwrap();
|
||||
let parsed = WwwAuthenticateHeader::from_str(src).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
parsed,
|
||||
WwwAuthenticateHeader {
|
||||
domain: Some(vec![
|
||||
"/my/nice/url".to_string(),
|
||||
"/login".to_string(),
|
||||
"/logout".to_string(),
|
||||
]),
|
||||
realm: "api@example.org".to_string(),
|
||||
nonce: "5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK".to_string(),
|
||||
opaque: Some("HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS".to_string()),
|
||||
stale: false,
|
||||
algorithm: Algorithm::new(AlgorithmType::SHA2_512_256, false),
|
||||
qop: Some(vec![Qop::AUTH]),
|
||||
userhash: true,
|
||||
charset: Charset::UTF8,
|
||||
nc: 0,
|
||||
}
|
||||
)
|
||||
}
|
||||
assert_eq!(
|
||||
parsed,
|
||||
WwwAuthenticateHeader {
|
||||
domain: Some(vec![
|
||||
"/my/nice/url".to_string(),
|
||||
"/login".to_string(),
|
||||
"/logout".to_string(),
|
||||
]),
|
||||
realm: "api@example.org".to_string(),
|
||||
nonce: "5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK".to_string(),
|
||||
opaque: Some("HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS".to_string()),
|
||||
stale: false,
|
||||
algorithm: Algorithm::new(AlgorithmType::SHA2_512_256, false),
|
||||
qop: Some(vec![Qop::AUTH]),
|
||||
userhash: true,
|
||||
charset: Charset::UTF8,
|
||||
nc: 0,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
// verify some defaults
|
||||
let src = r#"
|
||||
realm="a long realm with\\, weird \" characters",
|
||||
qop="auth-int",
|
||||
nonce="bla bla nonce aaaaa",
|
||||
stale=TRUE
|
||||
"#;
|
||||
#[test]
|
||||
fn test_www_hdr_tostring() {
|
||||
let mut hdr = WwwAuthenticateHeader {
|
||||
domain: Some(vec![
|
||||
"/my/nice/url".to_string(),
|
||||
"/login".to_string(),
|
||||
"/logout".to_string(),
|
||||
]),
|
||||
realm: "api@example.org".to_string(),
|
||||
nonce: "5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK".to_string(),
|
||||
opaque: Some("HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS".to_string()),
|
||||
stale: false,
|
||||
algorithm: Algorithm::new(AlgorithmType::SHA2_512_256, false),
|
||||
qop: Some(vec![Qop::AUTH]),
|
||||
userhash: true,
|
||||
charset: Charset::UTF8,
|
||||
nc: 0,
|
||||
};
|
||||
|
||||
let parsed = WwwAuthenticateHeader::from_str(src).unwrap();
|
||||
assert_eq!(
|
||||
r#"Digest realm="api@example.org",
|
||||
qop="auth",
|
||||
domain="/my/nice/url /login /logout",
|
||||
algorithm=SHA-512-256,
|
||||
nonce="5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK",
|
||||
opaque="HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS",
|
||||
charset=UTF-8,
|
||||
userhash=true"#.replace(",\n ", ", "), hdr.to_string());
|
||||
|
||||
assert_eq!(
|
||||
parsed,
|
||||
WwwAuthenticateHeader {
|
||||
domain: None,
|
||||
realm: "a long realm with\\, weird \" characters".to_string(),
|
||||
nonce: "bla bla nonce aaaaa".to_string(),
|
||||
opaque: None,
|
||||
stale: true,
|
||||
algorithm: Algorithm::default(),
|
||||
qop: Some(vec![Qop::AUTH_INT]),
|
||||
userhash: false,
|
||||
charset: Charset::ASCII,
|
||||
nc: 0,
|
||||
}
|
||||
)
|
||||
}
|
||||
hdr.stale=true;
|
||||
hdr.userhash=false;
|
||||
hdr.opaque = None;
|
||||
hdr.qop = None;
|
||||
|
||||
{
|
||||
// check that it correctly ignores leading Digest
|
||||
let src = r#"Digest realm="aaa", nonce="bbb""#;
|
||||
assert_eq!(
|
||||
r#"Digest realm="api@example.org",
|
||||
domain="/my/nice/url /login /logout",
|
||||
stale=true,
|
||||
algorithm=SHA-512-256,
|
||||
nonce="5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK",
|
||||
charset=UTF-8"#.replace(",\n ", ", "), hdr.to_string());
|
||||
|
||||
let parsed = WwwAuthenticateHeader::from_str(src).unwrap();
|
||||
hdr.qop = Some(vec![Qop::AUTH, Qop::AUTH_INT]);
|
||||
|
||||
assert_eq!(
|
||||
parsed,
|
||||
WwwAuthenticateHeader {
|
||||
domain: None,
|
||||
realm: "aaa".to_string(),
|
||||
nonce: "bbb".to_string(),
|
||||
opaque: None,
|
||||
stale: false,
|
||||
algorithm: Algorithm::default(),
|
||||
qop: None,
|
||||
userhash: false,
|
||||
charset: Charset::ASCII,
|
||||
nc: 0,
|
||||
}
|
||||
)
|
||||
}
|
||||
assert_eq!(
|
||||
r#"Digest realm="api@example.org",
|
||||
qop="auth, auth-int",
|
||||
domain="/my/nice/url /login /logout",
|
||||
stale=true,
|
||||
algorithm=SHA-512-256,
|
||||
nonce="5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK",
|
||||
charset=UTF-8"#.replace(",\n ", ", "), hdr.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_www_hdr_parse2() {
|
||||
// verify some defaults
|
||||
let src = r#"
|
||||
realm="a long realm with\\, weird \" characters",
|
||||
qop="auth-int",
|
||||
nonce="bla bla nonce aaaaa",
|
||||
stale=TRUE
|
||||
"#;
|
||||
|
||||
let parsed = WwwAuthenticateHeader::from_str(src).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
parsed,
|
||||
WwwAuthenticateHeader {
|
||||
domain: None,
|
||||
realm: "a long realm with\\, weird \" characters".to_string(),
|
||||
nonce: "bla bla nonce aaaaa".to_string(),
|
||||
opaque: None,
|
||||
stale: true,
|
||||
algorithm: Algorithm::default(),
|
||||
qop: Some(vec![Qop::AUTH_INT]),
|
||||
userhash: false,
|
||||
charset: Charset::ASCII,
|
||||
nc: 0,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_www_hdr_parse3() {
|
||||
// check that it correctly ignores leading Digest
|
||||
let src = r#"Digest realm="aaa", nonce="bbb""#;
|
||||
|
||||
let parsed = WwwAuthenticateHeader::from_str(src).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
parsed,
|
||||
WwwAuthenticateHeader {
|
||||
domain: None,
|
||||
realm: "aaa".to_string(),
|
||||
nonce: "bbb".to_string(),
|
||||
opaque: None,
|
||||
stale: false,
|
||||
algorithm: Algorithm::default(),
|
||||
qop: None,
|
||||
userhash: false,
|
||||
charset: Charset::ASCII,
|
||||
nc: 0,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -708,7 +821,7 @@ Digest username="Mufasa",
|
||||
response="1949323746fe6a43ef61f9606e7febea",
|
||||
opaque="5ccc069c403ebaf9f0171e9517f40e41"
|
||||
"#
|
||||
.trim()
|
||||
.trim()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -745,7 +858,7 @@ Digest username="Mufasa",
|
||||
opaque="5ccc069c403ebaf9f0171e9517f40e41",
|
||||
algorithm=MD5
|
||||
"#
|
||||
.trim()
|
||||
.trim()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -782,7 +895,7 @@ Digest username="Mufasa",
|
||||
opaque="FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS",
|
||||
algorithm=MD5
|
||||
"#
|
||||
.trim()
|
||||
.trim()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -830,7 +943,7 @@ Digest username="Mufasa",
|
||||
opaque="FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS",
|
||||
algorithm=SHA-256
|
||||
"#
|
||||
.trim()
|
||||
.trim()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +154,15 @@ impl FromStr for Charset {
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Charset {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(match self {
|
||||
Charset::ASCII => "ASCII",
|
||||
Charset::UTF8 => "UTF-8",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP method (used when generating the response hash for some Qop options)
|
||||
#[derive(Debug)]
|
||||
pub enum HttpMethod {
|
||||
|
||||
Reference in New Issue
Block a user