@ -212,6 +212,8 @@ impl Display for HttpMethod {
//endregion
//endregion
//region AuthContext
/// 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,
@ -236,13 +238,13 @@ pub struct AuthContext<'a> {
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 , ' s :' a , ' u :' a > ( username : & ' n str , password : & ' p str , uri : & ' u str ) -> Self {
pub fn new < ' n :' a , ' p :' a , ' u :' a > ( username : & ' n str , password : & ' p str , uri : & ' u str ) -> Self {
Self ::new_with_method ( username , password , uri , None , HttpMethod ::GET )
Self ::new_with_method ( username , password , uri , 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 , ' s :' a , ' u :' a , ' b :' a > (
pub fn new_post < ' n :' a , ' p :' a , ' u :' a , ' b :' a > (
username : & ' n str ,
username : & ' n str ,
password : & ' p str ,
password : & ' p str ,
uri : & ' u str ,
uri : & ' u str ,
@ -252,7 +254,7 @@ impl<'a> AuthContext<'a> {
}
}
/// 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 , ' s :' a , ' u :' a , ' b :' a > (
pub fn new_with_method < ' n :' a , ' p :' a , ' u :' a , ' b :' a > (
username : & ' n str ,
username : & ' n str ,
password : & ' p str ,
password : & ' p str ,
uri : & ' u str ,
uri : & ' u str ,
@ -279,8 +281,6 @@ impl<'a> AuthContext<'a> {
//region WwwAuthenticateHeader
//region WwwAuthenticateHeader
/// WWW-Authenticate header parsed from HTTP header value
/// WWW-Authenticate header parsed from HTTP header value
///
/// Use .from_str() to create it from data in a WWW-Authenticate HTTP header
#[ derive(Debug, PartialEq) ]
#[ derive(Debug, PartialEq) ]
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)
@ -303,22 +303,83 @@ pub struct WwwAuthenticateHeader {
pub userhash : bool ,
pub userhash : bool ,
/// Server-supported charset
/// Server-supported charset
pub charset : Charset ,
pub charset : Charset ,
/// NC - not part of the received header, but kept here for convenience and incremented each time
/// nc - not part of the received header, but kept here for convenience and incremented each time
/// a response is composed with the same nonce.
/// a response is composed with the same nonce.
pub nc : u32 ,
pub nc : u32 ,
}
}
impl WwwAuthenticateHeader {
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 < ' re , ' a :' re , ' c :' re > ( & ' a mut self , secrets : & ' c AuthContext ) -> Fallible < AuthorizationHeader < ' re > > {
pub fn respond < ' re , ' a :' re , ' c :' re > ( & ' a mut self , secrets : & ' c AuthContext ) -> Fallible < AuthorizationHeader < ' re > > {
AuthorizationHeader ::new ( self , secrets )
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 ) -> Fallible < Self > {
let mut input = input . trim ( ) ;
if input . starts_with ( "Digest" ) {
input = & input [ "Digest" . len ( ) .. ] ;
}
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 {
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 = > bail ! ( "realm not given" ) ,
} ,
nonce : match kv . remove ( "nonce" ) {
Some ( v ) = > v ,
None = > bail ! ( "nonce not given" ) ,
} ,
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 : algo ,
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
} )
}
}
}
}
/// Helper func that parses the key-value string received from server
/// Helper func that parses the key-value string received from server
///
pub fn parse_header_map ( input : & str ) -> Fallible < HashMap < String , String > > {
/// # Panics
/// if the input is malformed
pub fn parse_header_map ( input : & str ) -> HashMap < String , String > {
#[ derive(Debug) ]
#[ derive(Debug) ]
#[ allow(non_camel_case_types) ]
#[ allow(non_camel_case_types) ]
enum ParserState {
enum ParserState {
@ -401,86 +462,28 @@ pub fn parse_header_map(input: &str) -> HashMap<String, String> {
parsed . insert ( current_token . unwrap ( ) . to_string ( ) , current_value ) ; // consume the value here
parsed . insert ( current_token . unwrap ( ) . to_string ( ) , current_value ) ; // consume the value here
}
}
ParserState ::P_WHITE = > { }
ParserState ::P_WHITE = > { }
_ = > panic !( "Unexpected end state {:?}" , state ) ,
_ = > bail ! ( "Unexpected end state {:?}" , state ) ,
}
}
parsed
Ok ( parsed )
}
}
/// Parse the WWW-Authenticate header value into a struct
impl FromStr for WwwAuthenticateHeader {
impl FromStr for WwwAuthenticateHeader {
type Err = Error ;
type Err = Error ;
/// Parse HTTP header
/// Parse HTTP header
fn from_str ( input : & str ) -> Fallible < Self > {
fn from_str ( input : & str ) -> Fallible < Self > {
let mut input = input . trim ( ) ;
Self ::parse ( input )
if input . starts_with ( "Digest" ) {
input = & input [ "Digest" . len ( ) .. ] ;
}
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 {
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 = > bail ! ( "realm not given" ) ,
} ,
nonce : match kv . remove ( "nonce" ) {
Some ( v ) = > v ,
None = > bail ! ( "nonce not given" ) ,
} ,
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 : algo ,
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
} )
}
}
}
}
//endregion
//endregion
//region Authentication
//region AuthorizationHeader
/// Header sent back to the server, including password hashes
/// Header sent back to the server, including password hashes.
/// Always create it using ::new(), the hash calculation is done in the constructor.
///
/// This can also be obtained from the WwwAuthentication header with `WwwAuthenticateHeader::respond()`
///
///
/// Use .to_string() to generate the Authenticate HTTP header value
/// 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) ]
#[ derive(Debug) ]
pub struct AuthorizationHeader < ' ctx > {
pub struct AuthorizationHeader < ' ctx > {
/// The server header that triggered the authentication flow; used to retrieve some additional
/// The server header that triggered the authentication flow; used to retrieve some additional
@ -504,15 +507,19 @@ pub struct AuthorizationHeader<'ctx> {
}
}
impl < ' a > AuthorizationHeader < ' a > {
impl < ' a > AuthorizationHeader < ' a > {
/// Create from a prompt header and auth context, selecting suitable algorithm options.
/// Construct using a parsed prompt header and an auth context, selecting suitable algorithm
/// The header contains a 'nc' field that is incremented by this method.
/// 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 parsed WwwAuthenticateHeader, and - if the
/// For subsequent requests, simply reuse the same [`WwwAuthenticateHeader`](struct.WwwAuthenticateHeader.html)
/// server supports nonce reuse - it will work automatically
/// and - if the server supports nonce reuse - it will work automatically.
///
///
/// Returns Error if the source header is malformed so much that we can't figure out
/// # Errors
/// a proper response.
///
pub fn new < ' p :' a , ' s :' a > (
/// 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 < ' p :' a , ' s :' a > (
prompt : & ' p mut WwwAuthenticateHeader , context : & ' s AuthContext
prompt : & ' p mut WwwAuthenticateHeader , context : & ' s AuthContext
) -> Fallible < AuthorizationHeader < ' a > > {
) -> Fallible < AuthorizationHeader < ' a > > {
// figure out which QOP option to use
// figure out which QOP option to use
@ -591,16 +598,16 @@ 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 = match prompt . userhash {
let username = if prompt . userhash {
true = > h . hash (
h . hash (
format! (
format! (
"{username}:{realm}" ,
"{username}:{realm}" ,
username = context . username ,
username = context . username ,
realm = prompt . realm
realm = prompt . realm
) . as_bytes ( )
)
)
. as_bytes ( ) ,
} else {
) ,
context . username . to_owned ( )
false = > context . username . to_owned ( ) ,
} ;
} ;
let qop : Option < Qop > = qop_algo . into ( ) ;
let qop : Option < Qop > = qop_algo . into ( ) ;
@ -609,7 +616,7 @@ impl<'a> AuthorizationHeader<'a> {
let ha2 = h . hash_str ( & a2 ) ;
let ha2 = h . hash_str ( & a2 ) ;
// Increment nonce counter
// Increment nonce counter
prompt . nc = prompt . nc + 1 ;
prompt . nc + = 1 ;
// Compute the response
// Compute the response
let response = match & qop {
let response = match & qop {
@ -676,7 +683,7 @@ impl<'a> Display for AuthorizationHeader<'a> {
f . write_fmt ( format_args! ( ", uri=\"{}\"" , self . uri ) ) ? ;
f . write_fmt ( format_args! ( ", uri=\"{}\"" , self . uri ) ) ? ;
if self . prompt . qop . is_some ( ) {
if self . prompt . qop . is_some ( ) & & self . cnonce . is_some ( ) {
f . write_fmt ( format_args! (
f . write_fmt ( format_args! (
", qop={qop}, nc={nc:08x}, cnonce=\"{cnonce}\"" ,
", qop={qop}, nc={nc:08x}, cnonce=\"{cnonce}\"" ,
qop = self . qop . as_ref ( ) . unwrap ( ) ,
qop = self . qop . as_ref ( ) . unwrap ( ) ,
@ -736,7 +743,7 @@ mod tests {
userhash = true
userhash = true
" #;
" #;
let map = parse_header_map ( src ) ;
let map = parse_header_map ( src ) . unwrap ( ) ;
assert_eq! ( map . get ( "realm" ) . unwrap ( ) , "api@example.org" ) ;
assert_eq! ( map . get ( "realm" ) . unwrap ( ) , "api@example.org" ) ;
assert_eq! ( map . get ( "qop" ) . unwrap ( ) , "auth" ) ;
assert_eq! ( map . get ( "qop" ) . unwrap ( ) , "auth" ) ;
@ -755,19 +762,19 @@ mod tests {
{
{
let src = r#"realm="api@example.org""# ;
let src = r#"realm="api@example.org""# ;
let map = parse_header_map ( src ) ;
let map = parse_header_map ( src ) . unwrap ( ) ;
assert_eq! ( map . get ( "realm" ) . unwrap ( ) , "api@example.org" ) ;
assert_eq! ( map . get ( "realm" ) . unwrap ( ) , "api@example.org" ) ;
}
}
{
{
let src = r#"realm=api@example.org"# ;
let src = r#"realm=api@example.org"# ;
let map = parse_header_map ( src ) ;
let map = parse_header_map ( src ) . unwrap ( ) ;
assert_eq! ( map . get ( "realm" ) . unwrap ( ) , "api@example.org" ) ;
assert_eq! ( map . get ( "realm" ) . unwrap ( ) , "api@example.org" ) ;
}
}
{
{
let src = "" ;
let src = "" ;
let map = parse_header_map ( src ) ;
let map = parse_header_map ( src ) . unwrap ( ) ;
assert_eq! ( map . is_empty ( ) , true ) ;
assert_eq! ( map . is_empty ( ) , true ) ;
}
}
}
}
@ -874,7 +881,7 @@ mod tests {
let context = AuthContext ::new ( "Mufasa" , "CircleOfLife" , "/dir/index.html" ) ;
let context = AuthContext ::new ( "Mufasa" , "CircleOfLife" , "/dir/index.html" ) ;
let mut prompt = WwwAuthenticateHeader ::from_str ( src ) . unwrap ( ) ;
let mut prompt = WwwAuthenticateHeader ::from_str ( src ) . unwrap ( ) ;
let answer = AuthorizationHeader ::new ( & 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 str = answer . to_string ( ) . replace ( ", " , ",\n " ) ;
@ -906,7 +913,7 @@ Digest username="Mufasa",
context . set_custom_cnonce ( "0a4f113b" ) ;
context . set_custom_cnonce ( "0a4f113b" ) ;
let mut prompt = WwwAuthenticateHeader ::from_str ( src ) . unwrap ( ) ;
let mut prompt = WwwAuthenticateHeader ::from_str ( src ) . unwrap ( ) ;
let answer = AuthorizationHeader ::new ( & mut prompt , & context ) . unwrap ( ) ;
let answer = AuthorizationHeader ::from_prompt ( & mut prompt , & context ) . unwrap ( ) ;
let str = answer . to_string ( ) . replace ( ", " , ",\n " ) ;
let str = answer . to_string ( ) . replace ( ", " , ",\n " ) ;
//println!("{}", str);
//println!("{}", str);
@ -944,7 +951,7 @@ Digest username="Mufasa",
context . set_custom_cnonce ( "f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ" ) ;
context . set_custom_cnonce ( "f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ" ) ;
let mut prompt = WwwAuthenticateHeader ::from_str ( src ) . unwrap ( ) ;
let mut prompt = WwwAuthenticateHeader ::from_str ( src ) . unwrap ( ) ;
let answer = AuthorizationHeader ::new ( & mut prompt , & context ) . unwrap ( ) ;
let answer = AuthorizationHeader ::from_prompt ( & mut prompt , & context ) . unwrap ( ) ;
let str = answer . to_string ( ) . replace ( ", " , ",\n " ) ;
let str = answer . to_string ( ) . replace ( ", " , ",\n " ) ;
@ -991,7 +998,7 @@ Digest username="Mufasa",
// };
// };
let mut prompt = WwwAuthenticateHeader ::from_str ( src ) . unwrap ( ) ;
let mut prompt = WwwAuthenticateHeader ::from_str ( src ) . unwrap ( ) ;
let answer = AuthorizationHeader ::new ( & mut prompt , & context ) . unwrap ( ) ;
let answer = AuthorizationHeader ::from_prompt ( & mut prompt , & context ) . unwrap ( ) ;
let str = answer . to_string ( ) . replace ( ", " , ",\n " ) ;
let str = answer . to_string ( ) . replace ( ", " , ",\n " ) ;
//println!("{}", str);
//println!("{}", str);