You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
digest_auth_rs/src/digest.rs

1116 lines
35 KiB

use rand::Rng;
use std::collections::HashMap;
use std::fmt::{self, Display, Formatter};
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 {
fn quote_for_digest(&self) -> String;
}
impl QuoteForDigest for &str {
fn quote_for_digest(&self) -> String {
self.to_string().quote_for_digest()
}
}
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: &[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),
}
}
}
/// Helper func that parses the key-value string received from server
fn parse_header_map(input: &str) -> Result<HashMap<String, String>> {
#[derive(Debug)]
#[allow(non_camel_case_types)]
enum ParserState {
P_WHITE,
P_NAME(usize),
P_VALUE_BEGIN,
P_VALUE_QUOTED,
P_VALUE_QUOTED_NEXTLITERAL,
P_VALUE_PLAIN,
}
let mut state = ParserState::P_WHITE;
let mut parsed = HashMap::<String, String>::new();
let mut current_token = None;
let mut current_value = String::new();
for (char_n, c) in input.chars().enumerate() {
match state {
ParserState::P_WHITE => {
if c.is_alphabetic() {
state = ParserState::P_NAME(char_n);
}
}
ParserState::P_NAME(name_start) => {
if c == '=' {
current_token = Some(&input[name_start..char_n]);
state = ParserState::P_VALUE_BEGIN;
}
}
ParserState::P_VALUE_BEGIN => {
current_value.clear();
state = match c {
'"' => ParserState::P_VALUE_QUOTED,
_ => {
current_value.push(c);
ParserState::P_VALUE_PLAIN
}
};
}
ParserState::P_VALUE_QUOTED => {
match c {
'"' => {
parsed.insert(current_token.unwrap().to_string(), current_value.clone());
current_token = None;
current_value.clear();
state = ParserState::P_WHITE;
}
'\\' => {
state = ParserState::P_VALUE_QUOTED_NEXTLITERAL;
}
_ => {
current_value.push(c);
}
};
}
ParserState::P_VALUE_PLAIN => {
if c == ',' || c.is_ascii_whitespace() {
parsed.insert(current_token.unwrap().to_string(), current_value.clone());
current_token = None;
current_value.clear();
state = ParserState::P_WHITE;
} else {
current_value.push(c);
}
}
ParserState::P_VALUE_QUOTED_NEXTLITERAL => {
current_value.push(c);
state = ParserState::P_VALUE_QUOTED
}
}
}
match state {
ParserState::P_VALUE_PLAIN => {
parsed.insert(current_token.unwrap().to_string(), current_value); // consume the value here
}
ParserState::P_WHITE => {}
_ => return Err(InvalidHeaderSyntax(input.into())),
}
Ok(parsed)
}
/// Login attempt context
///
/// All fields are borrowed to reduce runtime overhead; this struct should not be stored anywhere,
/// it is normally meaningful only for the one request.
#[derive(Debug)]
pub struct AuthContext<'a> {
/// Login username
pub username: Cow<'a, str>,
/// Login password (plain)
pub password: Cow<'a, str>,
/// Requested URI (not a domain! should start with a slash)
pub uri: Cow<'a, str>,
/// Request payload body - used for auth-int (auth with integrity check)
/// May be left out if not using auth-int
pub body: Option<Cow<'a, [u8]>>,
/// HTTP method used (defaults to GET)
pub method: HttpMethod<'a>,
/// Spoofed client nonce (use only for tests; a random nonce is generated automatically)
pub cnonce: Option<Cow<'a, str>>,
}
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<UN, PW, UR>(username: UN, password: PW, uri: UR) -> Self
where
UN: Into<Cow<'a, str>>,
PW: Into<Cow<'a, str>>,
UR: Into<Cow<'a, str>>,
{
Self::new_with_method(
username,
password,
uri,
Option::<&'a [u8]>::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<UN, PW, UR, BD>(username: UN, password: PW, uri: UR, body: Option<BD>) -> Self
where
UN: Into<Cow<'a, str>>,
PW: Into<Cow<'a, str>>,
UR: Into<Cow<'a, str>>,
BD: Into<Cow<'a, [u8]>>,
{
Self::new_with_method(username, password, uri, body, HttpMethod::POST)
}
/// Construct a new context with arbitrary verb and, optionally, a payload body
pub fn new_with_method<UN, PW, UR, BD>(
username: UN,
password: PW,
uri: UR,
body: Option<BD>,
method: HttpMethod<'a>,
) -> Self
where
UN: Into<Cow<'a, str>>,
PW: Into<Cow<'a, str>>,
UR: Into<Cow<'a, str>>,
BD: Into<Cow<'a, [u8]>>,
{
Self {
username: username.into(),
password: password.into(),
uri: uri.into(),
body: body.map(Into::into),
method,
cnonce: None,
}
}
/// Set cnonce to the given value
pub fn set_custom_cnonce<CN>(&mut self, cnonce: CN)
where
CN: Into<Cow<'a, str>>,
{
self.cnonce = Some(cnonce.into());
}
}
/// WWW-Authenticate header parsed from HTTP header value
#[derive(Debug, PartialEq, Clone)]
pub struct WwwAuthenticateHeader {
/// Domain is a list of URIs that will accept the same digest. None if not given (i.e applies to all)
pub domain: Option<Vec<String>>,
/// Authorization realm (i.e. hostname, serial number...)
pub realm: String,
/// Server nonce
pub nonce: String,
/// Server opaque string
pub opaque: Option<String>,
/// True if the server nonce expired.
/// This is sent in response to an auth attempt with an older digest.
/// The response should contain a new WWW-Authenticate header.
pub stale: bool,
/// Hashing algo
pub algorithm: Algorithm,
/// Digest algorithm variant
pub qop: Option<Vec<Qop>>,
/// Flag that the server supports user-hashes
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
/// a response is composed with the same nonce.
pub nc: u32,
}
impl FromStr for WwwAuthenticateHeader {
type Err = crate::Error;
/// Parse HTTP header
fn from_str(input: &str) -> Result<Self> {
Self::parse(input)
}
}
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(&mut self, secrets: &AuthContext) -> Result<AuthorizationHeader> {
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) -> Result<Self> {
let mut input = input.trim();
// Remove leading "Digest"
if input.starts_with("Digest") {
input = &input["Digest".len()..];
}
let mut kv = parse_header_map(input)?;
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 => return Err(MissingRequired("realm", input.into())),
},
nonce: match kv.remove("nonce") {
Some(v) => v,
None => return Err(MissingRequired("nonce", input.into())),
},
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: match kv.get("algorithm") {
Some(a) => Algorithm::from_str(&a)?,
_ => Algorithm::default(),
},
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,
})
}
}
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(())
}
}
/// Header sent back to the server, including password hashes.
///
/// 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, PartialEq, Clone)]
pub struct AuthorizationHeader {
/// Authorization realm
pub realm: String,
/// Server nonce
pub nonce: String,
/// Server opaque
pub opaque: Option<String>,
/// Flag that userhash was used
pub userhash: bool,
/// Hash algorithm
pub algorithm: Algorithm,
/// Computed digest
pub response: String,
/// Username or hash (owned because of the computed hash)
pub username: String,
/// Requested URI
pub uri: String,
/// QOP chosen from the list offered by server, if any
/// None in legacy compat mode (RFC 2069)
pub qop: Option<Qop>,
/// Client nonce
/// None in legacy compat mode (RFC 2069)
pub cnonce: Option<String>,
/// How many requests have been signed with this server nonce
/// Not used in legacy compat mode (RFC 2069) - it's still incremented though
pub nc: u32,
}
impl AuthorizationHeader {
/// 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.
///
/// # Errors
///
/// 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(
prompt: &mut WwwAuthenticateHeader,
context: &AuthContext,
) -> Result<AuthorizationHeader> {
let qop = match &prompt.qop {
None => None,
Some(vec) => {
// this is at least RFC2617, qop was given
if vec.contains(&Qop::AUTH_INT) {
Some(Qop::AUTH_INT)
} else if vec.contains(&Qop::AUTH) {
// "auth" is the second best after "auth-int"
Some(Qop::AUTH)
} else {
// parser bug - prompt.qop should have been None
return Err(BadQopOptions(join_vec(vec, ", ")));
}
}
};
prompt.nc += 1;
let mut hdr = AuthorizationHeader {
realm: prompt.realm.clone(),
nonce: prompt.nonce.clone(),
opaque: prompt.opaque.clone(),
userhash: prompt.userhash,
algorithm: prompt.algorithm,
response: String::default(),
username: String::default(),
uri: context.uri.as_ref().into(),
qop,
cnonce: context
.cnonce
.as_ref()
.map(AsRef::as_ref)
.map(ToOwned::to_owned), // Will be generated if needed, if build_hash is set and this is None
nc: prompt.nc,
};
hdr.digest(context);
Ok(hdr)
}
/// Build the response digest from Auth Context.
///
/// This function is used by client to fill the Authorization header.
/// It can be used by server using a known password to replicate the hash
/// and then compare "response".
///
/// This function sets cnonce if it was None before, or reuses it.
///
/// Fields updated in the Authorization header:
/// - qop (if it was auth-int before but no body was given in context)
/// - cnonce (if it was None before)
/// - username copied from context
/// - response
pub fn digest(&mut self, context: &AuthContext) {
// figure out which QOP option to use
let qop_algo = match self.qop {
None => QopAlgo::NONE,
Some(Qop::AUTH_INT) => {
if let Some(b) = &context.body {
QopAlgo::AUTH_INT(b.as_ref())
} else {
// fallback
QopAlgo::AUTH
}
}
Some(Qop::AUTH) => QopAlgo::AUTH,
};
let h = &self.algorithm;
let cnonce = {
match &self.cnonce {
Some(cnonce) => cnonce.to_owned(),
None => {
let mut rng = rand::thread_rng();
let nonce_bytes: [u8; 16] = rng.gen();
hex::encode(nonce_bytes)
}
}
};
// a1 value for the hash algo. cnonce is generated if needed
let a1 = {
let a = format!(
"{name}:{realm}:{pw}",
name = context.username,
realm = self.realm,
pw = context.password
);
let sess = self.algorithm.sess;
if sess {
format!(
"{hash}:{nonce}:{cnonce}",
hash = h.hash(a.as_bytes()),
nonce = self.nonce,
cnonce = cnonce
)
} else {
a
}
};
// a2 value for the hash algo
let a2 = match qop_algo {
QopAlgo::AUTH | QopAlgo::NONE => {
format!("{method}:{uri}", method = context.method, uri = context.uri)
}
QopAlgo::AUTH_INT(body) => format!(
"{method}:{uri}:{bodyhash}",
method = context.method,
uri = context.uri,
bodyhash = h.hash(body)
),
};
// hashed or unhashed username - always hash if server wants it
let username = if self.userhash {
h.hash(
format!(
"{username}:{realm}",
username = context.username,
realm = self.realm
)
.as_bytes(),
)
} else {
context.username.as_ref().to_owned()
};
let qop: Option<Qop> = qop_algo.into();
let ha1 = h.hash_str(&a1);
let ha2 = h.hash_str(&a2);
self.response = match &qop {
Some(q) => {
let tmp = format!(
"{ha1}:{nonce}:{nc:08x}:{cnonce}:{qop}:{ha2}",
ha1 = ha1,
nonce = self.nonce,
nc = self.nc,
cnonce = cnonce,
qop = q,
ha2 = ha2
);
h.hash(tmp.as_bytes())
}
None => {
let tmp = format!(
"{ha1}:{nonce}:{ha2}",
ha1 = ha1,
nonce = self.nonce,
ha2 = ha2
);
h.hash(tmp.as_bytes())
}
};
self.qop = qop;
self.username = username;
self.cnonce = qop.map(|_| cnonce);
}
/// Produce a header string (also accessible through the Display trait)
pub fn to_header_string(&self) -> String {
self.to_string()
}
/// Construct from the `Authorization` header string
///
/// # Errors
/// If the header is malformed (e.g. missing mandatory fields)
pub fn parse(input: &str) -> Result<Self> {
let mut input = input.trim();
// Remove leading "Digest"
if input.starts_with("Digest") {
input = &input["Digest".len()..];
}
let mut kv = parse_header_map(input)?;
let mut auth = Self {
username: match kv.remove("username") {
Some(v) => v,
None => return Err(MissingRequired("username", input.into())),
},
realm: match kv.remove("realm") {
Some(v) => v,
None => return Err(MissingRequired("realm", input.into())),
},
nonce: match kv.remove("nonce") {
Some(v) => v,
None => return Err(MissingRequired("nonce", input.into())),
},
uri: match kv.remove("uri") {
Some(v) => v,
None => return Err(MissingRequired("uri", input.into())),
},
response: match kv.remove("response") {
Some(v) => v,
None => return Err(MissingRequired("response", input.into())),
},
qop: kv.remove("qop").map(|s| Qop::from_str(&s)).transpose()?,
nc: match kv.remove("nc") {
Some(v) => u32::from_str_radix(&v, 16)?,
None => 1,
},
cnonce: kv.remove("cnonce"),
opaque: kv.remove("opaque"),
algorithm: match kv.get("algorithm") {
Some(a) => Algorithm::from_str(&a)?,
_ => Algorithm::default(),
},
userhash: match kv.get("userhash") {
Some(v) => &v.to_ascii_lowercase() == "true",
None => false,
},
};
if auth.qop.is_some() {
if auth.cnonce.is_none() {
return Err(MissingRequired("cnonce", input.into()));
}
} else {
// cnonce must not be set if qop is not given, clear it.
auth.cnonce = None;
}
Ok(auth)
}
}
impl Display for AuthorizationHeader {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let mut entries = Vec::<NamedTag>::new();
f.write_str("Digest ")?;
entries.push(NamedTag::Quoted("username", (&self.username).into()));
entries.push(NamedTag::Quoted("realm", (&self.realm).into()));
entries.push(NamedTag::Quoted("nonce", (&self.nonce).into()));
entries.push(NamedTag::Quoted("uri", (&self.uri).into()));
if self.qop.is_some() && self.cnonce.is_some() {
entries.push(NamedTag::Plain(
"qop",
self.qop.as_ref().unwrap().to_string().into(),
));
entries.push(NamedTag::Plain("nc", format!("{:08x}", self.nc).into()));
entries.push(NamedTag::Quoted(
"cnonce",
self.cnonce.as_ref().unwrap().into(),
));
}
entries.push(NamedTag::Quoted("response", (&self.response).into()));
if let Some(opaque) = &self.opaque {
entries.push(NamedTag::Quoted("opaque", opaque.into()));
}
// algorithm can be omitted if it is the default value (or in legacy compat mode)
if self.qop.is_some() || self.algorithm.algo != AlgorithmType::MD5 {
entries.push(NamedTag::Plain(
"algorithm",
self.algorithm.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(())
}
}
impl FromStr for AuthorizationHeader {
type Err = crate::Error;
/// Parse HTTP header
fn from_str(input: &str) -> Result<Self> {
Self::parse(input)
}
}
#[cfg(test)]
mod tests {
use super::parse_header_map;
use super::Algorithm;
use super::AlgorithmType;
use super::AuthorizationHeader;
use super::Charset;
use super::Qop;
use super::WwwAuthenticateHeader;
use crate::digest::AuthContext;
use std::str::FromStr;
#[test]
fn test_parse_header_map() {
let src = r#"
realm="api@example.org",
qop="auth",
algorithm=SHA-512-256,
nonce="5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK",
opaque="HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS",
charset=UTF-8,
userhash=true
"#;
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");
}
#[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");
}
#[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();
assert_eq!(map.is_empty(), true);
}
}
#[test]
fn test_www_hdr_parse() {
// most things are parsed here...
let src = r#"
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
"#;
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,
}
)
}
#[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,
};
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()
);
hdr.stale = true;
hdr.userhash = false;
hdr.opaque = None;
hdr.qop = None;
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()
);
hdr.qop = Some(vec![Qop::AUTH, Qop::AUTH_INT]);
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]
fn test_rfc2069() {
let src = r#"
Digest
realm="testrealm@host.com",
nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
opaque="5ccc069c403ebaf9f0171e9517f40e41"
"#;
let context = AuthContext::new("Mufasa", "CircleOfLife", "/dir/index.html");
let mut prompt = WwwAuthenticateHeader::from_str(src).unwrap();
let answer = AuthorizationHeader::from_prompt(&mut prompt, &context).unwrap();
// The spec has a wrong hash in the example, see errata
let s = answer.to_string().replace(", ", ",\n ");
assert_eq!(
s,
r#"
Digest username="Mufasa",
realm="testrealm@host.com",
nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
uri="/dir/index.html",
response="1949323746fe6a43ef61f9606e7febea",
opaque="5ccc069c403ebaf9f0171e9517f40e41"
"#
.trim()
);
// Try round trip
let parsed = AuthorizationHeader::parse(&s).unwrap();
assert_eq!(answer, parsed);
}
#[test]
fn test_rfc2617() {
let src = r#"
Digest
realm="testrealm@host.com",
qop="auth,auth-int",
nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
opaque="5ccc069c403ebaf9f0171e9517f40e41"
"#;
let mut context = AuthContext::new("Mufasa", "Circle Of Life", "/dir/index.html");
context.set_custom_cnonce("0a4f113b");
assert_eq!(context.body, None);
let mut prompt = WwwAuthenticateHeader::from_str(src).unwrap();
let answer = AuthorizationHeader::from_prompt(&mut prompt, &context).unwrap();
let s = answer.to_string().replace(", ", ",\n ");
//println!("{}", str);
assert_eq!(
s,
r#"
Digest username="Mufasa",
realm="testrealm@host.com",
nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
uri="/dir/index.html",
qop=auth,
nc=00000001,
cnonce="0a4f113b",
response="6629fae49393a05397450978507c4ef1",
opaque="5ccc069c403ebaf9f0171e9517f40e41",
algorithm=MD5
"#
.trim()
);
// Try round trip
let parsed = AuthorizationHeader::parse(&s).unwrap();
assert_eq!(answer, parsed);
}
#[test]
fn test_rfc7616_md5() {
let src = r#"
Digest
realm="http-auth@example.org",
qop="auth, auth-int",
algorithm=MD5,
nonce="7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v",
opaque="FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS"
"#;
let mut context = AuthContext::new("Mufasa", "Circle of Life", "/dir/index.html");
context.set_custom_cnonce("f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ");
let mut prompt = WwwAuthenticateHeader::from_str(src).unwrap();
let answer = AuthorizationHeader::from_prompt(&mut prompt, &context).unwrap();
let s = answer.to_string().replace(", ", ",\n ");
assert_eq!(
s,
r#"
Digest username="Mufasa",
realm="http-auth@example.org",
nonce="7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v",
uri="/dir/index.html",
qop=auth,
nc=00000001,
cnonce="f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ",
response="8ca523f5e9506fed4657c9700eebdbec",
opaque="FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS",
algorithm=MD5
"#
.trim()
);
// Try round trip
let parsed = AuthorizationHeader::parse(&s).unwrap();
assert_eq!(answer, parsed);
}
#[test]
fn test_rfc7616_sha256() {
let src = r#"
Digest
realm="http-auth@example.org",
qop="auth, auth-int",
algorithm=SHA-256,
nonce="7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v",
opaque="FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS"
"#;
let mut context = AuthContext::new("Mufasa", "Circle of Life", "/dir/index.html");
context.set_custom_cnonce("f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ");
//
// let secrets = AuthSecrets {
// username: "Mufasa".to_string(),
// password: "Circle of Life".to_string(),
// uri: "/dir/index.html".to_string(),
// body: None,
// method: HttpMethod::GET,
// nc: 1,
// cnonce: Some("f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ".to_string()),
// };
let mut prompt = WwwAuthenticateHeader::from_str(src).unwrap();
let answer = AuthorizationHeader::from_prompt(&mut prompt, &context).unwrap();
let s = answer.to_string().replace(", ", ",\n ");
//println!("{}", str);
assert_eq!(
s,
r#"
Digest username="Mufasa",
realm="http-auth@example.org",
nonce="7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v",
uri="/dir/index.html",
qop=auth,
nc=00000001,
cnonce="f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ",
response="753927fa0e85d155564e2e272a28d1802ca10daf4496794697cf8db5856cb6c1",
opaque="FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS",
algorithm=SHA-256
"#
.trim()
);
// Try round trip
let parsed = AuthorizationHeader::parse(&s).unwrap();
assert_eq!(answer, parsed);
}
}