summaryrefslogtreecommitdiffstats
path: root/vendor/http-auth/src
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-04 12:41:41 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-04 12:41:41 +0000
commit10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87 (patch)
treebdffd5d80c26cf4a7a518281a204be1ace85b4c1 /vendor/http-auth/src
parentReleasing progress-linux version 1.70.0+dfsg1-9~progress7.99u1. (diff)
downloadrustc-10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87.tar.xz
rustc-10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87.zip
Merging upstream version 1.70.0+dfsg2.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/http-auth/src')
-rw-r--r--vendor/http-auth/src/basic.rs114
-rw-r--r--vendor/http-auth/src/digest.rs909
-rw-r--r--vendor/http-auth/src/lib.rs747
-rw-r--r--vendor/http-auth/src/parser.rs593
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(
+ &params,
+ "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(
+ &params,
+ "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(
+ &params,
+ "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(
+ &params,
+ "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(
+ &params,
+ "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(&params, "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();
+ }
+}