add parsing support to AuthorizationHeader

pull/4/head
Ondřej Hruška 5 years ago
parent f2346416cc
commit 1947affd75
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 574
      src/digest.rs
  2. 8
      src/enums.rs
  3. 14
      src/error.rs

@ -57,6 +57,98 @@ impl Display for NamedTag<'_> {
} }
} }
/// 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 /// Login attempt context
/// ///
/// All fields are borrowed to reduce runtime overhead; this struct should not be stored anywhere, /// All fields are borrowed to reduce runtime overhead; this struct should not be stored anywhere,
@ -64,64 +156,81 @@ impl Display for NamedTag<'_> {
#[derive(Debug)] #[derive(Debug)]
pub struct AuthContext<'a> { pub struct AuthContext<'a> {
/// Login username /// Login username
pub username: &'a str, pub username: Cow<'a, str>,
/// Login password (plain) /// Login password (plain)
pub password: &'a str, pub password: Cow<'a, str>,
/// Requested URI (not a domain! should start with a slash) /// Requested URI (not a domain! should start with a slash)
pub uri: &'a str, pub uri: Cow<'a, str>,
/// Request payload body - used for auth-int (auth with integrity check) /// Request payload body - used for auth-int (auth with integrity check)
/// May be left out if not using auth-int /// May be left out if not using auth-int
pub body: Option<&'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,
/// 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<&'a str>, pub cnonce: Option<Cow<'a, str>>,
} }
impl<'a> AuthContext<'a> { impl<'a> AuthContext<'a> {
/// Construct a new context with the GET verb and no payload body. /// Construct a new context with the GET verb and no payload body.
/// See the other constructors if this does not fit your situation. /// See the other constructors if this does not fit your situation.
pub fn new<'n: 'a, 'p: 'a, 'u: 'a>(username: &'n str, password: &'p str, uri: &'u str) -> Self { pub fn new<UN, PW, UR>(username: UN, password: PW, uri: UR) -> Self
Self::new_with_method(username, password, uri, None, HttpMethod::GET) 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). /// 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. /// See the other constructors if this does not fit your situation.
pub fn new_post<'n: 'a, 'p: 'a, 'u: 'a, 'b: 'a>( pub fn new_post<UN, PW, UR, BD>(
username: &'n str, username: UN,
password: &'p str, password: PW,
uri: &'u str, uri: UR,
body: Option<&'b [u8]>, body: Option<BD>,
) -> Self { ) -> 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) Self::new_with_method(username, password, uri, body, HttpMethod::POST)
} }
/// Construct a new context with arbitrary verb and, optionally, a payload body /// Construct a new context with arbitrary verb and, optionally, a payload body
pub fn new_with_method<'n: 'a, 'p: 'a, 'u: 'a, 'b: 'a>( pub fn new_with_method<UN, PW, UR, BD>(
username: &'n str, username: UN,
password: &'p str, password: PW,
uri: &'u str, uri: UR,
body: Option<&'b [u8]>, body: Option<BD>,
method: HttpMethod, method: HttpMethod,
) -> Self { ) -> Self
where UN: Into<Cow<'a, str>>,
PW: Into<Cow<'a, str>>,
UR: Into<Cow<'a, str>>,
BD: Into<Cow<'a, [u8]>>
{
Self { Self {
username, username : username.into(),
password, password : password.into(),
uri, uri : uri.into(),
body, body : body.map(Into::into),
method, method,
cnonce: None, cnonce: None,
} }
} }
/// Set cnonce to the given value /// Set cnonce to the given value
pub fn set_custom_cnonce<'x: 'a>(&mut self, cnonce: &'x str) { pub fn set_custom_cnonce<CN>(&mut self, cnonce: CN)
self.cnonce = Some(cnonce); where
CN: Into<Cow<'a, str>>
{
self.cnonce = Some(cnonce.into());
} }
} }
/// WWW-Authenticate header parsed from HTTP header value /// WWW-Authenticate header parsed from HTTP header value
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Clone)]
pub struct WwwAuthenticateHeader { pub struct WwwAuthenticateHeader {
/// Domain is a list of URIs that will accept the same digest. None if not given (i.e applies to all) /// 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>>, pub domain: Option<Vec<String>>,
@ -160,10 +269,10 @@ impl FromStr for WwwAuthenticateHeader {
impl WwwAuthenticateHeader { impl WwwAuthenticateHeader {
/// Generate an [`AuthorizationHeader`](struct.AuthorizationHeader.html) to be sent to the server in a new request. /// 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. /// The [`self.nc`](struct.AuthorizationHeader.html#structfield.nc) field is incremented.
pub fn respond<'re, 'a: 're, 'c: 're>( pub fn respond(
&'a mut self, &mut self,
secrets: &'c AuthContext, secrets: &AuthContext,
) -> Result<AuthorizationHeader<'re>> { ) -> Result<AuthorizationHeader> {
AuthorizationHeader::from_prompt(self, secrets) AuthorizationHeader::from_prompt(self, secrets)
} }
@ -181,13 +290,6 @@ impl WwwAuthenticateHeader {
let mut kv = parse_header_map(input)?; 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 { Ok(Self {
domain: if let Some(domains) = kv.get("domain") { domain: if let Some(domains) = kv.get("domain") {
let domains: Vec<&str> = domains.split(' ').collect(); let domains: Vec<&str> = domains.split(' ').collect();
@ -197,11 +299,11 @@ impl WwwAuthenticateHeader {
}, },
realm: match kv.remove("realm") { realm: match kv.remove("realm") {
Some(v) => v, Some(v) => v,
None => return Err(MissingRealm(input.into())), None => return Err(MissingRequired("realm", input.into())),
}, },
nonce: match kv.remove("nonce") { nonce: match kv.remove("nonce") {
Some(v) => v, Some(v) => v,
None => return Err(MissingNonce(input.into())), None => return Err(MissingRequired("nonce", input.into())),
}, },
opaque: kv.remove("opaque"), opaque: kv.remove("opaque"),
stale: match kv.get("stale") { stale: match kv.get("stale") {
@ -212,7 +314,10 @@ impl WwwAuthenticateHeader {
Some(v) => Charset::from_str(v)?, Some(v) => Charset::from_str(v)?,
None => Charset::ASCII, None => Charset::ASCII,
}, },
algorithm: algo, algorithm: match kv.get("algorithm") {
Some(a) => Algorithm::from_str(&a)?,
_ => Algorithm::default(),
},
qop: if let Some(domains) = kv.get("qop") { qop: if let Some(domains) = kv.get("qop") {
let domains: Vec<&str> = domains.split(',').collect(); let domains: Vec<&str> = domains.split(',').collect();
let mut qops = vec![]; let mut qops = vec![];
@ -264,9 +369,7 @@ impl Display for WwwAuthenticateHeader {
} }
for (i, e) in entries.iter().enumerate() { for (i, e) in entries.iter().enumerate() {
if i > 0 { if i > 0 { f.write_str(", ")?; }
f.write_str(", ")?;
}
f.write_str(&e.to_string())?; f.write_str(&e.to_string())?;
} }
@ -274,112 +377,29 @@ impl Display for WwwAuthenticateHeader {
} }
} }
/// 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)
}
/// Header sent back to the server, including password hashes. /// Header sent back to the server, including password hashes.
/// ///
/// This can be obtained by calling [`AuthorizationHeader::from_prompt()`](#method.from_prompt), /// This can be obtained by calling [`AuthorizationHeader::from_prompt()`](#method.from_prompt),
/// or from the [`WwwAuthenticateHeader`](struct.WwwAuthenticateHeader.html) prompt struct /// or from the [`WwwAuthenticateHeader`](struct.WwwAuthenticateHeader.html) prompt struct
/// with [`.respond()`](struct.WwwAuthenticateHeader.html#method.respond) /// with [`.respond()`](struct.WwwAuthenticateHeader.html#method.respond)
#[derive(Debug)] #[derive(Debug, PartialEq, Clone)]
pub struct AuthorizationHeader<'ctx> { pub struct AuthorizationHeader {
/// The server header that triggered the authentication flow; used to retrieve some additional /// Authorization realm
/// fields when serializing to the header string pub realm : String,
pub prompt: &'ctx WwwAuthenticateHeader, /// 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 /// Computed digest
pub response: String, pub response: String,
/// Username or hash (owned because of the computed hash) /// Username or hash (owned because of the computed hash)
pub username: String, pub username: String,
/// Requested URI /// Requested URI
pub uri: &'ctx str, pub uri: String,
/// QOP chosen from the list offered by server, if any /// QOP chosen from the list offered by server, if any
/// None in legacy compat mode (RFC 2069) /// None in legacy compat mode (RFC 2069)
pub qop: Option<Qop>, pub qop: Option<Qop>,
@ -391,7 +411,7 @@ pub struct AuthorizationHeader<'ctx> {
pub nc: u32, pub nc: u32,
} }
impl<'a> AuthorizationHeader<'a> { impl AuthorizationHeader {
/// Construct using a parsed prompt header and an auth context, selecting suitable algorithm /// Construct using a parsed prompt header and an auth context, selecting suitable algorithm
/// options. The [`WwwAuthenticateHeader`](struct.WwwAuthenticateHeader.html) struct contains a /// options. The [`WwwAuthenticateHeader`](struct.WwwAuthenticateHeader.html) struct contains a
/// [`nc`](struct.WwwAuthenticateHeader.html#structfield.nc) field that is incremented by this /// [`nc`](struct.WwwAuthenticateHeader.html#structfield.nc) field that is incremented by this
@ -404,30 +424,20 @@ impl<'a> AuthorizationHeader<'a> {
/// ///
/// Fails if the source header is malformed so much that we can't figure out /// 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) /// a proper response (e.g. given but invalid QOP options)
pub fn from_prompt<'p: 'a, 's: 'a>( pub fn from_prompt(
prompt: &'p mut WwwAuthenticateHeader, prompt: &mut WwwAuthenticateHeader,
context: &'s AuthContext, context: &AuthContext,
) -> Result<AuthorizationHeader<'a>> { ) -> Result<AuthorizationHeader> {
// figure out which QOP option to use
let empty_vec = vec![]; let qop = match &prompt.qop {
let qop_algo = match &prompt.qop { None => None,
None => QopAlgo::NONE,
Some(vec) => { Some(vec) => {
// this is at least RFC2617, qop was given // this is at least RFC2617, qop was given
if vec.contains(&Qop::AUTH_INT) { if vec.contains(&Qop::AUTH_INT) {
if let Some(b) = context.body { Some(Qop::AUTH_INT)
QopAlgo::AUTH_INT(b)
} else {
// we have no body. Fall back to regular auth if possible, or use empty
if vec.contains(&Qop::AUTH) {
QopAlgo::AUTH
} else {
QopAlgo::AUTH_INT(&empty_vec)
}
}
} else if vec.contains(&Qop::AUTH) { } else if vec.contains(&Qop::AUTH) {
// "auth" is the second best after "auth-int" // "auth" is the second best after "auth-int"
QopAlgo::AUTH Some(Qop::AUTH)
} else { } else {
// parser bug - prompt.qop should have been None // parser bug - prompt.qop should have been None
return Err(BadQopOptions(join_vec(vec, ", "))); return Err(BadQopOptions(join_vec(vec, ", ")));
@ -435,15 +445,68 @@ impl<'a> AuthorizationHeader<'a> {
} }
}; };
let h = &prompt.algorithm; 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.build_hash(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 build_hash(&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 = { let cnonce = {
match context.cnonce { match &self.cnonce {
Some(cnonce) => cnonce.to_owned(), Some(cnonce) => cnonce.to_owned(),
None => { None => {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let nonce_bytes: [u8; 16] = rng.gen(); let nonce_bytes: [u8; 16] = rng.gen();
hex::encode(nonce_bytes) let cnonce = hex::encode(nonce_bytes);
cnonce
} }
} }
}; };
@ -453,16 +516,16 @@ impl<'a> AuthorizationHeader<'a> {
let a = format!( let a = format!(
"{name}:{realm}:{pw}", "{name}:{realm}:{pw}",
name = context.username, name = context.username,
realm = prompt.realm, realm = self.realm,
pw = context.password pw = context.password
); );
let sess = prompt.algorithm.sess; let sess = self.algorithm.sess;
if sess { if sess {
format!( format!(
"{hash}:{nonce}:{cnonce}", "{hash}:{nonce}:{cnonce}",
hash = h.hash(a.as_bytes()), hash = h.hash(a.as_bytes()),
nonce = prompt.nonce, nonce = self.nonce,
cnonce = cnonce cnonce = cnonce
) )
} else { } else {
@ -484,17 +547,17 @@ impl<'a> AuthorizationHeader<'a> {
}; };
// hashed or unhashed username - always hash if server wants it // hashed or unhashed username - always hash if server wants it
let username = if prompt.userhash { let username = if self.userhash {
h.hash( h.hash(
format!( format!(
"{username}:{realm}", "{username}:{realm}",
username = context.username, username = context.username,
realm = prompt.realm realm = self.realm
) )
.as_bytes(), .as_bytes(),
) )
} else { } else {
context.username.to_owned() context.username.as_ref().to_owned()
}; };
let qop: Option<Qop> = qop_algo.into(); let qop: Option<Qop> = qop_algo.into();
@ -502,17 +565,13 @@ impl<'a> AuthorizationHeader<'a> {
let ha1 = h.hash_str(&a1); let ha1 = h.hash_str(&a1);
let ha2 = h.hash_str(&a2); let ha2 = h.hash_str(&a2);
// Increment nonce counter self.response = match &qop {
prompt.nc += 1;
// Compute the response
let response = match &qop {
Some(q) => { Some(q) => {
let tmp = format!( let tmp = format!(
"{ha1}:{nonce}:{nc:08x}:{cnonce}:{qop}:{ha2}", "{ha1}:{nonce}:{nc:08x}:{cnonce}:{qop}:{ha2}",
ha1 = ha1, ha1 = ha1,
nonce = prompt.nonce, nonce = self.nonce,
nc = prompt.nc, nc = self.nc,
cnonce = cnonce, cnonce = cnonce,
qop = q, qop = q,
ha2 = ha2 ha2 = ha2
@ -523,69 +582,138 @@ impl<'a> AuthorizationHeader<'a> {
let tmp = format!( let tmp = format!(
"{ha1}:{nonce}:{ha2}", "{ha1}:{nonce}:{ha2}",
ha1 = ha1, ha1 = ha1,
nonce = prompt.nonce, nonce = self.nonce,
ha2 = ha2 ha2 = ha2
); );
h.hash(tmp.as_bytes()) h.hash(tmp.as_bytes())
} }
}; };
Ok(AuthorizationHeader { self.qop = qop;
prompt, self.username = username;
response, self.cnonce = qop.map(|_| cnonce);
username,
uri: context.uri,
qop,
cnonce: Some(cnonce),
nc: prompt.nc,
})
} }
/// Produce a header string (also accessible through the Display trait) /// Produce a header string (also accessible through the Display trait)
pub fn to_header_string(&self) -> String { pub fn to_header_string(&self) -> String {
// TODO move impl from Display here & clean it up
self.to_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_some() {
return Err(MissingRequired("cnonce", input.into()))
}
} else {
// cnonce must not be set if qop is not given, clear it.
auth.cnonce = None;
} }
impl<'a> Display for AuthorizationHeader<'a> { Ok(auth)
}
}
impl Display for AuthorizationHeader {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let mut entries = Vec::<NamedTag>::new();
f.write_str("Digest ")?; f.write_str("Digest ")?;
//TODO charset shenanigans with username* (UTF-8 charset) entries.push(NamedTag::Quoted("username", (&self.username).into()));
write!(f, "username=\"{uname}\", realm=\"{realm}\", nonce=\"{nonce}\", uri=\"{uri}\"", entries.push(NamedTag::Quoted("realm", (&self.realm).into()));
uname = self.username.quote_for_digest(), entries.push(NamedTag::Quoted("nonce", (&self.nonce).into()));
realm = self.prompt.realm.quote_for_digest(), entries.push(NamedTag::Quoted("uri", (&self.uri).into()));
nonce = self.prompt.nonce.quote_for_digest(),
uri = self.uri)?;
if self.prompt.qop.is_some() && self.cnonce.is_some() { if self.qop.is_some() && self.cnonce.is_some() {
write!(f, ", qop={qop}, nc={nc:08x}, cnonce=\"{cnonce}\"", entries.push(NamedTag::Plain("qop", self.qop.as_ref().unwrap().to_string().into()));
qop = self.qop.as_ref().unwrap(), entries.push(NamedTag::Plain("nc", format!("{:08x}", self.nc).into()));
nc = self.nc, entries.push(NamedTag::Quoted("cnonce", self.cnonce.as_ref().unwrap().into()));
cnonce = self.cnonce.as_ref().unwrap().quote_for_digest(),
)?;
} }
write!(f, ", response=\"{}\"", self.response.quote_for_digest())?; entries.push(NamedTag::Quoted("response", (&self.response).into()));
if let Some(opaque) = &self.prompt.opaque { if let Some(opaque) = &self.opaque {
write!(f, ", opaque=\"{}\"", opaque.quote_for_digest())?; entries.push(NamedTag::Quoted("opaque", opaque.into()));
} }
// algorithm can be omitted if it is the default value (or in legacy compat mode) // 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 { if self.qop.is_some() || self.algorithm.algo != AlgorithmType::MD5 {
write!(f, ", algorithm={}", self.prompt.algorithm)?; entries.push(NamedTag::Plain("algorithm", self.algorithm.to_string().into()));
} }
if self.prompt.userhash { if self.userhash {
f.write_str(", userhash=true")?; 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(()) Ok(())
} }
} }
impl FromStr for AuthorizationHeader {
type Err = crate::Error;
/// Parse HTTP header
fn from_str(input: &str) -> Result<Self> {
Self::parse(input)
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::parse_header_map; use super::parse_header_map;
@ -810,9 +938,9 @@ r#"Digest realm="api@example.org",
let answer = AuthorizationHeader::from_prompt(&mut prompt, &context).unwrap(); let answer = AuthorizationHeader::from_prompt(&mut prompt, &context).unwrap();
// The spec has a wrong hash in the example, see errata // The spec has a wrong hash in the example, see errata
let str = answer.to_string().replace(", ", ",\n "); let s = answer.to_string().replace(", ", ",\n ");
assert_eq!( assert_eq!(
str, s,
r#" r#"
Digest username="Mufasa", Digest username="Mufasa",
realm="testrealm@host.com", realm="testrealm@host.com",
@ -823,6 +951,10 @@ Digest username="Mufasa",
"# "#
.trim() .trim()
); );
// Try round trip
let parsed = AuthorizationHeader::parse(&s).unwrap();
assert_eq!(answer, parsed);
} }
#[test] #[test]
@ -838,14 +970,16 @@ Digest username="Mufasa",
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("0a4f113b"); context.set_custom_cnonce("0a4f113b");
assert_eq!(context.body, None);
let mut prompt = WwwAuthenticateHeader::from_str(src).unwrap(); let mut prompt = WwwAuthenticateHeader::from_str(src).unwrap();
let answer = AuthorizationHeader::from_prompt(&mut prompt, &context).unwrap(); let answer = AuthorizationHeader::from_prompt(&mut prompt, &context).unwrap();
let str = answer.to_string().replace(", ", ",\n "); let s = answer.to_string().replace(", ", ",\n ");
//println!("{}", str); //println!("{}", str);
assert_eq!( assert_eq!(
str, s,
r#" r#"
Digest username="Mufasa", Digest username="Mufasa",
realm="testrealm@host.com", realm="testrealm@host.com",
@ -860,6 +994,10 @@ Digest username="Mufasa",
"# "#
.trim() .trim()
); );
// Try round trip
let parsed = AuthorizationHeader::parse(&s).unwrap();
assert_eq!(answer, parsed);
} }
#[test] #[test]
@ -879,10 +1017,10 @@ Digest username="Mufasa",
let mut prompt = WwwAuthenticateHeader::from_str(src).unwrap(); let mut prompt = WwwAuthenticateHeader::from_str(src).unwrap();
let answer = AuthorizationHeader::from_prompt(&mut prompt, &context).unwrap(); let answer = AuthorizationHeader::from_prompt(&mut prompt, &context).unwrap();
let str = answer.to_string().replace(", ", ",\n "); let s = answer.to_string().replace(", ", ",\n ");
assert_eq!( assert_eq!(
str, s,
r#" r#"
Digest username="Mufasa", Digest username="Mufasa",
realm="http-auth@example.org", realm="http-auth@example.org",
@ -897,6 +1035,10 @@ Digest username="Mufasa",
"# "#
.trim() .trim()
); );
// Try round trip
let parsed = AuthorizationHeader::parse(&s).unwrap();
assert_eq!(answer, parsed);
} }
#[test] #[test]
@ -926,11 +1068,11 @@ Digest username="Mufasa",
let mut prompt = WwwAuthenticateHeader::from_str(src).unwrap(); let mut prompt = WwwAuthenticateHeader::from_str(src).unwrap();
let answer = AuthorizationHeader::from_prompt(&mut prompt, &context).unwrap(); let answer = AuthorizationHeader::from_prompt(&mut prompt, &context).unwrap();
let str = answer.to_string().replace(", ", ",\n "); let s = answer.to_string().replace(", ", ",\n ");
//println!("{}", str); //println!("{}", str);
assert_eq!( assert_eq!(
str, s,
r#" r#"
Digest username="Mufasa", Digest username="Mufasa",
realm="http-auth@example.org", realm="http-auth@example.org",
@ -945,5 +1087,9 @@ Digest username="Mufasa",
"# "#
.trim() .trim()
); );
// Try round trip
let parsed = AuthorizationHeader::parse(&s).unwrap();
assert_eq!(answer, parsed);
} }
} }

@ -5,7 +5,7 @@ use std::fmt::{Display, Formatter};
use std::str::FromStr; use std::str::FromStr;
/// Algorithm type /// Algorithm type
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Clone, Copy)]
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]
pub enum AlgorithmType { pub enum AlgorithmType {
MD5, MD5,
@ -14,7 +14,7 @@ pub enum AlgorithmType {
} }
/// Algorithm and the -sess flag pair /// Algorithm and the -sess flag pair
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Clone, Copy)]
pub struct Algorithm { pub struct Algorithm {
pub algo: AlgorithmType, pub algo: AlgorithmType,
pub sess: bool, pub sess: bool,
@ -86,7 +86,7 @@ impl Display for Algorithm {
} }
/// QOP field values /// QOP field values
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Clone, Copy)]
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]
pub enum Qop { pub enum Qop {
AUTH, AUTH,
@ -136,7 +136,7 @@ impl<'a> Into<Option<Qop>> for QopAlgo<'a> {
} }
/// Charset field value as specified by the server /// Charset field value as specified by the server
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Clone)]
pub enum Charset { pub enum Charset {
ASCII, ASCII,
UTF8, UTF8,

@ -6,10 +6,10 @@ pub enum Error {
BadCharset(String), BadCharset(String),
UnknownAlgorithm(String), UnknownAlgorithm(String),
BadQop(String), BadQop(String),
MissingRealm(String), MissingRequired(&'static str, String),
MissingNonce(String),
InvalidHeaderSyntax(String), InvalidHeaderSyntax(String),
BadQopOptions(String), BadQopOptions(String),
NumParseError,
} }
pub type Result<T> = result::Result<T, Error>; pub type Result<T> = result::Result<T, Error>;
@ -22,10 +22,16 @@ impl Display for Error {
BadCharset(ctx) => write!(f, "Bad charset: {}", ctx), BadCharset(ctx) => write!(f, "Bad charset: {}", ctx),
UnknownAlgorithm(ctx) => write!(f, "Unknown algorithm: {}", ctx), UnknownAlgorithm(ctx) => write!(f, "Unknown algorithm: {}", ctx),
BadQop(ctx) => write!(f, "Bad Qop option: {}", ctx), BadQop(ctx) => write!(f, "Bad Qop option: {}", ctx),
MissingRealm(ctx) => write!(f, "Missing 'realm' in WWW-Authenticate: {}", ctx), MissingRequired(what, ctx) => write!(f, "Missing \"{}\" in header: {}", what, ctx),
MissingNonce(ctx) => write!(f, "Missing 'nonce' in WWW-Authenticate: {}", ctx),
InvalidHeaderSyntax(ctx) => write!(f, "Invalid header syntax: {}", ctx), InvalidHeaderSyntax(ctx) => write!(f, "Invalid header syntax: {}", ctx),
BadQopOptions(ctx) => write!(f, "Illegal Qop in prompt: {}", ctx), BadQopOptions(ctx) => write!(f, "Illegal Qop in prompt: {}", ctx),
NumParseError => write!(f, "Error parsing a number."),
} }
} }
} }
impl From<std::num::ParseIntError> for Error {
fn from(_: std::num::ParseIntError) -> Self {
NumParseError
}
}

Loading…
Cancel
Save