digest_auth-0.3.1/.cargo_vcs_info.json0000644000000001360000000000100133350ustar { "git": { "sha1": "fa3503e19acae8764d1804d9398a4814552939b2" }, "path_in_vcs": "" }digest_auth-0.3.1/.gitignore000064400000000000000000000000451046102023000141140ustar 00000000000000/target **/*.rs.bk Cargo.lock .idea digest_auth-0.3.1/CHANGELOG.md000064400000000000000000000004031046102023000137330ustar 00000000000000# 0.3.1 - Update deps # 0.3.0 - Added lifetime parameter to `HttpMethod` - Changed `HttpMethod` to `HttpMethod<'a>(Cow<'a, str>)` - Added unit tests - Added support for the `http` crate's `Method` struct (optional feature) # 0.2.4 - Update dependencies digest_auth-0.3.1/Cargo.toml0000644000000023170000000000100113360ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "digest_auth" version = "0.3.1" authors = ["Ondřej Hruška "] description = "Implementation of the Digest Auth algorithm as defined in IETF RFC 2069, 2617, and 7616, intended for HTTP clients" readme = "README.md" keywords = [ "http", "auth", "digest", ] categories = [ "authentication", "cryptography", "web-programming::http-client", ] license = "MIT" repository = "https://git.ondrovo.com/packages/digest_auth_rs" [dependencies.digest] version = "0.10" [dependencies.hex] version = "0.4" [dependencies.http] version = "0.2.4" optional = true [dependencies.md-5] version = "0.10" [dependencies.rand] version = "0.8" [dependencies.sha2] version = "0.10" [features] default = [] digest_auth-0.3.1/Cargo.toml.orig000064400000000000000000000012041046102023000150110ustar 00000000000000[package] name = "digest_auth" version = "0.3.1" authors = ["Ondřej Hruška "] edition = "2018" description = "Implementation of the Digest Auth algorithm as defined in IETF RFC 2069, 2617, and 7616, intended for HTTP clients" repository = "https://git.ondrovo.com/packages/digest_auth_rs" readme = "README.md" keywords = ["http", "auth", "digest"] categories = [ "authentication", "cryptography", "web-programming::http-client" ] license = "MIT" [dependencies] rand = "0.8" hex = "0.4" sha2 = "0.10" md-5 = "0.10" digest = "0.10" [dependencies.http] version = "0.2.4" optional = true [features] default = [] digest_auth-0.3.1/LICENSE.txt000064400000000000000000000020431046102023000137470ustar 00000000000000Copyright (c) 2019 Ondřej Hruška Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. digest_auth-0.3.1/README.md000064400000000000000000000003361046102023000134060ustar 00000000000000Rust 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. Please see the docs and tests for examples. digest_auth-0.3.1/src/digest.rs000064400000000000000000001053421046102023000145460ustar 00000000000000use 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(vec: &[T], sep: &str) -> String { vec.iter() .map(ToString::to_string) .collect::>() .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> { #[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::::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>, /// 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>, } 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(username: UN, password: PW, uri: UR) -> Self where UN: Into>, PW: Into>, UR: Into>, { 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(username: UN, password: PW, uri: UR, body: Option) -> Self where UN: Into>, PW: Into>, UR: Into>, BD: Into>, { 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( username: UN, password: PW, uri: UR, body: Option, method: HttpMethod<'a>, ) -> Self where UN: Into>, PW: Into>, UR: Into>, BD: Into>, { 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(&mut self, cnonce: CN) where CN: Into>, { 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>, /// Authorization realm (i.e. hostname, serial number...) pub realm: String, /// Server nonce pub nonce: String, /// Server opaque string pub opaque: Option, /// 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>, /// 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::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::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 { 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::::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, /// 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, /// Client nonce /// None in legacy compat mode (RFC 2069) pub cnonce: Option, /// 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 { 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_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 { 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::::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::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); } } digest_auth-0.3.1/src/enums.rs000064400000000000000000000330331046102023000144130ustar 00000000000000#![allow(clippy::upper_case_acronyms)] use crate::{Error, Error::*, Result}; use std::fmt; use std::fmt::{Display, Formatter}; use std::str::FromStr; use digest::{Digest, DynDigest}; use md5::Md5; use sha2::{Sha256, Sha512_256}; use std::borrow::Cow; /// Algorithm type #[derive(Debug, PartialEq, Clone, Copy)] #[allow(non_camel_case_types)] pub enum AlgorithmType { MD5, SHA2_256, SHA2_512_256, } /// Algorithm and the -sess flag pair #[derive(Debug, PartialEq, Clone, Copy)] pub struct Algorithm { pub algo: AlgorithmType, pub sess: bool, } impl Algorithm { /// Compose from algorithm type and the -sess flag pub fn new(algo: AlgorithmType, sess: bool) -> Algorithm { Algorithm { algo, sess } } /// Calculate a hash of bytes using the selected algorithm pub fn hash(self, bytes: &[u8]) -> String { let mut hash: Box = match self.algo { AlgorithmType::MD5 => Box::new(Md5::new()), AlgorithmType::SHA2_256 => Box::new(Sha256::new()), AlgorithmType::SHA2_512_256 => Box::new(Sha512_256::new()), }; hash.update(bytes); hex::encode(hash.finalize()) } /// Calculate a hash of string's bytes using the selected algorithm pub fn hash_str(self, bytes: &str) -> String { self.hash(bytes.as_bytes()) } } impl FromStr for Algorithm { type Err = Error; /// Parse from the format used in WWW-Authorization fn from_str(s: &str) -> Result { match s { "MD5" => Ok(Algorithm::new(AlgorithmType::MD5, false)), "MD5-sess" => Ok(Algorithm::new(AlgorithmType::MD5, true)), "SHA-256" => Ok(Algorithm::new(AlgorithmType::SHA2_256, false)), "SHA-256-sess" => Ok(Algorithm::new(AlgorithmType::SHA2_256, true)), "SHA-512-256" => Ok(Algorithm::new(AlgorithmType::SHA2_512_256, false)), "SHA-512-256-sess" => Ok(Algorithm::new(AlgorithmType::SHA2_512_256, true)), _ => Err(UnknownAlgorithm(s.into())), } } } impl Default for Algorithm { /// Get a MD5 instance fn default() -> Self { Algorithm::new(AlgorithmType::MD5, false) } } impl Display for Algorithm { /// Format to the form used in HTTP headers fn fmt(&self, f: &mut Formatter) -> fmt::Result { f.write_str(match self.algo { AlgorithmType::MD5 => "MD5", AlgorithmType::SHA2_256 => "SHA-256", AlgorithmType::SHA2_512_256 => "SHA-512-256", })?; if self.sess { f.write_str("-sess")?; } Ok(()) } } /// QOP field values #[derive(Debug, PartialEq, Clone, Copy)] #[allow(non_camel_case_types)] pub enum Qop { AUTH, AUTH_INT, } impl FromStr for Qop { type Err = Error; /// Parse from "auth" or "auth-int" as used in HTTP headers fn from_str(s: &str) -> Result { match s { "auth" => Ok(Qop::AUTH), "auth-int" => Ok(Qop::AUTH_INT), _ => Err(BadQop(s.into())), } } } impl Display for Qop { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(match self { Qop::AUTH => "auth", Qop::AUTH_INT => "auth-int", }) } } #[derive(Debug)] #[allow(non_camel_case_types)] pub enum QopAlgo<'a> { NONE, AUTH, AUTH_INT(&'a [u8]), } // casting back... impl<'a> From> for Option { fn from(algo: QopAlgo<'a>) -> Self { match algo { QopAlgo::NONE => None, QopAlgo::AUTH => Some(Qop::AUTH), QopAlgo::AUTH_INT(_) => Some(Qop::AUTH_INT), } } } /// Charset field value as specified by the server #[derive(Debug, PartialEq, Clone)] pub enum Charset { ASCII, UTF8, } impl FromStr for Charset { type Err = Error; /// Parse from string (only UTF-8 supported, as prescribed by the specification) fn from_str(s: &str) -> Result { match s { "UTF-8" => Ok(Charset::UTF8), _ => Err(BadCharset(s.into())), } } } 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, PartialEq, Clone)] pub struct HttpMethod<'a>(pub Cow<'a, str>); // Well-known methods are provided as convenient associated constants impl<'a> HttpMethod<'a> { pub const GET : Self = HttpMethod(Cow::Borrowed("GET")); pub const POST : Self = HttpMethod(Cow::Borrowed("POST")); pub const PUT : Self = HttpMethod(Cow::Borrowed("PUT")); pub const DELETE : Self = HttpMethod(Cow::Borrowed("DELETE")); pub const HEAD : Self = HttpMethod(Cow::Borrowed("HEAD")); pub const OPTIONS : Self = HttpMethod(Cow::Borrowed("OPTIONS")); pub const CONNECT : Self = HttpMethod(Cow::Borrowed("CONNECT")); pub const PATCH : Self = HttpMethod(Cow::Borrowed("PATCH")); pub const TRACE : Self = HttpMethod(Cow::Borrowed("TRACE")); } impl<'a> Default for HttpMethod<'a> { fn default() -> Self { HttpMethod::GET } } impl<'a> Display for HttpMethod<'a> { /// Convert to uppercase string fn fmt(&self, f: &mut Formatter) -> fmt::Result { f.write_str(&self.0) } } impl<'a> From<&'a str> for HttpMethod<'a> { fn from(s: &'a str) -> Self { Self(s.into()) } } impl<'a> From<&'a [u8]> for HttpMethod<'a> { fn from(s: &'a [u8]) -> Self { Self(String::from_utf8_lossy(s).into()) } } impl<'a> From for HttpMethod<'a> { fn from(s: String) -> Self { Self(s.into()) } } impl<'a> From> for HttpMethod<'a> { fn from(s: Cow<'a, str>) -> Self { Self(s) } } #[cfg(feature = "http")] impl From for HttpMethod<'static> { fn from(method: http::Method) -> Self { match method.as_str() { // Avoid cloning when possible "GET" => Self::GET, "POST" => Self::POST, "PUT" => Self::PUT, "DELETE" => Self::DELETE, "HEAD" => Self::HEAD, "OPTIONS" => Self::OPTIONS, "CONNECT" => Self::CONNECT, "PATCH" => Self::PATCH, "TRACE" => Self::TRACE, // Clone custom strings. This is inefficient, but the inner string is private other => Self(other.to_owned().into()) } } } #[cfg(feature = "http")] impl<'a> From<&'a http::Method> for HttpMethod<'a> { fn from(method: &'a http::Method) -> HttpMethod<'a> { Self(method.as_str().into()) } } #[cfg(test)] mod test { use crate::error::Error::{BadCharset, BadQop, UnknownAlgorithm}; use crate::{Algorithm, AlgorithmType, Charset, HttpMethod, Qop, QopAlgo}; use std::borrow::Cow; use std::str::FromStr; #[test] fn test_algorithm_type() { // String parsing assert_eq!( Ok(Algorithm::new(AlgorithmType::MD5, false)), Algorithm::from_str("MD5") ); assert_eq!( Ok(Algorithm::new(AlgorithmType::MD5, true)), Algorithm::from_str("MD5-sess") ); assert_eq!( Ok(Algorithm::new(AlgorithmType::SHA2_256, false)), Algorithm::from_str("SHA-256") ); assert_eq!( Ok(Algorithm::new(AlgorithmType::SHA2_256, true)), Algorithm::from_str("SHA-256-sess") ); assert_eq!( Ok(Algorithm::new(AlgorithmType::SHA2_512_256, false)), Algorithm::from_str("SHA-512-256") ); assert_eq!( Ok(Algorithm::new(AlgorithmType::SHA2_512_256, true)), Algorithm::from_str("SHA-512-256-sess") ); assert_eq!( Err(UnknownAlgorithm("OTHER_ALGORITHM".to_string())), Algorithm::from_str("OTHER_ALGORITHM") ); // String building assert_eq!( "MD5".to_string(), Algorithm::new(AlgorithmType::MD5, false).to_string() ); assert_eq!( "MD5-sess".to_string(), Algorithm::new(AlgorithmType::MD5, true).to_string() ); assert_eq!( "SHA-256".to_string(), Algorithm::new(AlgorithmType::SHA2_256, false).to_string() ); assert_eq!( "SHA-256-sess".to_string(), Algorithm::new(AlgorithmType::SHA2_256, true).to_string() ); assert_eq!( "SHA-512-256".to_string(), Algorithm::new(AlgorithmType::SHA2_512_256, false).to_string() ); assert_eq!( "SHA-512-256-sess".to_string(), Algorithm::new(AlgorithmType::SHA2_512_256, true).to_string() ); // Default assert_eq!( Algorithm::new(AlgorithmType::MD5, false), Default::default() ); // Hash calculation assert_eq!( "e2fc714c4727ee9395f324cd2e7f331f".to_string(), Algorithm::new(AlgorithmType::MD5, false).hash("abcd".as_bytes()) ); assert_eq!( "e2fc714c4727ee9395f324cd2e7f331f".to_string(), Algorithm::new(AlgorithmType::MD5, false).hash_str("abcd") ); assert_eq!( "88d4266fd4e6338d13b845fcf289579d209c897823b9217da3e161936f031589".to_string(), Algorithm::new(AlgorithmType::SHA2_256, false).hash("abcd".as_bytes()) ); assert_eq!( "d2891c7978be0e24948f37caa415b87cb5cbe2b26b7bad9dc6391b8a6f6ddcc9".to_string(), Algorithm::new(AlgorithmType::SHA2_512_256, false).hash("abcd".as_bytes()) ); } #[test] fn test_qop() { assert_eq!(Ok(Qop::AUTH), Qop::from_str("auth")); assert_eq!(Ok(Qop::AUTH_INT), Qop::from_str("auth-int")); assert_eq!(Err(BadQop("banana".to_string())), Qop::from_str("banana")); assert_eq!("auth".to_string(), Qop::AUTH.to_string()); assert_eq!("auth-int".to_string(), Qop::AUTH_INT.to_string()); } #[test] fn test_qop_algo() { assert_eq!(Option::::None, QopAlgo::NONE.into()); assert_eq!(Some(Qop::AUTH), QopAlgo::AUTH.into()); assert_eq!( Some(Qop::AUTH_INT), QopAlgo::AUTH_INT("foo".as_bytes()).into() ); } #[test] fn test_charset() { assert_eq!(Ok(Charset::UTF8), Charset::from_str("UTF-8")); assert_eq!(Err(BadCharset("ASCII".into())), Charset::from_str("ASCII")); assert_eq!("UTF-8".to_string(), Charset::UTF8.to_string()); assert_eq!("ASCII".to_string(), Charset::ASCII.to_string()); } #[test] fn test_http_method() { // Well known 'static assert_eq!(HttpMethod::GET, "GET".into()); assert_eq!(HttpMethod::POST, "POST".into()); assert_eq!(HttpMethod::PUT, "PUT".into()); assert_eq!(HttpMethod::DELETE, "DELETE".into()); assert_eq!(HttpMethod::HEAD, "HEAD".into()); assert_eq!(HttpMethod::OPTIONS, "OPTIONS".into()); assert_eq!(HttpMethod::CONNECT, "CONNECT".into()); assert_eq!(HttpMethod::PATCH, "PATCH".into()); assert_eq!(HttpMethod::TRACE, "TRACE".into()); // As bytes assert_eq!(HttpMethod::GET, "GET".as_bytes().into()); assert_eq!( HttpMethod(Cow::Borrowed("ěščř")), "ěščř".as_bytes().into() ); assert_eq!( HttpMethod(Cow::Owned("AB�".to_string())), // Lossy conversion (&[65u8, 66, 156][..]).into() ); // Well known String assert_eq!(HttpMethod::GET, String::from("GET").into()); // Custom String assert_eq!( HttpMethod(Cow::Borrowed("NonsenseMethod")), "NonsenseMethod".into() ); assert_eq!( HttpMethod(Cow::Owned("NonsenseMethod".to_string())), "NonsenseMethod".to_string().into() ); // Custom Cow assert_eq!(HttpMethod::HEAD, Cow::Borrowed("HEAD").into()); assert_eq!( HttpMethod(Cow::Borrowed("NonsenseMethod")), Cow::Borrowed("NonsenseMethod").into() ); // to string assert_eq!("GET".to_string(), HttpMethod::GET.to_string()); assert_eq!("POST".to_string(), HttpMethod::POST.to_string()); assert_eq!("PUT".to_string(), HttpMethod::PUT.to_string()); assert_eq!("DELETE".to_string(), HttpMethod::DELETE.to_string()); assert_eq!("HEAD".to_string(), HttpMethod::HEAD.to_string()); assert_eq!("OPTIONS".to_string(), HttpMethod::OPTIONS.to_string()); assert_eq!("CONNECT".to_string(), HttpMethod::CONNECT.to_string()); assert_eq!("PATCH".to_string(), HttpMethod::PATCH.to_string()); assert_eq!("TRACE".to_string(), HttpMethod::TRACE.to_string()); assert_eq!( "NonsenseMethod".to_string(), HttpMethod(Cow::Borrowed("NonsenseMethod")).to_string() ); assert_eq!( "NonsenseMethod".to_string(), HttpMethod(Cow::Owned("NonsenseMethod".to_string())).to_string() ); } #[cfg(feature = "http")] #[test] fn test_http_crate() { assert_eq!(HttpMethod::GET, http::Method::GET.clone().into()); assert_eq!( HttpMethod(Cow::Owned("BANANA".to_string())), http::Method::from_str("BANANA").unwrap().into() ); assert_eq!(HttpMethod::GET, (&http::Method::GET).into()); let x = http::Method::from_str("BANANA").unwrap(); assert_eq!( HttpMethod(Cow::Borrowed("BANANA")), (&x).into() ); } } digest_auth-0.3.1/src/error.rs000064400000000000000000000022461046102023000144170ustar 00000000000000use std::fmt::{self, Display, Formatter}; use std::result; #[derive(Debug, PartialEq)] pub enum Error { BadCharset(String), UnknownAlgorithm(String), BadQop(String), MissingRequired(&'static str, String), InvalidHeaderSyntax(String), BadQopOptions(String), NumParseError, } pub type Result = result::Result; use Error::*; impl Display for Error { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { BadCharset(ctx) => write!(f, "Bad charset: {}", ctx), UnknownAlgorithm(ctx) => write!(f, "Unknown algorithm: {}", ctx), BadQop(ctx) => write!(f, "Bad Qop option: {}", ctx), MissingRequired(what, ctx) => write!(f, "Missing \"{}\" in header: {}", what, ctx), InvalidHeaderSyntax(ctx) => write!(f, "Invalid header syntax: {}", ctx), BadQopOptions(ctx) => write!(f, "Illegal Qop in prompt: {}", ctx), NumParseError => write!(f, "Error parsing a number."), } } } impl From for Error { fn from(_: std::num::ParseIntError) -> Self { NumParseError } } impl std::error::Error for Error {} digest_auth-0.3.1/src/lib.rs000064400000000000000000000116751046102023000140420ustar 00000000000000//! This crate implements Digest Auth headers as specified by IETF RFCs 2069, 2617, and 7616. //! It can be used in conjunction with libraries like reqwest to access e.g. IP cameras //! that use this authentication scheme. //! //! This library was written for the http client, but since the algorithm is symmetrical, //! it can be used by the server side as well. Server-side nonce management (generation, timed //! expiry) and authorization checking is left to user's implementation. //! //! The `AuthorizationHeader::digest()` method can be used server-side to replicate the //! password/body hash; then just check if the computed digest matches what the user sent. //! //! # Examples //! //! Basic usage: //! //! ``` //! use digest_auth::AuthContext; //! //! // Value from the WWW-Authenticate HTTP header (usually in a HTTP 401 response) //! let www_authenticate = r#"Digest realm="http-auth@example.org", qop="auth, auth-int", algorithm=MD5, nonce="7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v", opaque="FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS""#; //! //! // Prepare an authorization context. Note that this is a GET request. There are different //! // constructors available for POST or other request types. You can re-use it, but //! // it's cheap to create a fresh one each time, as the struct uses references only. //! let mut context = AuthContext::new("Mufasa", "Circle of Life", "/dir/index.html"); //! // For this test, we inject a custom cnonce. It's generated for you otherwise //! // - you don't need `mut` in that case and needn't worry about this at all. //! context.set_custom_cnonce("f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ"); //! //! // Parse the prompt header. You can inspect the parsed object, its fields are public. //! let mut prompt = digest_auth::parse(www_authenticate).unwrap(); //! //! // Compute a value for the Authorization header that we'll send back to the server //! let answer = prompt.respond(&context).unwrap().to_string(); //! assert_eq!(answer, 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"#); //! //! // The `prompt` variable is mutable, because the 'nc' counter (nonce reuse count) //! // is inside the struct and updated automatically. //! //! // You can re-use it for subsequent requests, assuming the server allows nonce re-use. //! // Some poorly implemented servers will reject it and give you 401 again, in which case //! // you should parse the new "WWW-Authenticate" header and use that instead. //! //! let answer2 = prompt.respond(&context).unwrap().to_string(); //! // notice how the 'response' field changed - the 'nc' counter is included in the hash //! assert_eq!(answer2, r#"Digest username="Mufasa", realm="http-auth@example.org", nonce="7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v", uri="/dir/index.html", qop=auth, nc=00000002, cnonce="f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ", response="4b5d595ecf2db9df612ea5b45cd97101", opaque="FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS", algorithm=MD5"#); //! ``` mod digest; mod enums; mod error; pub use error::{Error, Result}; pub use crate::digest::{AuthContext, AuthorizationHeader, WwwAuthenticateHeader}; pub use crate::enums::*; /// Parse the WWW-Authorization header value. /// It's just a convenience method to call [`WwwAuthenticateHeader::parse()`](struct.WwwAuthenticateHeader.html#method.parse). pub fn parse(www_authorize: &str) -> Result { WwwAuthenticateHeader::parse(www_authorize) } #[cfg(test)] mod test { use crate::{AuthContext, Error}; #[test] fn test_parse_respond() { 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 = crate::parse(src).unwrap(); let answer = prompt.respond(&context).unwrap(); let str = answer.to_string().replace(", ", ",\n "); // This is only for easier reading assert_eq!( str, 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() ); } #[test] fn test_cast_error() { let _m: Box = Error::UnknownAlgorithm("Uhhh".into()).into(); } }