use std::{convert::TryFrom, fmt::Write as _, io::Write as _};
use digest::Digest;
use crate::{
char_classes, ChallengeRef, ParamValue, PasswordParams, C_ATTR, C_ESCAPABLE, C_QDTEXT,
};
#[derive(Copy, Clone, Debug)]
#[repr(u8)]
#[non_exhaustive]
pub enum Qop {
Auth = 1,
AuthInt = 2,
}
impl Qop {
fn as_str(self) -> &'static str {
match self {
Qop::Auth => "auth",
Qop::AuthInt => "auth-int",
}
}
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub struct QopSet(u8);
impl std::fmt::Debug for QopSet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut l = f.debug_set();
if (self.0 & Qop::Auth as u8) != 0 {
l.entry(&"auth");
}
if (self.0 & Qop::AuthInt as u8) != 0 {
l.entry(&"auth-int");
}
l.finish()
}
}
impl std::ops::BitAnd<Qop> for QopSet {
type Output = bool;
fn bitand(self, rhs: Qop) -> Self::Output {
(self.0 & (rhs as u8)) != 0
}
}
#[derive(Eq, PartialEq)]
pub struct DigestClient {
buf: Box<str>,
domain_start: u16,
opaque_start: u16,
nonce_start: u16,
algorithm: Algorithm,
session: bool,
stale: bool,
rfc2069_compat: bool,
userhash: bool,
qop: QopSet,
nc: u32,
}
impl DigestClient {
#[inline]
pub fn realm(&self) -> &str {
&self.buf[..self.domain_start as usize]
}
#[inline]
pub fn domain(&self) -> &str {
&self.buf[self.domain_start as usize..self.opaque_start as usize]
}
#[inline]
pub fn nonce(&self) -> &str {
&self.buf[self.nonce_start as usize..]
}
#[inline]
pub fn opaque(&self) -> Option<&str> {
if self.opaque_start == self.nonce_start {
None
} else {
Some(&self.buf[self.opaque_start as usize..self.nonce_start as usize])
}
}
#[inline]
pub fn stale(&self) -> bool {
self.stale
}
#[inline]
pub fn rfc2069_compat(&self) -> bool {
self.rfc2069_compat
}
#[inline]
pub fn algorithm(&self) -> Algorithm {
self.algorithm
}
#[inline]
pub fn session(&self) -> bool {
self.session
}
#[inline]
pub fn qop(&self) -> QopSet {
self.qop
}
#[inline]
pub fn nonce_count(&self) -> u32 {
self.nc
}
#[inline]
pub fn respond(&mut self, p: &PasswordParams) -> Result<String, String> {
self.respond_inner(p, &new_random_cnonce())
}
#[inline]
pub fn respond_with_testing_cnonce(
&mut self,
p: &PasswordParams,
cnonce: &str,
) -> Result<String, String> {
self.respond_inner(p, cnonce)
}
fn respond_inner(&mut self, p: &PasswordParams, cnonce: &str) -> Result<String, String> {
let realm = self.realm();
let mut h_a1 = self.algorithm.h(&[
p.username.as_bytes(),
b":",
realm.as_bytes(),
b":",
p.password.as_bytes(),
]);
if self.session {
h_a1 = self.algorithm.h(&[
h_a1.as_bytes(),
b":",
self.nonce().as_bytes(),
b":",
cnonce.as_bytes(),
]);
}
let (h_a2, qop);
if let (Some(body), true) = (p.body, self.qop & Qop::AuthInt) {
h_a2 = self
.algorithm
.h(&[p.method.as_bytes(), b":", p.uri.as_bytes(), b":", body]);
qop = Qop::AuthInt;
} else if self.qop & Qop::Auth {
h_a2 = self
.algorithm
.h(&[p.method.as_bytes(), b":", p.uri.as_bytes()]);
qop = Qop::Auth;
} else {
return Err("no supported/available qop".into());
}
let nc = self.nc.checked_add(1).ok_or("nonce count exhausted")?;
let mut hex_nc = [0u8; 8];
let _ = write!(&mut hex_nc[..], "{:08x}", nc);
let str_hex_nc = match std::str::from_utf8(&hex_nc[..]) {
Ok(h) => h,
Err(_) => unreachable!(),
};
let response = if self.rfc2069_compat {
self.algorithm.h(&[
h_a1.as_bytes(),
b":",
self.nonce().as_bytes(),
b":",
h_a2.as_bytes(),
])
} else {
self.algorithm.h(&[
h_a1.as_bytes(),
b":",
self.nonce().as_bytes(),
b":",
&hex_nc[..],
b":",
cnonce.as_bytes(),
b":",
qop.as_str().as_bytes(),
b":",
h_a2.as_bytes(),
])
};
let mut out = String::with_capacity(128);
out.push_str("Digest ");
if self.userhash {
let hashed = self
.algorithm
.h(&[p.username.as_bytes(), b":", realm.as_bytes()]);
append_quoted_key_value(&mut out, "username", &hashed)?;
append_unquoted_key_value(&mut out, "userhash", "true");
} else if is_valid_quoted_value(p.username) {
append_quoted_key_value(&mut out, "username", p.username)?;
} else {
append_extended_key_value(&mut out, "username", p.username);
}
append_quoted_key_value(&mut out, "realm", self.realm())?;
append_quoted_key_value(&mut out, "uri", p.uri)?;
append_quoted_key_value(&mut out, "nonce", self.nonce())?;
if !self.rfc2069_compat {
append_unquoted_key_value(&mut out, "algorithm", self.algorithm.as_str(self.session));
append_unquoted_key_value(&mut out, "nc", str_hex_nc);
append_quoted_key_value(&mut out, "cnonce", cnonce)?;
append_unquoted_key_value(&mut out, "qop", qop.as_str());
}
append_quoted_key_value(&mut out, "response", &response)?;
if let Some(o) = self.opaque() {
append_quoted_key_value(&mut out, "opaque", o)?;
}
out.truncate(out.len() - 2); self.nc = nc;
Ok(out)
}
}
impl TryFrom<&ChallengeRef<'_>> for DigestClient {
type Error = String;
fn try_from(value: &ChallengeRef<'_>) -> Result<Self, Self::Error> {
if !value.scheme.eq_ignore_ascii_case("Digest") {
return Err(format!(
"DigestClientContext doesn't support challenge scheme {:?}",
value.scheme
));
}
let mut buf_len = 0;
let mut unused_len = 0;
let mut realm = None;
let mut domain = None;
let mut nonce = None;
let mut opaque = None;
let mut stale = false;
let mut algorithm_and_session = None;
let mut qop_str = None;
let mut userhash_str = None;
for (k, v) in &value.params {
if store_param(k, v, "realm", &mut realm, &mut buf_len)?
|| store_param(k, v, "domain", &mut domain, &mut buf_len)?
|| store_param(k, v, "nonce", &mut nonce, &mut buf_len)?
|| store_param(k, v, "opaque", &mut opaque, &mut buf_len)?
|| store_param(k, v, "qop", &mut qop_str, &mut unused_len)?
|| store_param(k, v, "userhash", &mut userhash_str, &mut unused_len)?
{
} else if k.eq_ignore_ascii_case("stale") {
stale = v.escaped.eq_ignore_ascii_case("true");
} else if k.eq_ignore_ascii_case("algorithm") {
algorithm_and_session = Some(Algorithm::parse(v.escaped)?);
}
}
let realm = realm.ok_or("missing required parameter realm")?;
let nonce = nonce.ok_or("missing required parameter nonce")?;
if buf_len > u16::MAX as usize {
return Err(format!(
"Unescaped parameters' length {} exceeds u16::MAX!",
buf_len
));
}
let algorithm_and_session = algorithm_and_session.unwrap_or((Algorithm::Md5, false));
let mut buf = String::with_capacity(buf_len);
let mut qop = QopSet(0);
let rfc2069_compat = if let Some(qop_str) = qop_str {
let qop_str = qop_str.unescaped_with_scratch(&mut buf);
for v in qop_str.split(',') {
let v = v.trim();
if v.eq_ignore_ascii_case("auth") {
qop.0 |= Qop::Auth as u8;
} else if v.eq_ignore_ascii_case("auth-int") {
qop.0 |= Qop::AuthInt as u8;
}
}
if qop.0 == 0 {
return Err(format!("no supported qop in {:?}", qop_str));
}
buf.clear();
false
} else {
qop.0 |= Qop::Auth as u8;
true
};
let userhash;
if let Some(userhash_str) = userhash_str {
let userhash_str = userhash_str.unescaped_with_scratch(&mut buf);
userhash = userhash_str.eq_ignore_ascii_case("true");
buf.clear();
} else {
userhash = false;
};
realm.append_unescaped(&mut buf);
let domain_start = buf.len();
if let Some(d) = domain {
d.append_unescaped(&mut buf);
}
let opaque_start = buf.len();
if let Some(o) = opaque {
o.append_unescaped(&mut buf);
}
let nonce_start = buf.len();
nonce.append_unescaped(&mut buf);
Ok(DigestClient {
buf: buf.into_boxed_str(),
domain_start: domain_start as u16,
opaque_start: opaque_start as u16,
nonce_start: nonce_start as u16,
algorithm: algorithm_and_session.0,
session: algorithm_and_session.1,
stale,
rfc2069_compat,
userhash,
qop,
nc: 0,
})
}
}
impl std::fmt::Debug for DigestClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DigestClient")
.field("realm", &self.realm())
.field("domain", &self.domain())
.field("opaque", &self.opaque())
.field("nonce", &self.nonce())
.field("algorithm", &self.algorithm.as_str(self.session))
.field("stale", &self.stale)
.field("qop", &self.qop)
.field("rfc2069_compat", &self.rfc2069_compat)
.field("userhash", &self.userhash)
.field("nc", &self.nc)
.finish()
}
}
fn store_param<'v, 'tmp>(
k: &'tmp str,
v: &'v ParamValue<'v>,
expected_k: &'tmp str,
set_v: &'tmp mut Option<&'v ParamValue<'v>>,
add_len: &'tmp mut usize,
) -> Result<bool, String> {
if !k.eq_ignore_ascii_case(expected_k) {
return Ok(false);
}
if set_v.is_some() {
return Err(format!("duplicate parameter {:?}", k));
}
*add_len += v.unescaped_len();
*set_v = Some(v);
Ok(true)
}
fn is_valid_quoted_value(s: &str) -> bool {
for &b in s.as_bytes() {
if char_classes(b) & (C_QDTEXT | C_ESCAPABLE) == 0 {
return false;
}
}
true
}
fn append_extended_key_value(out: &mut String, key: &str, value: &str) {
out.push_str(key);
out.push_str("*=UTF-8''");
for &b in value.as_bytes() {
if (char_classes(b) & C_ATTR) != 0 {
out.push(char::from(b));
} else {
let _ = write!(out, "%{:02X}", b);
}
}
out.push_str(", ");
}
fn append_unquoted_key_value(out: &mut String, key: &str, value: &str) {
out.push_str(key);
out.push('=');
out.push_str(value);
out.push_str(", ");
}
fn append_quoted_key_value(out: &mut String, key: &str, value: &str) -> Result<(), String> {
out.push_str(key);
out.push_str("=\"");
let mut first_unwritten = 0;
let bytes = value.as_bytes();
for (i, &b) in bytes.iter().enumerate() {
let class = char_classes(b);
if (class & C_QDTEXT) != 0 {
} else if (class & C_ESCAPABLE) != 0 {
out.push_str(&value[first_unwritten..i]);
out.push('\\');
out.push(char::from(b));
first_unwritten = i + 1;
} else {
return Err(format!("invalid {} value {:?}", key, value));
}
}
out.push_str(&value[first_unwritten..]);
out.push_str("\", ");
Ok(())
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum Algorithm {
Md5,
Sha256,
Sha512Trunc256,
}
impl Algorithm {
fn parse(s: &str) -> Result<(Self, bool), String> {
Ok(match s {
"MD5" => (Algorithm::Md5, false),
"MD5-sess" => (Algorithm::Md5, true),
"SHA-256" => (Algorithm::Sha256, false),
"SHA-256-sess" => (Algorithm::Sha256, true),
"SHA-512-256" => (Algorithm::Sha512Trunc256, false),
"SHA-512-256-sess" => (Algorithm::Sha512Trunc256, true),
_ => return Err(format!("unknown algorithm {:?}", s)),
})
}
fn as_str(&self, session: bool) -> &'static str {
match (self, session) {
(Algorithm::Md5, false) => "MD5",
(Algorithm::Md5, true) => "MD5-sess",
(Algorithm::Sha256, false) => "SHA-256",
(Algorithm::Sha256, true) => "SHA-256-sess",
(Algorithm::Sha512Trunc256, false) => "SHA-512-256",
(Algorithm::Sha512Trunc256, true) => "SHA-512-256-sess",
}
}
fn h(&self, items: &[&[u8]]) -> String {
match self {
Algorithm::Md5 => h(md5::Md5::new(), items),
Algorithm::Sha256 => h(sha2::Sha256::new(), items),
Algorithm::Sha512Trunc256 => h(sha2::Sha512Trunc256::new(), items),
}
}
}
fn h<D: Digest>(mut d: D, items: &[&[u8]]) -> String {
for i in items {
d.update(i);
}
hex::encode(d.finalize())
}
fn new_random_cnonce() -> String {
let raw: [u8; 16] = rand::random();
hex::encode(&raw[..])
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn sha256_and_md5() {
let www_authenticate = "\
Digest \
realm=\"http-auth@example.org\", \
qop=\"auth, auth-int\", \
algorithm=SHA-256, \
nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\", \
opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\", \
Digest \
realm=\"http-auth@example.org\", \
qop=\"auth, auth-int\", \
algorithm=MD5, \
nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\", \
opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\"";
let challenges = dbg!(crate::parse_challenges(www_authenticate).unwrap());
assert_eq!(challenges.len(), 2);
let ctxs: Result<Vec<_>, _> = challenges.iter().map(DigestClient::try_from).collect();
let mut ctxs = dbg!(ctxs.unwrap());
assert_eq!(ctxs[1].realm(), "http-auth@example.org");
assert_eq!(ctxs[1].domain(), "");
assert_eq!(
ctxs[1].nonce(),
"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v"
);
assert_eq!(
ctxs[1].opaque(),
Some("FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS")
);
assert_eq!(ctxs[1].stale(), false);
assert_eq!(ctxs[1].algorithm(), Algorithm::Md5);
assert_eq!(ctxs[1].qop().0, (Qop::Auth as u8) | (Qop::AuthInt as u8));
assert_eq!(ctxs[1].nonce_count(), 0);
let params = crate::PasswordParams {
username: "Mufasa",
password: "Circle of Life",
uri: "/dir/index.html",
body: None,
method: "GET",
};
assert_eq!(
&mut ctxs[0]
.respond_with_testing_cnonce(
¶ms,
"f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ"
)
.unwrap(),
"Digest username=\"Mufasa\", \
realm=\"http-auth@example.org\", \
uri=\"/dir/index.html\", \
nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\", \
algorithm=SHA-256, \
nc=00000001, \
cnonce=\"f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ\", \
qop=auth, \
response=\"753927fa0e85d155564e2e272a28d1802ca10daf4496794697cf8db5856cb6c1\", \
opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\""
);
assert_eq!(ctxs[0].nc, 1);
assert_eq!(
&mut ctxs[1]
.respond_with_testing_cnonce(
¶ms,
"f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ"
)
.unwrap(),
"Digest username=\"Mufasa\", \
realm=\"http-auth@example.org\", \
uri=\"/dir/index.html\", \
nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\", \
algorithm=MD5, \
nc=00000001, \
cnonce=\"f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ\", \
qop=auth, \
response=\"8ca523f5e9506fed4657c9700eebdbec\", \
opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\""
);
assert_eq!(ctxs[1].nc, 1);
}
#[test]
fn md5_sess() {
let www_authenticate = "\
Digest \
realm=\"http-auth@example.org\", \
qop=\"auth, auth-int\", \
algorithm=MD5-sess, \
nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\", \
opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\"";
let challenges = dbg!(crate::parse_challenges(www_authenticate).unwrap());
assert_eq!(challenges.len(), 1);
let ctxs: Result<Vec<_>, _> = challenges.iter().map(DigestClient::try_from).collect();
let mut ctxs = dbg!(ctxs.unwrap());
assert_eq!(ctxs[0].realm(), "http-auth@example.org");
assert_eq!(ctxs[0].domain(), "");
assert_eq!(
ctxs[0].nonce(),
"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v"
);
assert_eq!(
ctxs[0].opaque(),
Some("FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS")
);
assert_eq!(ctxs[0].stale(), false);
assert_eq!(ctxs[0].algorithm(), Algorithm::Md5);
assert_eq!(ctxs[0].session(), true);
assert_eq!(ctxs[0].qop().0, (Qop::Auth as u8) | (Qop::AuthInt as u8));
assert_eq!(ctxs[0].nonce_count(), 0);
let params = crate::PasswordParams {
username: "Mufasa",
password: "Circle of Life",
uri: "/dir/index.html",
body: None,
method: "GET",
};
assert_eq!(
&mut ctxs[0]
.respond_with_testing_cnonce(
¶ms,
"f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ"
)
.unwrap(),
"Digest username=\"Mufasa\", \
realm=\"http-auth@example.org\", \
uri=\"/dir/index.html\", \
nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\", \
algorithm=MD5-sess, \
nc=00000001, \
cnonce=\"f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ\", \
qop=auth, \
response=\"e783283f46242139c486a698fec7211d\", \
opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\""
);
assert_eq!(ctxs[0].nc, 1);
}
#[test]
fn sha512_256_charset() {
let www_authenticate = "\
Digest \
realm=\"api@example.org\", \
qop=\"auth\", \
algorithm=SHA-512-256, \
nonce=\"5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK\", \
opaque=\"HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS\", \
charset=UTF-8, \
userhash=true";
let challenges = dbg!(crate::parse_challenges(www_authenticate).unwrap());
assert_eq!(challenges.len(), 1);
let ctxs: Result<Vec<_>, _> = challenges.iter().map(DigestClient::try_from).collect();
let mut ctxs = dbg!(ctxs.unwrap());
assert_eq!(ctxs.len(), 1);
assert_eq!(ctxs[0].realm(), "api@example.org");
assert_eq!(ctxs[0].domain(), "");
assert_eq!(
ctxs[0].nonce(),
"5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK"
);
assert_eq!(
ctxs[0].opaque(),
Some("HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS")
);
assert_eq!(ctxs[0].stale, false);
assert_eq!(ctxs[0].userhash, true);
assert_eq!(ctxs[0].algorithm, Algorithm::Sha512Trunc256);
assert_eq!(ctxs[0].qop.0, Qop::Auth as u8);
assert_eq!(ctxs[0].nc, 0);
let params = crate::PasswordParams {
username: "J\u{E4}s\u{F8}n Doe",
password: "Secret, or not?",
uri: "/doe.json",
body: None,
method: "GET",
};
assert_eq!(
&mut ctxs[0]
.respond_with_testing_cnonce(
¶ms,
"NTg6RKcb9boFIAS3KrFK9BGeh+iDa/sm6jUMp2wds69v"
)
.unwrap(),
"\
Digest \
username=\"793263caabb707a56211940d90411ea4a575adeccb7e360aeb624ed06ece9b0b\", \
userhash=true, \
realm=\"api@example.org\", \
uri=\"/doe.json\", \
nonce=\"5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK\", \
algorithm=SHA-512-256, \
nc=00000001, \
cnonce=\"NTg6RKcb9boFIAS3KrFK9BGeh+iDa/sm6jUMp2wds69v\", \
qop=auth, \
response=\"3798d4131c277846293534c3edc11bd8a5e4cdcbff78b05db9d95eeb1cec68a5\", \
opaque=\"HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS\""
);
assert_eq!(ctxs[0].nc, 1);
ctxs[0].userhash = false;
ctxs[0].nc = 0;
assert_eq!(
&mut ctxs[0]
.respond_with_testing_cnonce(
¶ms,
"NTg6RKcb9boFIAS3KrFK9BGeh+iDa/sm6jUMp2wds69v"
)
.unwrap(),
"\
Digest \
username*=UTF-8''J%C3%A4s%C3%B8n%20Doe, \
realm=\"api@example.org\", \
uri=\"/doe.json\", \
nonce=\"5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK\", \
algorithm=SHA-512-256, \
nc=00000001, \
cnonce=\"NTg6RKcb9boFIAS3KrFK9BGeh+iDa/sm6jUMp2wds69v\", \
qop=auth, \
response=\"3798d4131c277846293534c3edc11bd8a5e4cdcbff78b05db9d95eeb1cec68a5\", \
opaque=\"HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS\""
);
assert_eq!(ctxs[0].nc, 1);
}
#[test]
fn rfc2069() {
let www_authenticate = "\
Digest \
realm=\"testrealm@host.com\", \
nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", \
opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"";
let challenges = dbg!(crate::parse_challenges(www_authenticate).unwrap());
assert_eq!(challenges.len(), 1);
let ctxs: Result<Vec<_>, _> = challenges.iter().map(DigestClient::try_from).collect();
let mut ctxs = dbg!(ctxs.unwrap());
assert_eq!(ctxs.len(), 1);
assert_eq!(ctxs[0].qop.0, Qop::Auth as u8);
assert_eq!(ctxs[0].rfc2069_compat, true);
let params = crate::PasswordParams {
username: "Mufasa",
password: "CircleOfLife",
uri: "/dir/index.html",
body: None,
method: "GET",
};
assert_eq!(
&mut ctxs[0]
.respond_with_testing_cnonce(¶ms, "unused")
.unwrap(),
"\
Digest \
username=\"Mufasa\", \
realm=\"testrealm@host.com\", \
uri=\"/dir/index.html\", \
nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", \
response=\"1949323746fe6a43ef61f9606e7febea\", \
opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"",
);
assert_eq!(ctxs[0].nc, 1);
}
#[test]
fn size() {
assert_eq!(
dbg!(std::mem::size_of::<DigestClient>()),
dbg!(std::mem::size_of::<Option<DigestClient>>()),
)
}
}