diff options
Diffstat (limited to 'vendor/http-auth/src')
-rw-r--r-- | vendor/http-auth/src/basic.rs | 114 | ||||
-rw-r--r-- | vendor/http-auth/src/digest.rs | 909 | ||||
-rw-r--r-- | vendor/http-auth/src/lib.rs | 747 | ||||
-rw-r--r-- | vendor/http-auth/src/parser.rs | 593 |
4 files changed, 2363 insertions, 0 deletions
diff --git a/vendor/http-auth/src/basic.rs b/vendor/http-auth/src/basic.rs new file mode 100644 index 000000000..d8e94ab65 --- /dev/null +++ b/vendor/http-auth/src/basic.rs @@ -0,0 +1,114 @@ +// Copyright (C) 2021 Scott Lamb <slamb@slamb.org> +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! `Basic` authentication scheme as in +//! [RFC 7617](https://datatracker.ietf.org/doc/html/rfc7617). + +use std::convert::TryFrom; + +use crate::ChallengeRef; + +/// Encodes the given credentials. +/// +/// This can be used to preemptively send `Basic` authentication, without +/// sending an unauthenticated request and waiting for a `401 Unauthorized` +/// response. +/// +/// The caller should use the returned string as an `Authorization` or +/// `Proxy-Authorization` header value. +/// +/// The caller is responsible for `username` and `password` being in the +/// correct format. Servers may expect arguments to be in Unicode +/// Normalization Form C as noted in [RFC 7617 section +/// 2.1](https://datatracker.ietf.org/doc/html/rfc7617#section-2.1). +/// +/// ```rust +/// assert_eq!( +/// http_auth::basic::encode_credentials("Aladdin", "open sesame"), +/// "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==", +/// ); +pub fn encode_credentials(username: &str, password: &str) -> String { + let user_pass = format!("{}:{}", username, password); + const PREFIX: &str = "Basic "; + let mut value = String::with_capacity(PREFIX.len() + base64_encoded_len(user_pass.len())); + value.push_str(PREFIX); + base64::encode_config_buf(&user_pass[..], base64::STANDARD, &mut value); + value +} + +/// Returns the base64-encoded length for the given input length, including padding. +fn base64_encoded_len(input_len: usize) -> usize { + (input_len + 2) / 3 * 4 +} + +/// Client for a `Basic` challenge, as in +/// [RFC 7617](https://datatracker.ietf.org/doc/html/rfc7617). +/// +/// This implementation always uses `UTF-8`. Thus it doesn't use or store the +/// `charset` parameter, which the RFC only allows to be set to `UTF-8` anyway. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct BasicClient { + realm: Box<str>, +} + +impl BasicClient { + pub fn realm(&self) -> &str { + &*self.realm + } + + /// Responds to the challenge with the supplied parameters. + /// + /// This is functionally identical to [`encode_credentials`]; no parameters + /// of the `BasicClient` are needed to produce the credentials. + #[inline] + pub fn respond(&self, username: &str, password: &str) -> String { + encode_credentials(username, password) + } +} + +impl TryFrom<&ChallengeRef<'_>> for BasicClient { + type Error = String; + + fn try_from(value: &ChallengeRef<'_>) -> Result<Self, Self::Error> { + if !value.scheme.eq_ignore_ascii_case("Basic") { + return Err(format!( + "BasicClient doesn't support challenge scheme {:?}", + value.scheme + )); + } + let mut realm = None; + for (k, v) in &value.params { + if k.eq_ignore_ascii_case("realm") { + realm = Some(v.to_unescaped()); + } + } + let realm = realm.ok_or("missing required parameter realm")?; + Ok(BasicClient { + realm: realm.into_boxed_str(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn basic() { + // Example from https://datatracker.ietf.org/doc/html/rfc7617#section-2 + let ctx = BasicClient { + realm: "WallyWorld".into(), + }; + assert_eq!( + ctx.respond("Aladdin", "open sesame"), + "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" + ); + + // Example from https://datatracker.ietf.org/doc/html/rfc7617#section-2.1 + // Note that this crate *always* uses UTF-8, not just when the server requests it. + let ctx = BasicClient { + realm: "foo".into(), + }; + assert_eq!(ctx.respond("test", "123\u{A3}"), "Basic dGVzdDoxMjPCow=="); + } +} diff --git a/vendor/http-auth/src/digest.rs b/vendor/http-auth/src/digest.rs new file mode 100644 index 000000000..b32278060 --- /dev/null +++ b/vendor/http-auth/src/digest.rs @@ -0,0 +1,909 @@ +// Copyright (C) 2021 Scott Lamb <slamb@slamb.org> +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! `Digest` authentication scheme, as in +//! [RFC 7616](https://datatracker.ietf.org/doc/html/rfc7616). + +use std::{convert::TryFrom, fmt::Write as _, io::Write as _}; + +use digest::Digest; + +use crate::{ + char_classes, ChallengeRef, ParamValue, PasswordParams, C_ATTR, C_ESCAPABLE, C_QDTEXT, +}; + +/// "Quality of protection" value. +/// +/// The values here can be used in a bitmask as in [`DigestClient::qop`]. +#[derive(Copy, Clone, Debug)] +#[repr(u8)] +#[non_exhaustive] +pub enum Qop { + /// Authentication. + Auth = 1, + + /// Authentication with integrity protection. + /// + /// "Integrity protection" means protection of the request entity body. + AuthInt = 2, +} + +impl Qop { + /// Returns a string form as expected over the wire. + fn as_str(self) -> &'static str { + match self { + Qop::Auth => "auth", + Qop::AuthInt => "auth-int", + } + } +} + +/// A set of zero or more [`Qop`]s. +#[derive(Copy, Clone, PartialEq, Eq)] +pub struct QopSet(u8); + +impl std::fmt::Debug for QopSet { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut l = f.debug_set(); + if (self.0 & Qop::Auth as u8) != 0 { + l.entry(&"auth"); + } + if (self.0 & Qop::AuthInt as u8) != 0 { + l.entry(&"auth-int"); + } + l.finish() + } +} + +impl std::ops::BitAnd<Qop> for QopSet { + type Output = bool; + + fn bitand(self, rhs: Qop) -> Self::Output { + (self.0 & (rhs as u8)) != 0 + } +} + +/// Client for a `Digest` challenge, as in [RFC 7616](https://datatracker.ietf.org/doc/html/rfc7616). +/// +/// Most of the information here is taken from the `WWW-Authenticate` or +/// `Proxy-Authenticate` header. This also internally maintains a nonce counter. +/// +/// ## Implementation notes +/// +/// * Recalculates `H(A1)` on each [`DigestClient::respond`] call. It'd be +/// more CPU-efficient to calculate `H(A1)` only once by supplying the +/// username and password at construction time or by caching (username, +/// password) -> `H(A1)` mappings internally. `DigestClient` prioritizes +/// simplicity instead. +/// * There's no support yet for parsing the `Authentication-Info` and +/// `Proxy-Authentication-Info` header fields described by [RFC 7616 section +/// 3.5](https://datatracker.ietf.org/doc/html/rfc7616#section-3.5). +/// PRs welcome! +/// * Always responds using `UTF-8`, and thus doesn't use or keep around the `charset` +/// parameter. The RFC only allows that parameter to be set to `UTF-8` anyway. +/// * Supports [RFC 2069](https://datatracker.ietf.org/doc/html/rfc2069) compatibility as in +/// [RFC 2617 section 3.2.2.1](https://datatracker.ietf.org/doc/html/rfc2617#section-3.2.2.1), +/// even though RFC 7616 drops it. There are still RTSP cameras being sold +/// in 2021 that use the RFC 2069-style calculations. +/// * Supports RFC 7616 `userhash`, even though it seems impractical and only +/// marginally useful. The server must index the userhash for each supported +/// algorithm or calculate it on-the-fly for all users in the database. +/// * The `-sess` algorithm variants haven't been tested; there's no example +/// in the RFCs. +/// +/// ## Security considerations +/// +/// We strongly advise *servers* against implementing `Digest`: +/// +/// * It's actively harmful in that it prevents the server from securing their +/// password storage via salted password hashes. See [RFC 7616 Section +/// 5.2](https://datatracker.ietf.org/doc/html/rfc7616#section-5.2). +/// When your server offers `Digest` authentication, it is advertising that +/// it stores plaintext passwords! +/// * It's no replacement for TLS in terms of protecting confidentiality of +/// the password, much less confidentiality of any other information. +/// +/// For *clients*, when a server supports both `Digest` and `Basic`, we advise +/// using `Digest`. It provides (slightly) more confidentiality of passwords +/// over the wire. +/// +/// Some servers *only* support `Digest`. E.g., +/// [ONVIF](https://www.onvif.org/profiles/specifications/) mandates the +/// `Digest` scheme. It doesn't prohibit implementing other schemes, but some +/// cameras meet the specification's requirement and do no more. +#[derive(Eq, PartialEq)] +pub struct DigestClient { + /// Holds unescaped versions of all string fields. + /// + /// Using a single `String` minimizes the size of the `DigestClient` + /// itself and/or any option/enum it may be wrapped in. It also minimizes + /// padding bytes after each allocation. The fields as stored as follows: + /// + /// 1. `realm`: `[0, domain_start)` + /// 2. `domain`: `[domain_start, opaque_start)` + /// 3. `opaque`: `[opaque_start, nonce_start)` + /// 4. `nonce`: `[nonce_start, buf.len())` + buf: Box<str>, + + // Positions described in `buf` comment above. See respective methods' doc + // comments for more information. These are stored as `u16` to save space, + // and because it's unreasonable for them to be large. + domain_start: u16, + opaque_start: u16, + nonce_start: u16, + + // Non-string fields. See respective methods' doc comments for more information. + algorithm: Algorithm, + session: bool, + stale: bool, + rfc2069_compat: bool, + userhash: bool, + qop: QopSet, + nc: u32, +} + +impl DigestClient { + /// Returns a string to be displayed to users so they know which username + /// and password to use. + /// + /// This string should contain at least the name of + /// the host performing the authentication and might additionally + /// indicate the collection of users who might have access. An + /// example is `registered_users@example.com`. (See [Section 2.2 of + /// RFC 7235](https://datatracker.ietf.org/doc/html/rfc7235#section-2.2) for + /// more details.) + #[inline] + pub fn realm(&self) -> &str { + &self.buf[..self.domain_start as usize] + } + + /// Returns the domain, a space-separated list of URIs, as specified in RFC + /// 3986, that define the protection space. + /// + /// If the domain parameter is absent, returns an empty string, which is semantically + /// identical according to the RFC. + #[inline] + pub fn domain(&self) -> &str { + &self.buf[self.domain_start as usize..self.opaque_start as usize] + } + + /// Returns the nonce, a server-specified string which should be uniquely + /// generated each time a 401 response is made. + #[inline] + pub fn nonce(&self) -> &str { + &self.buf[self.nonce_start as usize..] + } + + /// Returns string of data, specified by the server, that SHOULD be returned + /// by the client unchanged in the Authorization header field of subsequent + /// requests with URIs in the same protection space. + /// + /// Currently an empty `opaque` is treated as an absent one. + #[inline] + pub fn opaque(&self) -> Option<&str> { + if self.opaque_start == self.nonce_start { + None + } else { + Some(&self.buf[self.opaque_start as usize..self.nonce_start as usize]) + } + } + + /// Returns a flag indicating that the previous request from the client was + /// rejected because the nonce value was stale. + #[inline] + pub fn stale(&self) -> bool { + self.stale + } + + /// Returns true if using [RFC 2069](https://datatracker.ietf.org/doc/html/rfc2069) + /// compatibility mode as in [RFC 2617 section + /// 3.2.2.1](https://datatracker.ietf.org/doc/html/rfc2617#section-3.2.2.1). + /// + /// If so, `request-digest` is calculated without the nonce count, conce, or qop. + #[inline] + pub fn rfc2069_compat(&self) -> bool { + self.rfc2069_compat + } + + /// Returns the algorithm used to produce the digest and an unkeyed digest. + #[inline] + pub fn algorithm(&self) -> Algorithm { + self.algorithm + } + + /// Returns if the session style `A1` will be used. + #[inline] + pub fn session(&self) -> bool { + self.session + } + + /// Returns the acceptable `qop` (quality of protection) values. + #[inline] + pub fn qop(&self) -> QopSet { + self.qop + } + + /// Returns the number of times the server-supplied nonce has been used by + /// [`DigestClient::respond`]. + #[inline] + pub fn nonce_count(&self) -> u32 { + self.nc + } + + /// Responds to the challenge with the supplied parameters. + /// + /// The caller should use the returned string as an `Authorization` or + /// `Proxy-Authorization` header value. + #[inline] + pub fn respond(&mut self, p: &PasswordParams) -> Result<String, String> { + self.respond_inner(p, &new_random_cnonce()) + } + + /// Responds using a fixed cnonce **for testing only**. + /// + /// In production code, use [`DigestClient::respond`] instead, which generates a new + /// random cnonce value. + #[inline] + pub fn respond_with_testing_cnonce( + &mut self, + p: &PasswordParams, + cnonce: &str, + ) -> Result<String, String> { + self.respond_inner(p, cnonce) + } + + /// Helper for respond methods. + /// + /// We don't simply implement this as `respond_with_testing_cnonce` and have + /// `respond` delegate to that method because it'd be confusing/alarming if + /// that method name ever shows up in production stack traces. + /// and have `respond` delegate to the testing version. We don't do that because + fn respond_inner(&mut self, p: &PasswordParams, cnonce: &str) -> Result<String, String> { + let realm = self.realm(); + let mut h_a1 = self.algorithm.h(&[ + p.username.as_bytes(), + b":", + realm.as_bytes(), + b":", + p.password.as_bytes(), + ]); + if self.session { + h_a1 = self.algorithm.h(&[ + h_a1.as_bytes(), + b":", + self.nonce().as_bytes(), + b":", + cnonce.as_bytes(), + ]); + } + + // Select the best available qop and calculate H(A2) as in + // [https://datatracker.ietf.org/doc/html/rfc7616#section-3.4.3]. + let (h_a2, qop); + if let (Some(body), true) = (p.body, self.qop & Qop::AuthInt) { + h_a2 = self + .algorithm + .h(&[p.method.as_bytes(), b":", p.uri.as_bytes(), b":", body]); + qop = Qop::AuthInt; + } else if self.qop & Qop::Auth { + h_a2 = self + .algorithm + .h(&[p.method.as_bytes(), b":", p.uri.as_bytes()]); + qop = Qop::Auth; + } else { + return Err("no supported/available qop".into()); + } + + let nc = self.nc.checked_add(1).ok_or("nonce count exhausted")?; + let mut hex_nc = [0u8; 8]; + let _ = write!(&mut hex_nc[..], "{:08x}", nc); + let str_hex_nc = match std::str::from_utf8(&hex_nc[..]) { + Ok(h) => h, + Err(_) => unreachable!(), + }; + + // https://datatracker.ietf.org/doc/html/rfc2617#section-3.2.2.1 + let response = if self.rfc2069_compat { + self.algorithm.h(&[ + h_a1.as_bytes(), + b":", + self.nonce().as_bytes(), + b":", + h_a2.as_bytes(), + ]) + } else { + self.algorithm.h(&[ + h_a1.as_bytes(), + b":", + self.nonce().as_bytes(), + b":", + &hex_nc[..], + b":", + cnonce.as_bytes(), + b":", + qop.as_str().as_bytes(), + b":", + h_a2.as_bytes(), + ]) + }; + + let mut out = String::with_capacity(128); + out.push_str("Digest "); + if self.userhash { + let hashed = self + .algorithm + .h(&[p.username.as_bytes(), b":", realm.as_bytes()]); + append_quoted_key_value(&mut out, "username", &hashed)?; + append_unquoted_key_value(&mut out, "userhash", "true"); + } else if is_valid_quoted_value(p.username) { + append_quoted_key_value(&mut out, "username", p.username)?; + } else { + append_extended_key_value(&mut out, "username", p.username); + } + append_quoted_key_value(&mut out, "realm", self.realm())?; + append_quoted_key_value(&mut out, "uri", p.uri)?; + append_quoted_key_value(&mut out, "nonce", self.nonce())?; + if !self.rfc2069_compat { + append_unquoted_key_value(&mut out, "algorithm", self.algorithm.as_str(self.session)); + append_unquoted_key_value(&mut out, "nc", str_hex_nc); + append_quoted_key_value(&mut out, "cnonce", cnonce)?; + append_unquoted_key_value(&mut out, "qop", qop.as_str()); + } + append_quoted_key_value(&mut out, "response", &response)?; + if let Some(o) = self.opaque() { + append_quoted_key_value(&mut out, "opaque", o)?; + } + out.truncate(out.len() - 2); // remove final ", " + self.nc = nc; + Ok(out) + } +} + +impl TryFrom<&ChallengeRef<'_>> for DigestClient { + type Error = String; + + fn try_from(value: &ChallengeRef<'_>) -> Result<Self, Self::Error> { + if !value.scheme.eq_ignore_ascii_case("Digest") { + return Err(format!( + "DigestClientContext doesn't support challenge scheme {:?}", + value.scheme + )); + } + let mut buf_len = 0; + let mut unused_len = 0; + let mut realm = None; + let mut domain = None; + let mut nonce = None; + let mut opaque = None; + let mut stale = false; + let mut algorithm_and_session = None; + let mut qop_str = None; + let mut userhash_str = None; + + // Parse response header field parameters as in + // [https://datatracker.ietf.org/doc/html/rfc7616#section-3.3]. + for (k, v) in &value.params { + // Note that "stale" and "algorithm" can be directly compared + // without unescaping because RFC 7616 section 3.3 says "For + // historical reasons, a sender MUST NOT generate the quoted string + // syntax values for the following parameters: stale and algorithm." + if store_param(k, v, "realm", &mut realm, &mut buf_len)? + || store_param(k, v, "domain", &mut domain, &mut buf_len)? + || store_param(k, v, "nonce", &mut nonce, &mut buf_len)? + || store_param(k, v, "opaque", &mut opaque, &mut buf_len)? + || store_param(k, v, "qop", &mut qop_str, &mut unused_len)? + || store_param(k, v, "userhash", &mut userhash_str, &mut unused_len)? + { + // Do nothing here. + } else if k.eq_ignore_ascii_case("stale") { + stale = v.escaped.eq_ignore_ascii_case("true"); + } else if k.eq_ignore_ascii_case("algorithm") { + algorithm_and_session = Some(Algorithm::parse(v.escaped)?); + } + } + let realm = realm.ok_or("missing required parameter realm")?; + let nonce = nonce.ok_or("missing required parameter nonce")?; + if buf_len > u16::MAX as usize { + // Incredibly unlikely, but just for completeness. + return Err(format!( + "Unescaped parameters' length {} exceeds u16::MAX!", + buf_len + )); + } + + let algorithm_and_session = algorithm_and_session.unwrap_or((Algorithm::Md5, false)); + + let mut buf = String::with_capacity(buf_len); + let mut qop = QopSet(0); + let rfc2069_compat = if let Some(qop_str) = qop_str { + let qop_str = qop_str.unescaped_with_scratch(&mut buf); + for v in qop_str.split(',') { + let v = v.trim(); + if v.eq_ignore_ascii_case("auth") { + qop.0 |= Qop::Auth as u8; + } else if v.eq_ignore_ascii_case("auth-int") { + qop.0 |= Qop::AuthInt as u8; + } + } + if qop.0 == 0 { + return Err(format!("no supported qop in {:?}", qop_str)); + } + buf.clear(); + false + } else { + // An absent qop is treated as "auth", according to + // https://datatracker.ietf.org/doc/html/rfc7616#section-3.4.3 + qop.0 |= Qop::Auth as u8; + true + }; + let userhash; + if let Some(userhash_str) = userhash_str { + let userhash_str = userhash_str.unescaped_with_scratch(&mut buf); + userhash = userhash_str.eq_ignore_ascii_case("true"); + buf.clear(); + } else { + userhash = false; + }; + realm.append_unescaped(&mut buf); + let domain_start = buf.len(); + if let Some(d) = domain { + d.append_unescaped(&mut buf); + } + let opaque_start = buf.len(); + if let Some(o) = opaque { + o.append_unescaped(&mut buf); + } + let nonce_start = buf.len(); + nonce.append_unescaped(&mut buf); + Ok(DigestClient { + buf: buf.into_boxed_str(), + domain_start: domain_start as u16, + opaque_start: opaque_start as u16, + nonce_start: nonce_start as u16, + algorithm: algorithm_and_session.0, + session: algorithm_and_session.1, + stale, + rfc2069_compat, + userhash, + qop, + nc: 0, + }) + } +} + +impl std::fmt::Debug for DigestClient { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DigestClient") + .field("realm", &self.realm()) + .field("domain", &self.domain()) + .field("opaque", &self.opaque()) + .field("nonce", &self.nonce()) + .field("algorithm", &self.algorithm.as_str(self.session)) + .field("stale", &self.stale) + .field("qop", &self.qop) + .field("rfc2069_compat", &self.rfc2069_compat) + .field("userhash", &self.userhash) + .field("nc", &self.nc) + .finish() + } +} + +/// Helper for `DigestClient::try_from` which stashes away a `&ParamValue`. +fn store_param<'v, 'tmp>( + k: &'tmp str, + v: &'v ParamValue<'v>, + expected_k: &'tmp str, + set_v: &'tmp mut Option<&'v ParamValue<'v>>, + add_len: &'tmp mut usize, +) -> Result<bool, String> { + if !k.eq_ignore_ascii_case(expected_k) { + return Ok(false); + } + if set_v.is_some() { + return Err(format!("duplicate parameter {:?}", k)); + } + *add_len += v.unescaped_len(); + *set_v = Some(v); + Ok(true) +} + +fn is_valid_quoted_value(s: &str) -> bool { + for &b in s.as_bytes() { + if char_classes(b) & (C_QDTEXT | C_ESCAPABLE) == 0 { + return false; + } + } + true +} + +fn append_extended_key_value(out: &mut String, key: &str, value: &str) { + out.push_str(key); + out.push_str("*=UTF-8''"); + for &b in value.as_bytes() { + if (char_classes(b) & C_ATTR) != 0 { + out.push(char::from(b)); + } else { + let _ = write!(out, "%{:02X}", b); + } + } + out.push_str(", "); +} + +fn append_unquoted_key_value(out: &mut String, key: &str, value: &str) { + out.push_str(key); + out.push('='); + out.push_str(value); + out.push_str(", "); +} + +fn append_quoted_key_value(out: &mut String, key: &str, value: &str) -> Result<(), String> { + out.push_str(key); + out.push_str("=\""); + let mut first_unwritten = 0; + let bytes = value.as_bytes(); + for (i, &b) in bytes.iter().enumerate() { + // Note that bytes >= 128 are in neither C_QDTEXT nor C_ESCAPABLE, so every allowed byte + // is a full character. + let class = char_classes(b); + if (class & C_QDTEXT) != 0 { + // Just advance. + } else if (class & C_ESCAPABLE) != 0 { + out.push_str(&value[first_unwritten..i]); + out.push('\\'); + out.push(char::from(b)); + first_unwritten = i + 1; + } else { + return Err(format!("invalid {} value {:?}", key, value)); + } + } + out.push_str(&value[first_unwritten..]); + out.push_str("\", "); + Ok(()) +} + +/// Supported algorithm from the [HTTP Digest Algorithm Values +/// registry](https://www.iana.org/assignments/http-dig-alg/http-dig-alg.xhtml). +/// +/// This doesn't store whether the session variant (`<Algorithm>-sess`) was +/// requested; see [`DigestClient::session`] for that. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum Algorithm { + Md5, + Sha256, + Sha512Trunc256, +} + +impl Algorithm { + /// Parses a string into a tuple of `Algorithm` and a bool representing + /// whether the `-sess` suffix is present. + fn parse(s: &str) -> Result<(Self, bool), String> { + Ok(match s { + "MD5" => (Algorithm::Md5, false), + "MD5-sess" => (Algorithm::Md5, true), + "SHA-256" => (Algorithm::Sha256, false), + "SHA-256-sess" => (Algorithm::Sha256, true), + "SHA-512-256" => (Algorithm::Sha512Trunc256, false), + "SHA-512-256-sess" => (Algorithm::Sha512Trunc256, true), + _ => return Err(format!("unknown algorithm {:?}", s)), + }) + } + + fn as_str(&self, session: bool) -> &'static str { + match (self, session) { + (Algorithm::Md5, false) => "MD5", + (Algorithm::Md5, true) => "MD5-sess", + (Algorithm::Sha256, false) => "SHA-256", + (Algorithm::Sha256, true) => "SHA-256-sess", + (Algorithm::Sha512Trunc256, false) => "SHA-512-256", + (Algorithm::Sha512Trunc256, true) => "SHA-512-256-sess", + } + } + + fn h(&self, items: &[&[u8]]) -> String { + match self { + Algorithm::Md5 => h(md5::Md5::new(), items), + Algorithm::Sha256 => h(sha2::Sha256::new(), items), + Algorithm::Sha512Trunc256 => h(sha2::Sha512_256::new(), items), + } + } +} + +fn h<D: Digest>(mut d: D, items: &[&[u8]]) -> String { + for i in items { + d.update(i); + } + hex::encode(d.finalize()) +} + +fn new_random_cnonce() -> String { + let raw: [u8; 16] = rand::random(); + hex::encode(&raw[..]) +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + /// Tests the example from [RFC 7616 section 3.9.1: SHA-256 and + /// MD5](https://datatracker.ietf.org/doc/html/rfc7616#section-3.9.1). + #[test] + fn sha256_and_md5() { + let www_authenticate = "\ + Digest \ + realm=\"http-auth@example.org\", \ + qop=\"auth, auth-int\", \ + algorithm=SHA-256, \ + nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\", \ + opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\", \ + Digest \ + realm=\"http-auth@example.org\", \ + qop=\"auth, auth-int\", \ + algorithm=MD5, \ + nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\", \ + opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\""; + let challenges = dbg!(crate::parse_challenges(www_authenticate).unwrap()); + assert_eq!(challenges.len(), 2); + let ctxs: Result<Vec<_>, _> = challenges.iter().map(DigestClient::try_from).collect(); + let mut ctxs = dbg!(ctxs.unwrap()); + assert_eq!(ctxs[1].realm(), "http-auth@example.org"); + assert_eq!(ctxs[1].domain(), ""); + assert_eq!( + ctxs[1].nonce(), + "7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v" + ); + assert_eq!( + ctxs[1].opaque(), + Some("FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS") + ); + assert_eq!(ctxs[1].stale(), false); + assert_eq!(ctxs[1].algorithm(), Algorithm::Md5); + assert_eq!(ctxs[1].qop().0, (Qop::Auth as u8) | (Qop::AuthInt as u8)); + assert_eq!(ctxs[1].nonce_count(), 0); + let params = crate::PasswordParams { + username: "Mufasa", + password: "Circle of Life", + uri: "/dir/index.html", + body: None, + method: "GET", + }; + assert_eq!( + &mut ctxs[0] + .respond_with_testing_cnonce( + ¶ms, + "f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ" + ) + .unwrap(), + "Digest username=\"Mufasa\", \ + realm=\"http-auth@example.org\", \ + uri=\"/dir/index.html\", \ + nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\", \ + algorithm=SHA-256, \ + nc=00000001, \ + cnonce=\"f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ\", \ + qop=auth, \ + response=\"753927fa0e85d155564e2e272a28d1802ca10daf4496794697cf8db5856cb6c1\", \ + opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\"" + ); + assert_eq!(ctxs[0].nc, 1); + assert_eq!( + &mut ctxs[1] + .respond_with_testing_cnonce( + ¶ms, + "f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ" + ) + .unwrap(), + "Digest username=\"Mufasa\", \ + realm=\"http-auth@example.org\", \ + uri=\"/dir/index.html\", \ + nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\", \ + algorithm=MD5, \ + nc=00000001, \ + cnonce=\"f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ\", \ + qop=auth, \ + response=\"8ca523f5e9506fed4657c9700eebdbec\", \ + opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\"" + ); + assert_eq!(ctxs[1].nc, 1); + } + + /// Tests a made-up example with `MD5-sess`. There's no example in the RFC, + /// and these values haven't been tested against any other implementation. + /// But having the test here ensures we don't accidentally change the + /// algorithm. + #[test] + fn md5_sess() { + let www_authenticate = "\ + Digest \ + realm=\"http-auth@example.org\", \ + qop=\"auth, auth-int\", \ + algorithm=MD5-sess, \ + nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\", \ + opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\""; + let challenges = dbg!(crate::parse_challenges(www_authenticate).unwrap()); + assert_eq!(challenges.len(), 1); + let ctxs: Result<Vec<_>, _> = challenges.iter().map(DigestClient::try_from).collect(); + let mut ctxs = dbg!(ctxs.unwrap()); + assert_eq!(ctxs[0].realm(), "http-auth@example.org"); + assert_eq!(ctxs[0].domain(), ""); + assert_eq!( + ctxs[0].nonce(), + "7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v" + ); + assert_eq!( + ctxs[0].opaque(), + Some("FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS") + ); + assert_eq!(ctxs[0].stale(), false); + assert_eq!(ctxs[0].algorithm(), Algorithm::Md5); + assert_eq!(ctxs[0].session(), true); + assert_eq!(ctxs[0].qop().0, (Qop::Auth as u8) | (Qop::AuthInt as u8)); + assert_eq!(ctxs[0].nonce_count(), 0); + let params = crate::PasswordParams { + username: "Mufasa", + password: "Circle of Life", + uri: "/dir/index.html", + body: None, + method: "GET", + }; + assert_eq!( + &mut ctxs[0] + .respond_with_testing_cnonce( + ¶ms, + "f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ" + ) + .unwrap(), + "Digest username=\"Mufasa\", \ + realm=\"http-auth@example.org\", \ + uri=\"/dir/index.html\", \ + nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\", \ + algorithm=MD5-sess, \ + nc=00000001, \ + cnonce=\"f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ\", \ + qop=auth, \ + response=\"e783283f46242139c486a698fec7211d\", \ + opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\"" + ); + assert_eq!(ctxs[0].nc, 1); + } + + /// Tests the example from [RFC 7616 section 3.9.2: SHA-512-256, Charset, and + /// Userhash](https://datatracker.ietf.org/doc/html/rfc7616#section-3.9.2). + #[test] + fn sha512_256_charset() { + let www_authenticate = "\ + Digest \ + realm=\"api@example.org\", \ + qop=\"auth\", \ + algorithm=SHA-512-256, \ + nonce=\"5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK\", \ + opaque=\"HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS\", \ + charset=UTF-8, \ + userhash=true"; + let challenges = dbg!(crate::parse_challenges(www_authenticate).unwrap()); + assert_eq!(challenges.len(), 1); + let ctxs: Result<Vec<_>, _> = challenges.iter().map(DigestClient::try_from).collect(); + let mut ctxs = dbg!(ctxs.unwrap()); + assert_eq!(ctxs.len(), 1); + assert_eq!(ctxs[0].realm(), "api@example.org"); + assert_eq!(ctxs[0].domain(), ""); + assert_eq!( + ctxs[0].nonce(), + "5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK" + ); + assert_eq!( + ctxs[0].opaque(), + Some("HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS") + ); + assert_eq!(ctxs[0].stale, false); + assert_eq!(ctxs[0].userhash, true); + assert_eq!(ctxs[0].algorithm, Algorithm::Sha512Trunc256); + assert_eq!(ctxs[0].qop.0, Qop::Auth as u8); + assert_eq!(ctxs[0].nc, 0); + let params = crate::PasswordParams { + username: "J\u{E4}s\u{F8}n Doe", + password: "Secret, or not?", + uri: "/doe.json", + body: None, + method: "GET", + }; + + // Note the username and response values in the RFC are *wrong*! + // https://www.rfc-editor.org/errata/eid4897 + assert_eq!( + &mut ctxs[0] + .respond_with_testing_cnonce( + ¶ms, + "NTg6RKcb9boFIAS3KrFK9BGeh+iDa/sm6jUMp2wds69v" + ) + .unwrap(), + "\ + Digest \ + username=\"793263caabb707a56211940d90411ea4a575adeccb7e360aeb624ed06ece9b0b\", \ + userhash=true, \ + realm=\"api@example.org\", \ + uri=\"/doe.json\", \ + nonce=\"5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK\", \ + algorithm=SHA-512-256, \ + nc=00000001, \ + cnonce=\"NTg6RKcb9boFIAS3KrFK9BGeh+iDa/sm6jUMp2wds69v\", \ + qop=auth, \ + response=\"3798d4131c277846293534c3edc11bd8a5e4cdcbff78b05db9d95eeb1cec68a5\", \ + opaque=\"HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS\"" + ); + assert_eq!(ctxs[0].nc, 1); + ctxs[0].userhash = false; + ctxs[0].nc = 0; + assert_eq!( + &mut ctxs[0] + .respond_with_testing_cnonce( + ¶ms, + "NTg6RKcb9boFIAS3KrFK9BGeh+iDa/sm6jUMp2wds69v" + ) + .unwrap(), + "\ + Digest \ + username*=UTF-8''J%C3%A4s%C3%B8n%20Doe, \ + realm=\"api@example.org\", \ + uri=\"/doe.json\", \ + nonce=\"5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK\", \ + algorithm=SHA-512-256, \ + nc=00000001, \ + cnonce=\"NTg6RKcb9boFIAS3KrFK9BGeh+iDa/sm6jUMp2wds69v\", \ + qop=auth, \ + response=\"3798d4131c277846293534c3edc11bd8a5e4cdcbff78b05db9d95eeb1cec68a5\", \ + opaque=\"HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS\"" + ); + assert_eq!(ctxs[0].nc, 1); + } + + #[test] + fn rfc2069() { + // https://datatracker.ietf.org/doc/html/rfc2069#section-2.4 + // The response there is wrong! See https://www.rfc-editor.org/errata/eid749 + let www_authenticate = "\ + Digest \ + realm=\"testrealm@host.com\", \ + nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", \ + opaque=\"5ccc069c403ebaf9f0171e9517f40e41\""; + let challenges = dbg!(crate::parse_challenges(www_authenticate).unwrap()); + assert_eq!(challenges.len(), 1); + let ctxs: Result<Vec<_>, _> = challenges.iter().map(DigestClient::try_from).collect(); + let mut ctxs = dbg!(ctxs.unwrap()); + assert_eq!(ctxs.len(), 1); + assert_eq!(ctxs[0].qop.0, Qop::Auth as u8); + assert_eq!(ctxs[0].rfc2069_compat, true); + let params = crate::PasswordParams { + username: "Mufasa", + password: "CircleOfLife", + uri: "/dir/index.html", + body: None, + method: "GET", + }; + assert_eq!( + &mut ctxs[0] + .respond_with_testing_cnonce(¶ms, "unused") + .unwrap(), + "\ + Digest \ + username=\"Mufasa\", \ + realm=\"testrealm@host.com\", \ + uri=\"/dir/index.html\", \ + nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", \ + response=\"1949323746fe6a43ef61f9606e7febea\", \ + opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"", + ); + assert_eq!(ctxs[0].nc, 1); + } + + // See sizes with: cargo test -- --nocapture digest::tests::size + #[test] + fn size() { + // This type should have a niche. + assert_eq!( + dbg!(std::mem::size_of::<DigestClient>()), + dbg!(std::mem::size_of::<Option<DigestClient>>()), + ) + } +} diff --git a/vendor/http-auth/src/lib.rs b/vendor/http-auth/src/lib.rs new file mode 100644 index 000000000..13a657ebb --- /dev/null +++ b/vendor/http-auth/src/lib.rs @@ -0,0 +1,747 @@ +// Copyright (C) 2021 Scott Lamb <slamb@slamb.org> +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! HTTP authentication. Currently meant for clients; to be extended for servers. +//! +//! As described in the following documents and specifications: +//! +//! * [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). +//! * [RFC 7235](https://datatracker.ietf.org/doc/html/rfc7235): +//! Hypertext Transfer Protocol (HTTP/1.1): Authentication. +//! * [RFC 7617](https://datatracker.ietf.org/doc/html/rfc7617): +//! The 'Basic' HTTP Authentication Scheme +//! * [RFC 7616](https://datatracker.ietf.org/doc/html/rfc7616): +//! HTTP Digest Access Authentication +//! +//! This framework is primarily used with HTTP, as suggested by the name. It is +//! also used by some other protocols such as RTSP. +//! +//! ## Cargo Features +//! +//! | feature | default? | description | +//! |-----------------|----------|-------------------------------------------------| +//! | `basic-scheme` | yes | support for the `Basic` auth scheme | +//! | `digest-scheme` | yes | support for the `Digest` auth scheme | +//! | `http` | no | convenient conversion from [`http`] crate types | +//! +//! ## Example +//! +//! In most cases, callers only need to use [`PasswordClient`] and +//! [`PasswordParams`] to handle `Basic` and `Digest` authentication schemes. +//! +#![cfg_attr( + feature = "http", + doc = r##" +```rust +use std::convert::TryFrom as _; +use http_auth::PasswordClient; + +let WWW_AUTHENTICATE_VAL = "UnsupportedSchemeA, Basic realm=\"foo\", UnsupportedSchemeB"; +let mut pw_client = http_auth::PasswordClient::try_from(WWW_AUTHENTICATE_VAL).unwrap(); +assert!(matches!(pw_client, http_auth::PasswordClient::Basic(_))); +let response = pw_client.respond(&http_auth::PasswordParams { + username: "Aladdin", + password: "open sesame", + uri: "/", + method: "GET", + body: Some(&[]), +}).unwrap(); +assert_eq!(response, "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="); +``` +"## +)] +//! +//! The `http` feature allows parsing all `WWW-Authenticate` headers within a +//! [`http::HeaderMap`] in one call. +//! +#![cfg_attr( + feature = "http", + doc = r##" +```rust +# use std::convert::TryFrom as _; +use http::header::{HeaderMap, WWW_AUTHENTICATE}; +# use http_auth::PasswordClient; + +let mut headers = HeaderMap::new(); +headers.append(WWW_AUTHENTICATE, "UnsupportedSchemeA".parse().unwrap()); +headers.append(WWW_AUTHENTICATE, "Basic realm=\"foo\", UnsupportedSchemeB".parse().unwrap()); + +let mut pw_client = PasswordClient::try_from(headers.get_all(WWW_AUTHENTICATE)).unwrap(); +assert!(matches!(pw_client, http_auth::PasswordClient::Basic(_))); +``` +"## +)] +#![cfg_attr(docsrs, feature(doc_cfg))] + +use std::convert::TryFrom; + +pub mod parser; + +#[cfg(feature = "basic-scheme")] +#[cfg_attr(docsrs, doc(cfg(feature = "basic-scheme")))] +pub mod basic; + +#[cfg(feature = "digest-scheme")] +#[cfg_attr(docsrs, doc(cfg(feature = "digest-scheme")))] +pub mod digest; + +pub use parser::ChallengeParser; + +#[cfg(feature = "basic-scheme")] +#[cfg_attr(docsrs, doc(cfg(feature = "basic-scheme")))] +pub use crate::basic::BasicClient; + +#[cfg(feature = "digest-scheme")] +#[cfg_attr(docsrs, doc(cfg(feature = "digest-scheme")))] +pub use crate::digest::DigestClient; + +// Must match build.rs exactly. +const C_TCHAR: u8 = 1; +const C_QDTEXT: u8 = 2; +const C_ESCAPABLE: u8 = 4; +const C_OWS: u8 = 8; + +#[cfg_attr(not(feature = "digest-scheme"), allow(unused))] +const C_ATTR: u8 = 16; + +/// Returns a bitmask of `C_*` values indicating character classes. +fn char_classes(b: u8) -> u8 { + // This table is built by build.rs. + const TABLE: &[u8; 128] = include_bytes!(concat!(env!("OUT_DIR"), "/char_class_table.bin")); + *TABLE.get(usize::from(b)).unwrap_or(&0) +} + +/// Parsed challenge (scheme and body) using references to the original header value. +/// Produced by [`crate::parser::ChallengeParser`]. +/// +/// This is not directly useful for responding to a challenge; it's an +/// intermediary for constructing a client that knows how to respond to a specific +/// challenge scheme. In most cases, callers should construct a [`PasswordClient`] +/// without directly using `ChallengeRef`. +/// +/// Only supports the param form, not the apocryphal `token68` form, as described +/// in [`crate::parser::ChallengeParser`]. +#[derive(Clone, Eq, PartialEq)] +pub struct ChallengeRef<'i> { + /// The scheme name, which should be compared case-insensitively. + pub scheme: &'i str, + + /// Zero or more parameters. + /// + /// These are represented as a `Vec` of key-value pairs rather than a + /// map. Given that the parameters are generally only used once when + /// constructing a challenge client and each challenge only supports a few + /// parameter types, it's more efficient in terms of CPU usage and code size + /// to scan through them directly. + pub params: Vec<ChallengeParamRef<'i>>, +} + +impl<'i> ChallengeRef<'i> { + pub fn new(scheme: &'i str) -> Self { + ChallengeRef { + scheme, + params: Vec::new(), + } + } +} + +impl<'i> std::fmt::Debug for ChallengeRef<'i> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ChallengeRef") + .field("scheme", &self.scheme) + .field("params", &ParamsPrinter(&self.params)) + .finish() + } +} + +type ChallengeParamRef<'i> = (&'i str, ParamValue<'i>); + +struct ParamsPrinter<'i>(&'i [ChallengeParamRef<'i>]); + +impl<'i> std::fmt::Debug for ParamsPrinter<'i> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_map() + .entries(self.0.iter().map(|&(ref k, ref v)| (k, v))) + .finish() + } +} + +/// Builds a [`PasswordClient`] from the supplied challenges; create via +/// [`PasswordClient::builder`]. +/// +/// Often you can just use [`PasswordClient`]'s [`TryFrom`] implementations +/// to convert from a parsed challenge ([`crate::ChallengeRef`]) or +/// unparsed challenges (`str`, [`http::header::HeaderValue`], or +/// [`http::header::GetAll`]). +/// +/// The builder allows more flexibility. For example, if you are using a HTTP +/// library which is not based on a `http` crate, you might need to create +/// a `PasswordClient` from an iterator over multiple `WWW-Authenticate` +/// headers. You can feed each to [`PasswordClientBuilder::challenges`]. +/// +/// Prefers `Digest` over `Basic`, consistent with the [RFC 7235 section +/// 2.1](https://datatracker.ietf.org/doc/html/rfc7235#section-2.1) advice +/// for a user-agent to pick the most secure auth-scheme it understands. +/// +/// When there are multiple `Digest` challenges, currently uses the first, +/// consistent with the [RFC 7616 section +/// 3.7](https://datatracker.ietf.org/doc/html/rfc7616#section-3.7) +/// advice to "use the first challenge it supports, unless a local policy +/// dictates otherwise". In the future, it may prioritize by algorithm. +/// +/// Ignores parse errors as long as there's at least one parseable, supported +/// challenge. +/// +/// ## Example +/// +#[cfg_attr( + feature = "digest", + doc = r##" +```rust +use http_auth::PasswordClient; +let client = PasswordClient::builder() + .challenges("UnsupportedSchemeA, Basic realm=\"foo\", UnsupportedSchemeB") + .challenges("Digest \ + realm=\"http-auth@example.org\", \ + qop=\"auth, auth-int\", \ + algorithm=MD5, \ + nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\", \ + opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\"") + .build() + .unwrap(); +assert!(matches!(client, PasswordClient::Digest(_))); +``` +"## +)] +#[derive(Default)] +pub struct PasswordClientBuilder( + /// The current result: + /// * `Some(Ok(_))` if there is a suitable client. + /// * `Some(Err(_))` if there is no suitable client and has been a parse error. + /// * `None` otherwise. + Option<Result<PasswordClient, String>>, +); + +impl PasswordClientBuilder { + /// Considers all challenges from the given [`http::HeaderValue`] challenge list. + #[cfg(feature = "http")] + #[cfg_attr(docsrs, doc(cfg(feature = "http")))] + pub fn header_value(mut self, value: &http::HeaderValue) -> Self { + if self.complete() { + return self; + } + + match value.to_str() { + Ok(v) => self = self.challenges(v), + Err(_) if matches!(self.0, None) => self.0 = Some(Err("non-ASCII header value".into())), + _ => {} + } + + self + } + + /// Returns true if no more challenges need to be examined. + #[cfg(feature = "digest-scheme")] + fn complete(&self) -> bool { + matches!(self.0, Some(Ok(PasswordClient::Digest(_)))) + } + + /// Returns true if no more challenges need to be examined. + #[cfg(not(feature = "digest-scheme"))] + fn complete(&self) -> bool { + matches!(self.0, Some(Ok(_))) + } + + /// Considers all challenges from the given `&str` challenge list. + pub fn challenges(mut self, value: &str) -> Self { + let mut parser = ChallengeParser::new(value); + while !self.complete() { + match parser.next() { + Some(Ok(c)) => self = self.challenge(&c), + Some(Err(e)) if self.0.is_none() => self.0 = Some(Err(e.to_string())), + _ => break, + } + } + self + } + + /// Considers a single challenge. + pub fn challenge(mut self, challenge: &ChallengeRef<'_>) -> Self { + if self.complete() { + return self; + } + + #[cfg(feature = "digest-scheme")] + if challenge.scheme.eq_ignore_ascii_case("Digest") { + match DigestClient::try_from(challenge) { + Ok(c) => self.0 = Some(Ok(PasswordClient::Digest(c))), + Err(e) if self.0.is_none() => self.0 = Some(Err(e)), + _ => {} + } + return self; + } + + #[cfg(feature = "basic-scheme")] + if challenge.scheme.eq_ignore_ascii_case("Basic") && !matches!(self.0, Some(Ok(_))) { + match BasicClient::try_from(challenge) { + Ok(c) => self.0 = Some(Ok(PasswordClient::Basic(c))), + Err(e) if self.0.is_none() => self.0 = Some(Err(e)), + _ => {} + } + return self; + } + + if self.0.is_none() { + self.0 = Some(Err(format!("Unsupported scheme {:?}", challenge.scheme))); + } + + self + } + + /// Returns a new [`PasswordClient`] or fails. + pub fn build(self) -> Result<PasswordClient, String> { + self.0.unwrap_or_else(|| Err("no challenges given".into())) + } +} + +/// Client for responding to a password challenge. +/// +/// Typically created via [`TryFrom`] implementations for a parsed challenge +/// ([`crate::ChallengeRef`]) or unparsed challenges (`str`, +/// [`http::header::HeaderValue`], or [`http::header::GetAll`]). See full +/// example in the [crate-level documentation](crate). +/// +/// For more complex scenarios, see [`PasswordClientBuilder`]. +#[derive(Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum PasswordClient { + #[cfg(feature = "basic-scheme")] + #[cfg_attr(docsrs, doc(cfg(feature = "basic-scheme")))] + Basic(BasicClient), + + #[cfg(feature = "digest-scheme")] + #[cfg_attr(docsrs, doc(cfg(feature = "digest-scheme")))] + Digest(DigestClient), +} + +/// Tries to create a `PasswordClient` from the single supplied challenge. +/// +/// This is a convenience wrapper around [`PasswordClientBuilder`]. +impl TryFrom<&ChallengeRef<'_>> for PasswordClient { + type Error = String; + + fn try_from(value: &ChallengeRef<'_>) -> Result<Self, Self::Error> { + #[cfg(feature = "basic-scheme")] + if value.scheme.eq_ignore_ascii_case("Basic") { + return Ok(PasswordClient::Basic(BasicClient::try_from(value)?)); + } + #[cfg(feature = "digest-scheme")] + if value.scheme.eq_ignore_ascii_case("Digest") { + return Ok(PasswordClient::Digest(DigestClient::try_from(value)?)); + } + + Err(format!("unsupported challenge scheme {:?}", value.scheme)) + } +} + +/// Tries to create a `PasswordClient` forom the supplied `str` challenge list. +/// +/// This is a convenience wrapper around [`PasswordClientBuilder`]. +impl TryFrom<&str> for PasswordClient { + type Error = String; + + #[inline] + fn try_from(value: &str) -> Result<Self, Self::Error> { + PasswordClient::builder().challenges(value).build() + } +} + +/// Tries to create a `PasswordClient` from the supplied `HeaderValue` challenge list. +/// +/// This is a convenience wrapper around [`PasswordClientBuilder`]. +#[cfg(feature = "http")] +#[cfg_attr(docsrs, doc(cfg(feature = "http")))] +impl TryFrom<&http::HeaderValue> for PasswordClient { + type Error = String; + + #[inline] + fn try_from(value: &http::HeaderValue) -> Result<Self, Self::Error> { + PasswordClient::builder().header_value(value).build() + } +} + +/// Tries to create a `PasswordClient` from the supplied `http::header::GetAll` challenge lists. +/// +/// This is a convenience wrapper around [`PasswordClientBuilder`]. +#[cfg(feature = "http")] +#[cfg_attr(docsrs, doc(cfg(feature = "http")))] +impl TryFrom<http::header::GetAll<'_, http::HeaderValue>> for PasswordClient { + type Error = String; + + fn try_from(value: http::header::GetAll<'_, http::HeaderValue>) -> Result<Self, Self::Error> { + let mut builder = PasswordClient::builder(); + for v in value { + builder = builder.header_value(v); + } + builder.build() + } +} + +impl PasswordClient { + /// Builds a new `PasswordClient`. + /// + /// See example at [`PasswordClientBuilder`]. + pub fn builder() -> PasswordClientBuilder { + PasswordClientBuilder::default() + } + + /// Responds to the challenge with the supplied parameters. + /// + /// The caller should use the returned string as an `Authorization` or + /// `Proxy-Authorization` header value. + #[allow(unused_variables)] // p is unused with no features. + pub fn respond(&mut self, p: &PasswordParams) -> Result<String, String> { + match self { + #[cfg(feature = "basic-scheme")] + Self::Basic(c) => Ok(c.respond(p.username, p.password)), + #[cfg(feature = "digest-scheme")] + Self::Digest(c) => c.respond(p), + + // Rust 1.55 + --no-default-features produces a "non-exhaustive + // patterns" error without this. I think this is a rustc bug given + // that the enum is empty in this case. Work around it. + #[cfg(not(any(feature = "basic-scheme", feature = "digest-scheme")))] + _ => unreachable!(), + } + } +} + +/// Parameters for responding to a password challenge. +/// +/// This is cheap to construct; callers generally use a fresh `PasswordParams` +/// for each request. +/// +/// The caller is responsible for supplying parameters in the correct +/// format. Servers may expect character data to be in Unicode Normalization +/// Form C as noted in [RFC 7617 section +/// 2.1](https://datatracker.ietf.org/doc/html/rfc7617#section-2.1) for the +/// `Basic` scheme and [RFC 7616 section +/// 4](https://datatracker.ietf.org/doc/html/rfc7616#section-4) for the `Digest` +/// scheme. +/// +/// Note that most of these fields are only needed for [`DigestClient`]. Callers +/// that only care about the `Basic` challenge scheme can use +/// [`BasicClient::respond`] directly with only username and password. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct PasswordParams<'a> { + pub username: &'a str, + pub password: &'a str, + + /// The URI from the Request-URI of the Request-Line, as described in + /// [RFC 2617 section 3.2.2](https://datatracker.ietf.org/doc/html/rfc2617#section-3.2.2). + /// + /// [RFC 2617 section + /// 3.2.2.5](https://datatracker.ietf.org/doc/html/rfc2617#section-3.2.2.5), + /// which says the following: + /// > This may be `*`, an `absoluteURL` or an `abs_path` as specified in + /// > section 5.1.2 of [RFC 2616](https://datatracker.ietf.org/doc/html/rfc2616), + /// > but it MUST agree with the Request-URI. In particular, it MUST + /// > be an `absoluteURL` if the Request-URI is an `absoluteURL`. + /// + /// [RFC 7616 section 3.4](https://datatracker.ietf.org/doc/html/rfc7616#section-3.4) + /// describes this as the "Effective Request URI", which is *always* an + /// absolute form. This may be a mistake. [Section + /// 3.4.6](https://datatracker.ietf.org/doc/html/rfc7616#section-3.4.6) + /// matches RFC 2617 section 3.2.2.5, and [Appendix + /// A](https://datatracker.ietf.org/doc/html/rfc7616#appendix-A) doesn't + /// mention a change from RFC 2617. + pub uri: &'a str, + + /// The HTTP method, such as `GET`. + /// + /// When using the `http` crate, use the return value of + /// [`http::Method::as_str`]. + pub method: &'a str, + + /// The entity body, if available. Use `Some(&[])` for HTTP methods with no + /// body. + /// + /// When `None`, `Digest` challenges will only be able to use + /// [`crate::digest::Qop::Auth`], not + /// [`crate::digest::Qop::AuthInt`]. + pub body: Option<&'a [u8]>, +} + +/// Parses a list of challenges into a `Vec`. +/// +/// Most callers don't need to directly parse; see [`PasswordClient`] instead. +/// +/// This is a shorthand for `parser::ChallengeParser::new(input).collect()`. Use +/// [`crate::parser::ChallengeParser`] directly when you want to parse lazily, +/// avoid allocation, and/or see any well-formed challenges before an error. +/// +/// ## Example +/// +/// ```rust +/// use http_auth::{parse_challenges, ChallengeRef, ParamValue}; +/// +/// // When all challenges are well-formed, returns them. +/// assert_eq!( +/// parse_challenges("UnsupportedSchemeA, Basic realm=\"foo\"").unwrap(), +/// vec![ +/// ChallengeRef { +/// scheme: "UnsupportedSchemeA", +/// params: vec![], +/// }, +/// ChallengeRef { +/// scheme: "Basic", +/// params: vec![("realm", ParamValue::try_from_escaped("foo").unwrap())], +/// }, +/// ], +/// ); +/// +/// // Returns `Err` if there is a syntax error anywhere in the input. +/// parse_challenges("UnsupportedSchemeA, Basic realm=\"foo\", error error").unwrap_err(); +/// ``` +#[inline] +pub fn parse_challenges(input: &str) -> Result<Vec<ChallengeRef>, parser::Error> { + parser::ChallengeParser::new(input).collect() +} + +/// Parsed challenge parameter value used within [`ChallengeRef`]. +#[derive(Copy, Clone, Eq, PartialEq)] +pub struct ParamValue<'i> { + /// The number of backslash escapes in a quoted-text parameter; 0 for a plain token. + escapes: usize, + + /// The escaped string, which must be pure ASCII (no bytes >= 128) and be + /// consistent with `escapes`. + escaped: &'i str, +} + +impl<'i> ParamValue<'i> { + /// Tries to create a new `ParamValue` from an escaped sequence, primarily for testing. + /// + /// Validates the sequence and counts the number of escapes. + pub fn try_from_escaped(escaped: &'i str) -> Result<Self, String> { + let mut escapes = 0; + let mut pos = 0; + while pos < escaped.len() { + let slash = memchr::memchr(b'\\', &escaped.as_bytes()[pos..]).map(|off| pos + off); + for i in pos..slash.unwrap_or(escaped.len()) { + if (char_classes(escaped.as_bytes()[i]) & C_QDTEXT) == 0 { + return Err(format!("{:?} has non-qdtext at byte {}", escaped, i)); + } + } + if let Some(slash) = slash { + escapes += 1; + if escaped.len() <= slash + 1 { + return Err(format!("{:?} ends at a quoted-pair escape", escaped)); + } + if (char_classes(escaped.as_bytes()[slash + 1]) & C_ESCAPABLE) == 0 { + return Err(format!( + "{:?} has an invalid quote-pair escape at byte {}", + escaped, + slash + 1 + )); + } + pos = slash + 2; + } else { + break; + } + } + Ok(Self { escaped, escapes }) + } + + /// Creates a new param, panicking if invariants are not satisfied. + /// This not part of the stable API; it's just for the fuzz tester to use. + #[doc(hidden)] + pub fn new(escapes: usize, escaped: &'i str) -> Self { + let mut pos = 0; + for escape in 0..escapes { + match memchr::memchr(b'\\', &escaped.as_bytes()[pos..]) { + Some(rel_pos) => pos += rel_pos + 2, + None => panic!( + "expected {} backslashes in {:?}, ran out after {}", + escapes, escaped, escape + ), + }; + } + if memchr::memchr(b'\\', &escaped.as_bytes()[pos..]).is_some() { + panic!( + "expected {} backslashes in {:?}, are more", + escapes, escaped + ); + } + ParamValue { escapes, escaped } + } + + /// Appends the unescaped form of this parameter to the supplied string. + pub fn append_unescaped(&self, to: &mut String) { + to.reserve(self.escaped.len() - self.escapes); + let mut first_unwritten = 0; + for _ in 0..self.escapes { + let i = match memchr::memchr(b'\\', &self.escaped.as_bytes()[first_unwritten..]) { + Some(rel_i) => first_unwritten + rel_i, + None => panic!("bad ParamValues; not as many backslash escapes as promised"), + }; + to.push_str(&self.escaped[first_unwritten..i]); + to.push_str(&self.escaped[i + 1..i + 2]); + first_unwritten = i + 2; + } + to.push_str(&self.escaped[first_unwritten..]); + } + + /// Returns the unescaped length of this parameter; cheap. + #[inline] + pub fn unescaped_len(&self) -> usize { + self.escaped.len() - self.escapes + } + + /// Returns the unescaped form of this parameter as a fresh `String`. + pub fn to_unescaped(&self) -> String { + let mut to = String::new(); + self.append_unescaped(&mut to); + to + } + + /// Returns the unescaped form of this parameter, possibly appending it to `scratch`. + #[cfg(feature = "digest-scheme")] + fn unescaped_with_scratch<'tmp>(&self, scratch: &'tmp mut String) -> &'tmp str + where + 'i: 'tmp, + { + if self.escapes == 0 { + self.escaped + } else { + let start = scratch.len(); + self.append_unescaped(scratch); + &scratch[start..] + } + } + + /// Returns the escaped string, unquoted. + #[inline] + pub fn as_escaped(&self) -> &'i str { + self.escaped + } +} + +impl<'i> std::fmt::Debug for ParamValue<'i> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "\"{}\"", self.escaped) + } +} + +#[cfg(test)] +mod tests { + use crate::ParamValue; + use crate::{C_ATTR, C_ESCAPABLE, C_OWS, C_QDTEXT, C_TCHAR}; + + /// Prints the character classes of all ASCII bytes from the table. + /// + /// ```console + /// $ cargo test -- --nocapture tests::table + /// ``` + #[test] + fn table() { + // Print the table to allow human inspection. + println!("oct dec hex char tchar qdtext escapable ows attr"); + for b in 0..128 { + let classes = crate::char_classes(b); + let if_class = + |class: u8, label: &'static str| if (classes & class) != 0 { label } else { "" }; + println!( + "{:03o} {:>3} 0x{:02x} {:8} {:5} {:6} {:9} {:3} {:4}", + b, + b, + b, + format!("{:?}", char::from(b)), + if_class(C_TCHAR, "tchar"), + if_class(C_QDTEXT, "qdtext"), + if_class(C_ESCAPABLE, "escapable"), + if_class(C_OWS, "ows"), + if_class(C_ATTR, "attr") + ); + + // Do basic sanity checks: all tchar and ows should be qdtext; all + // qdtext should be escapable. + assert!(classes & (C_TCHAR | C_QDTEXT) != C_TCHAR); + assert!(classes & (C_OWS | C_QDTEXT) != C_OWS); + assert!(classes & (C_QDTEXT | C_ESCAPABLE) != C_QDTEXT); + } + } + + #[test] + fn try_from_escaped() { + assert_eq!(ParamValue::try_from_escaped("").unwrap().escapes, 0); + assert_eq!(ParamValue::try_from_escaped("foo").unwrap().escapes, 0); + assert_eq!(ParamValue::try_from_escaped("\\\"").unwrap().escapes, 1); + assert_eq!( + ParamValue::try_from_escaped("foo\\\"bar").unwrap().escapes, + 1 + ); + assert_eq!( + ParamValue::try_from_escaped("foo\\\"bar\\\"baz") + .unwrap() + .escapes, + 2 + ); + ParamValue::try_from_escaped("\\").unwrap_err(); // ends in slash + ParamValue::try_from_escaped("\"").unwrap_err(); // not valid qdtext + ParamValue::try_from_escaped("\n").unwrap_err(); // not valid qdtext + ParamValue::try_from_escaped("\\\n").unwrap_err(); // not valid escape + } + + #[test] + fn unescape() { + assert_eq!( + &ParamValue { + escapes: 0, + escaped: "" + } + .to_unescaped(), + "" + ); + assert_eq!( + &ParamValue { + escapes: 0, + escaped: "foo" + } + .to_unescaped(), + "foo" + ); + assert_eq!( + &ParamValue { + escapes: 1, + escaped: "\\foo" + } + .to_unescaped(), + "foo" + ); + assert_eq!( + &ParamValue { + escapes: 1, + escaped: "fo\\o" + } + .to_unescaped(), + "foo" + ); + assert_eq!( + &ParamValue { + escapes: 1, + escaped: "foo\\bar" + } + .to_unescaped(), + "foobar" + ); + assert_eq!( + &ParamValue { + escapes: 3, + escaped: "\\foo\\ba\\r" + } + .to_unescaped(), + "foobar" + ); + } +} diff --git a/vendor/http-auth/src/parser.rs b/vendor/http-auth/src/parser.rs new file mode 100644 index 000000000..a6eedf5b0 --- /dev/null +++ b/vendor/http-auth/src/parser.rs @@ -0,0 +1,593 @@ +// Copyright (C) 2021 Scott Lamb <slamb@slamb.org> +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! Parses as in [RFC 7235](https://datatracker.ietf.org/doc/html/rfc7235). +//! +//! Most callers don't need to directly parse; see [`crate::PasswordClient`] instead. + +// State machine implementation of challenge parsing with a state machine. +// Nice qualities: predictable performance (no backtracking), low dependencies. +// +// The implementation is *not* a straightforward translation of the ABNF +// grammar, so we verify correctness via a fuzz tester that compares with a +// nom-based parser. See `fuzz/fuzz_targets/parse_challenges.rs`. + +use std::{fmt::Display, ops::Range}; + +use crate::{ChallengeRef, ParamValue}; + +use crate::{char_classes, C_ESCAPABLE, C_OWS, C_QDTEXT, C_TCHAR}; + +/// Calls `log::trace!` only if the `trace` cargo feature is enabled. +macro_rules! trace { + ($($arg:tt)+) => (#[cfg(feature = "trace")] log::trace!($($arg)+)) +} + +/// Parses a list of challenges as in [RFC +/// 7235](https://datatracker.ietf.org/doc/html/rfc7235) `Proxy-Authenticate` +/// or `WWW-Authenticate` header values. +/// +/// Most callers don't need to directly parse; see [`crate::PasswordClient`] instead. +/// +/// This is an iterator that parses lazily, returning each challenge as soon as +/// its end has been found. (Due to the grammar's ambiguous use of commas to +/// separate both challenges and parameters, a challenge's end is found after +/// parsing the *following* challenge's scheme name.) On encountering a syntax +/// error, it yields `Some(Err(_))` and fuses: all subsequent calls to +/// [`Iterator::next`] will return `None`. +/// +/// See also the [`crate::parse_challenges`] convenience wrapper. +/// +/// ## Example +/// +/// ```rust +/// use http_auth::{parser::ChallengeParser, ChallengeRef, ParamValue}; +/// let challenges = "UnsupportedSchemeA, Basic realm=\"foo\", error error"; +/// let mut parser = ChallengeParser::new(challenges); +/// let c = parser.next().unwrap().unwrap(); +/// assert_eq!(c, ChallengeRef { +/// scheme: "UnsupportedSchemeA", +/// params: vec![], +/// }); +/// let c = parser.next().unwrap().unwrap(); +/// assert_eq!(c, ChallengeRef { +/// scheme: "Basic", +/// params: vec![("realm", ParamValue::try_from_escaped("foo").unwrap())], +/// }); +/// let c = parser.next().unwrap().unwrap_err(); +/// ``` +/// +/// ## Implementation notes +/// +/// This rigorously matches the official ABNF grammar except as follows: +/// +/// * Doesn't allow non-ASCII characters. [RFC 7235 Appendix +/// B](https://datatracker.ietf.org/doc/html/rfc7235#appendix-B) references +/// the `quoted-string` rule from [RFC 7230 section +/// 3.2.6](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6), +/// which allows these via `obs-text`, but the meaning is ill-defined in +/// the context of RFC 7235. +/// * Doesn't allow `token68`, which as far as I know has never been and will +/// never be used in a `challenge`: +/// * [RFC 2617](https://datatracker.ietf.org/doc/html/rfc2617) never +/// allowed `token68` for challenges. +/// * [RFC 7235 Appendix +/// A](https://datatracker.ietf.org/doc/html/rfc7235#appendix-A) says +/// `token68` "was added for consistency with legacy authentication +/// schemes such as `Basic`", but `Basic` only uses `token68` in +/// `credential`, not `challenge`. +/// * [RFC 7235 section +/// 5.1.2](https://datatracker.ietf.org/doc/html/rfc7235#section-5.1.2) +/// says "new schemes ought to use the `auth-param` syntax instead +/// [of `token68`], because otherwise future extensions will be +/// impossible." +/// * No scheme in the [registry](https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml) +/// uses `token68` challenges as of 2021-10-19. +pub struct ChallengeParser<'i> { + input: &'i str, + pos: usize, + state: State<'i>, +} + +impl<'i> ChallengeParser<'i> { + pub fn new(input: &'i str) -> Self { + ChallengeParser { + input, + pos: 0, + state: State::PreToken { + challenge: None, + next: Possibilities(P_SCHEME), + }, + } + } +} + +/// Describes a parse error and where in the input it occurs. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct Error<'i> { + input: &'i str, + pos: usize, + error: &'static str, +} + +impl<'i> Error<'i> { + fn invalid_byte(input: &'i str, pos: usize) -> Self { + Self { + input, + pos, + error: "invalid byte", + } + } +} + +impl<'i> Display for Error<'i> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{} at byte {}: {:?}", + self.error, + self.pos, + format!( + "{}(HERE-->){}", + &self.input[..self.pos], + &self.input[self.pos..] + ), + ) + } +} + +impl<'i> std::error::Error for Error<'i> {} + +/// A set of zero or more `P_*` values indicating possibilities for the current +/// and/or upcoming tokens. +#[derive(Copy, Clone, PartialEq, Eq)] +struct Possibilities(u8); + +const P_SCHEME: u8 = 1; +const P_PARAM_KEY: u8 = 2; +const P_EOF: u8 = 4; +const P_WHITESPACE: u8 = 8; +const P_COMMA_PARAM_KEY: u8 = 16; // a comma, then a param_key. +const P_COMMA_EOF: u8 = 32; // a comma, then eof. + +impl std::fmt::Debug for Possibilities { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut l = f.debug_set(); + if (self.0 & P_SCHEME) != 0 { + l.entry(&"scheme"); + } + if (self.0 & P_PARAM_KEY) != 0 { + l.entry(&"param_key"); + } + if (self.0 & P_EOF) != 0 { + l.entry(&"eof"); + } + if (self.0 & P_WHITESPACE) != 0 { + l.entry(&"whitespace"); + } + if (self.0 & P_COMMA_PARAM_KEY) != 0 { + l.entry(&"comma_param_key"); + } + if (self.0 & P_COMMA_EOF) != 0 { + l.entry(&"comma_eof"); + } + l.finish() + } +} + +enum State<'i> { + Done, + + /// Consuming OWS and commas, then advancing to `Token`. + PreToken { + challenge: Option<ChallengeRef<'i>>, + next: Possibilities, + }, + + /// Parsing a scheme/parameter key, or the whitespace immediately following it. + Token { + /// Current `challenge`, if any. If none, this token must be a scheme. + challenge: Option<ChallengeRef<'i>>, + token_pos: Range<usize>, + cur: Possibilities, // subset of P_SCHEME|P_PARAM_KEY + }, + + /// Transitioned from `Token` or `PostToken` on first `=` after parameter key. + /// Kept there for BWS in param case. + PostEquals { + challenge: ChallengeRef<'i>, + key_pos: Range<usize>, + }, + + /// Transitioned from `Equals` on initial `C_TCHAR`. + ParamUnquotedValue { + challenge: ChallengeRef<'i>, + key_pos: Range<usize>, + value_start: usize, + }, + + /// Transitioned from `Equals` on initial `"`. + ParamQuotedValue { + challenge: ChallengeRef<'i>, + key_pos: Range<usize>, + value_start: usize, + escapes: usize, + in_backslash: bool, + }, +} + +impl<'i> Iterator for ChallengeParser<'i> { + type Item = Result<ChallengeRef<'i>, Error<'i>>; + + fn next(&mut self) -> Option<Self::Item> { + while self.pos < self.input.len() { + let b = self.input.as_bytes()[self.pos]; + let classes = char_classes(b); + match std::mem::replace(&mut self.state, State::Done) { + State::Done => return None, + State::PreToken { challenge, next } => { + trace!( + "PreToken({:?}) pos={} b={:?}", + next, + self.pos, + char::from(b) + ); + if (classes & C_OWS) != 0 && (next.0 & P_WHITESPACE) != 0 { + self.state = State::PreToken { + challenge, + next: Possibilities(next.0 & !P_EOF), + } + } else if b == b',' { + let next = Possibilities( + next.0 + | P_WHITESPACE + | P_SCHEME + | if (next.0 & P_COMMA_PARAM_KEY) != 0 { + P_PARAM_KEY + } else { + 0 + } + | if (next.0 & P_COMMA_EOF) != 0 { + P_EOF + } else { + 0 + }, + ); + self.state = State::PreToken { challenge, next } + } else if (classes & C_TCHAR) != 0 { + self.state = State::Token { + challenge, + token_pos: self.pos..self.pos + 1, + cur: Possibilities(next.0 & (P_SCHEME | P_PARAM_KEY)), + } + } else { + return Some(Err(Error::invalid_byte(self.input, self.pos))); + } + } + State::Token { + challenge, + token_pos, + cur, + } => { + trace!( + "Token({:?}, {:?}) pos={} b={:?}, cur challenge = {:#?}", + token_pos, + cur, + self.pos, + char::from(b), + challenge + ); + if (classes & C_TCHAR) != 0 { + if token_pos.end == self.pos { + self.state = State::Token { + challenge, + token_pos: token_pos.start..self.pos + 1, + cur, + }; + } else { + // Ending a scheme, starting a parameter key without an intermediate comma. + // The whitespace between must be exactly one space. + if (cur.0 & P_SCHEME) == 0 + || &self.input[token_pos.end..self.pos] != " " + { + return Some(Err(Error::invalid_byte(self.input, self.pos))); + } + self.state = State::Token { + challenge: Some(ChallengeRef::new(&self.input[token_pos])), + token_pos: self.pos..self.pos + 1, + cur: Possibilities(P_PARAM_KEY), + }; + if let Some(c) = challenge { + self.pos += 1; + return Some(Ok(c)); + } + } + } else { + match b { + b',' if (cur.0 & P_SCHEME) != 0 => { + self.state = State::PreToken { + challenge: Some(ChallengeRef::new(&self.input[token_pos])), + next: Possibilities( + P_SCHEME | P_WHITESPACE | P_EOF | P_COMMA_EOF, + ), + }; + if let Some(c) = challenge { + self.pos += 1; + return Some(Ok(c)); + } + } + b'=' if (cur.0 & P_PARAM_KEY) != 0 => match challenge { + Some(challenge) => { + self.state = State::PostEquals { + challenge, + key_pos: token_pos, + } + } + None => { + return Some(Err(Error { + input: self.input, + pos: self.pos, + error: "= without existing challenge", + })); + } + }, + + b' ' | b'\t' => { + self.state = State::Token { + challenge, + token_pos, + cur, + } + } + + _ => return Some(Err(Error::invalid_byte(self.input, self.pos))), + } + } + } + State::PostEquals { challenge, key_pos } => { + trace!("PostEquals pos={} b={:?}", self.pos, char::from(b)); + if (classes & C_OWS) != 0 { + // Note this doesn't advance key_pos.end, so in the token68 case, another + // `=` will not be allowed. + self.state = State::PostEquals { challenge, key_pos }; + } else if b == b'"' { + self.state = State::ParamQuotedValue { + challenge, + key_pos, + value_start: self.pos + 1, + escapes: 0, + in_backslash: false, + }; + } else if (classes & C_TCHAR) != 0 { + self.state = State::ParamUnquotedValue { + challenge, + key_pos, + value_start: self.pos, + }; + } else { + return Some(Err(Error::invalid_byte(self.input, self.pos))); + } + } + State::ParamUnquotedValue { + mut challenge, + key_pos, + value_start, + } => { + trace!("ParamUnquotedValue pos={} b={:?}", self.pos, char::from(b)); + if (classes & C_TCHAR) != 0 { + self.state = State::ParamUnquotedValue { + challenge, + key_pos, + value_start, + }; + } else if (classes & C_OWS) != 0 { + challenge.params.push(( + &self.input[key_pos], + ParamValue { + escapes: 0, + escaped: &self.input[value_start..self.pos], + }, + )); + self.state = State::PreToken { + challenge: Some(challenge), + next: Possibilities(P_WHITESPACE | P_COMMA_PARAM_KEY | P_COMMA_EOF), + }; + } else if b == b',' { + challenge.params.push(( + &self.input[key_pos], + ParamValue { + escapes: 0, + escaped: &self.input[value_start..self.pos], + }, + )); + self.state = State::PreToken { + challenge: Some(challenge), + next: Possibilities( + P_WHITESPACE + | P_PARAM_KEY + | P_SCHEME + | P_EOF + | P_COMMA_PARAM_KEY + | P_COMMA_EOF, + ), + }; + } else { + return Some(Err(Error::invalid_byte(self.input, self.pos))); + } + } + State::ParamQuotedValue { + mut challenge, + key_pos, + value_start, + escapes, + in_backslash, + } => { + trace!("ParamQuotedValue pos={} b={:?}", self.pos, char::from(b)); + if in_backslash { + if (classes & C_ESCAPABLE) == 0 { + return Some(Err(Error::invalid_byte(self.input, self.pos))); + } + self.state = State::ParamQuotedValue { + challenge, + key_pos, + value_start, + escapes: escapes + 1, + in_backslash: false, + }; + } else if b == b'\\' { + self.state = State::ParamQuotedValue { + challenge, + key_pos, + value_start, + escapes, + in_backslash: true, + }; + } else if b == b'"' { + challenge.params.push(( + &self.input[key_pos], + ParamValue { + escapes, + escaped: &self.input[value_start..self.pos], + }, + )); + self.state = State::PreToken { + challenge: Some(challenge), + next: Possibilities( + P_WHITESPACE | P_EOF | P_COMMA_PARAM_KEY | P_COMMA_EOF, + ), + }; + } else if (classes & C_QDTEXT) != 0 { + self.state = State::ParamQuotedValue { + challenge, + key_pos, + value_start, + escapes, + in_backslash, + }; + } else { + return Some(Err(Error::invalid_byte(self.input, self.pos))); + } + } + }; + self.pos += 1; + } + match std::mem::replace(&mut self.state, State::Done) { + State::Done => {} + State::PreToken { + challenge, next, .. + } => { + trace!("eof, PreToken({:?})", next); + if (next.0 & P_EOF) == 0 { + return Some(Err(Error { + input: self.input, + pos: self.input.len(), + error: "unexpected EOF", + })); + } + if let Some(challenge) = challenge { + return Some(Ok(challenge)); + } + } + State::Token { + challenge, + token_pos, + cur, + } => { + trace!("eof, Token({:?})", cur); + if (cur.0 & P_SCHEME) == 0 { + return Some(Err(Error { + input: self.input, + pos: self.input.len(), + error: "unexpected EOF expecting =", + })); + } + if token_pos.end != self.input.len() && &self.input[token_pos.end..] != " " { + return Some(Err(Error { + input: self.input, + pos: self.input.len(), + error: "EOF after whitespace", + })); + } + if let Some(challenge) = challenge { + self.state = State::Token { + challenge: None, + token_pos, + cur, + }; + return Some(Ok(challenge)); + } + return Some(Ok(ChallengeRef::new(&self.input[token_pos]))); + } + State::PostEquals { .. } => { + trace!("eof, PostEquals"); + return Some(Err(Error { + input: self.input, + pos: self.input.len(), + error: "unexpected EOF expecting param value", + })); + } + State::ParamUnquotedValue { + mut challenge, + key_pos, + value_start, + } => { + trace!("eof, ParamUnquotedValue"); + challenge.params.push(( + &self.input[key_pos], + ParamValue { + escapes: 0, + escaped: &self.input[value_start..], + }, + )); + return Some(Ok(challenge)); + } + State::ParamQuotedValue { .. } => { + trace!("eof, ParamQuotedValue"); + return Some(Err(Error { + input: self.input, + pos: self.input.len(), + error: "unexpected EOF in quoted param value", + })); + } + } + None + } +} + +#[cfg(test)] +mod tests { + use crate::{ChallengeRef, ParamValue}; + + // A couple basic tests. The fuzz testing is far more comprehensive. + + #[test] + fn multi_challenge() { + // https://datatracker.ietf.org/doc/html/rfc7235#section-4.1 + let input = + r#"Newauth realm="apps", type=1, title="Login to \"apps\"", Basic realm="simple""#; + let challenges = crate::parse_challenges(input).unwrap(); + assert_eq!( + &challenges[..], + &[ + ChallengeRef { + scheme: "Newauth", + params: vec![ + ("realm", ParamValue::new(0, "apps")), + ("type", ParamValue::new(0, "1")), + ("title", ParamValue::new(2, r#"Login to \"apps\""#)), + ], + }, + ChallengeRef { + scheme: "Basic", + params: vec![("realm", ParamValue::new(0, "simple")),], + }, + ] + ); + } + + #[test] + fn empty() { + crate::parse_challenges("").unwrap_err(); + crate::parse_challenges(",").unwrap_err(); + } +} |