summaryrefslogtreecommitdiffstats
path: root/vendor/http-auth
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
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')
-rw-r--r--vendor/http-auth/.cargo-checksum.json1
-rw-r--r--vendor/http-auth/CHANGELOG.md31
-rw-r--r--vendor/http-auth/Cargo.lock1092
-rw-r--r--vendor/http-auth/Cargo.toml75
-rw-r--r--vendor/http-auth/LICENSE-APACHE.txt202
-rw-r--r--vendor/http-auth/LICENSE-MIT.txt20
-rw-r--r--vendor/http-auth/README.md99
-rw-r--r--vendor/http-auth/build.rs136
-rw-r--r--vendor/http-auth/examples/reqwest.rs67
-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
13 files changed, 4086 insertions, 0 deletions
diff --git a/vendor/http-auth/.cargo-checksum.json b/vendor/http-auth/.cargo-checksum.json
new file mode 100644
index 000000000..59ce8579a
--- /dev/null
+++ b/vendor/http-auth/.cargo-checksum.json
@@ -0,0 +1 @@
+{"files":{"CHANGELOG.md":"30d41f9637e61db485d133d00ec51436c8193c470327fba0bbc1bc78c47f68b2","Cargo.lock":"9bb234a5ffdb53064979fa28964d90fcfeef6aa6c9dffb1f3014e7f59ac1808e","Cargo.toml":"376152840ce224bcf24137dd4a28ab844bf666de3ce85244644bb71209ce3145","LICENSE-APACHE.txt":"531ae0e8652952207e2e577daa311955290f78380be845087f2acd86d808df7d","LICENSE-MIT.txt":"ed057943f5d1306ad0c87d38e1e523b9e167dce174ef7138fa27200c7cf1243f","README.md":"dd83686e7205081302f20f3bdd73a6555956a0b8ec1622795dda3f010158f8fd","build.rs":"700d886f3f3b6361c5e56542a7c395d12bf93bf19223996bf7de3e40fe8ad98b","examples/reqwest.rs":"e14686de096ba44a6402e7daa62d35b5a62d6448b1068fddbde327311b274fb5","src/basic.rs":"5b8f424d6e4d983d0e0d769c44cd73d10de3b5fa413b423aaf0b699edb0adac3","src/digest.rs":"378700648fe09ad736d3d84e4a6b8e7785fe01593ad332029ab037676d80ce88","src/lib.rs":"0b55273b9b8b428f06195d58d63efd062b246aa8bfafb10c4de44ba0dc7e6d22","src/parser.rs":"9556546afaece815485fe9884510c95e19884f1e7f1e6f6b47750522804dbf5b"},"package":"c0b40b39d66c28829a0cf4d09f7e139ff8201f7500a5083732848ed3b4b4d850"} \ No newline at end of file
diff --git a/vendor/http-auth/CHANGELOG.md b/vendor/http-auth/CHANGELOG.md
new file mode 100644
index 000000000..8aaf39ed8
--- /dev/null
+++ b/vendor/http-auth/CHANGELOG.md
@@ -0,0 +1,31 @@
+## `v0.1.6` (2021-05-02)
+
+* upgrade `digest`, `md5`, and `sha2` dependencies.
+
+## `v0.1.5` (2021-11-30)
+
+* add `http_auth::basic::encode_credentials` for preemptively sending `Basic`
+ credentials.
+
+## `v0.1.4` (2021-11-18)
+
+* more thorough documentation
+* shrink `DigestClient`
+* support `userhash` in `DigestClient`
+* support `-sess` algorithm variants in `DigestClient`
+
+## `v0.1.3` (2021-10-20)
+
+* fix `docs.rs`
+
+## `v0.1.2` (2021-10-20)
+
+* add RFC 2069 compatibility mode.
+
+## `v0.1.1` (2021-10-20)
+
+* allow `Digest`'s `qop` parameter to be omitted.
+
+## `v0.1.0` (2021-10-20)
+
+* initial version
diff --git a/vendor/http-auth/Cargo.lock b/vendor/http-auth/Cargo.lock
new file mode 100644
index 000000000..3f6855440
--- /dev/null
+++ b/vendor/http-auth/Cargo.lock
@@ -0,0 +1,1092 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "ansi_term"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
+
+[[package]]
+name = "base64"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c"
+
+[[package]]
+name = "bytes"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
+
+[[package]]
+name = "cc"
+version = "1.0.72"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "core-foundation"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "ctor"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccc0a48a9b826acdf4028595adc9db92caea352f7af011a3034acd172a52a0aa"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "diff"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499"
+
+[[package]]
+name = "digest"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
+dependencies = [
+ "matches",
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fc8cd39e3dbf865f7340dce6a2d401d24fd37c6fe6c4f0ee0de8bfca2252d27"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445"
+
+[[package]]
+name = "futures-io"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e481354db6b5c353246ccf6a728b0c5511d752c08da7260546fc0933869daa11"
+
+[[package]]
+name = "futures-sink"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "996c6442437b62d21a32cd9906f9c41e7dc1e19a9579843fad948696769305af"
+
+[[package]]
+name = "futures-task"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12"
+
+[[package]]
+name = "futures-util"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e"
+dependencies = [
+ "futures-core",
+ "futures-io",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "h2"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fd819562fcebdac5afc5c113c3ec36f902840b70fd4fc458799c8ce4607ae55"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "http"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-auth"
+version = "0.1.6"
+dependencies = [
+ "base64",
+ "digest",
+ "hex",
+ "http",
+ "log",
+ "md-5",
+ "memchr",
+ "pretty_assertions",
+ "rand",
+ "reqwest",
+ "sha2",
+]
+
+[[package]]
+name = "http-body"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6"
+dependencies = [
+ "bytes",
+ "http",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503"
+
+[[package]]
+name = "httpdate"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
+
+[[package]]
+name = "hyper"
+version = "0.14.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "436ec0091e4f20e655156a30a0df3770fe2900aa301e548e08446ec794b6953c"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
+dependencies = [
+ "bytes",
+ "hyper",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+]
+
+[[package]]
+name = "idna"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
+dependencies = [
+ "matches",
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9"
+
+[[package]]
+name = "itoa"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
+
+[[package]]
+name = "js-sys"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.108"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8521a1b57e76b1ec69af7599e75e38e7b7fad6610f037db8c79b127201b5d119"
+
+[[package]]
+name = "log"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "matches"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
+
+[[package]]
+name = "md-5"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "658646b21e0b72f7866c7038ab086d3d5e1cd6271f060fd37defb241949d0582"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "memchr"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
+
+[[package]]
+name = "mime"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
+
+[[package]]
+name = "mio"
+version = "0.7.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc"
+dependencies = [
+ "libc",
+ "log",
+ "miow",
+ "ntapi",
+ "winapi",
+]
+
+[[package]]
+name = "miow"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "native-tls"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "ntapi"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
+
+[[package]]
+name = "openssl"
+version = "0.10.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.71"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7df13d165e607909b363a4757a6f133f8a818a74e9d3a98d09c6128e15fa4c73"
+dependencies = [
+ "autocfg",
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "output_vt100"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12295df4f294471248581bc09bef3c38a5e46f1e36d6a37353621a0c6c357e1f"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba"
+
+[[package]]
+name = "pretty_assertions"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0cfe1b2403f172ba0f234e500906ee0a3e493fb81092dac23ebefe129301cc"
+dependencies = [
+ "ansi_term",
+ "ctor",
+ "diff",
+ "output_vt100",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43"
+dependencies = [
+ "unicode-xid",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+ "rand_hc",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.11.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bea77bc708afa10e59905c3d4af7c8fd43c9214251673095ff8b14345fcbc5"
+dependencies = [
+ "base64",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "hyper-tls",
+ "ipnet",
+ "js-sys",
+ "lazy_static",
+ "log",
+ "mime",
+ "native-tls",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "tokio",
+ "tokio-native-tls",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "winreg",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c9613b5a66ab9ba26415184cfc41156594925a9cf3a2057e57f31ff145f6568"
+
+[[package]]
+name = "schannel"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
+dependencies = [
+ "lazy_static",
+ "winapi",
+]
+
+[[package]]
+name = "security-framework"
+version = "2.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "525bc1abfda2e1998d152c45cf13e696f76d0a4972310b22fac1658b05df7c87"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.130"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
+
+[[package]]
+name = "serde_json"
+version = "1.0.72"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0ffa0837f2dfa6fb90868c2b5468cad482e175f7dad97e7421951e663f2b527"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
+
+[[package]]
+name = "socket2"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "rand",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
+
+[[package]]
+name = "tokio"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144"
+dependencies = [
+ "autocfg",
+ "bytes",
+ "libc",
+ "memchr",
+ "mio",
+ "num_cpus",
+ "pin-project-lite",
+ "winapi",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "log",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tower-service"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6"
+
+[[package]]
+name = "tracing"
+version = "0.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105"
+dependencies = [
+ "cfg-if",
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
+
+[[package]]
+name = "typenum"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
+
+[[package]]
+name = "url"
+version = "2.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "matches",
+ "percent-encoding",
+]
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "version_check"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
+
+[[package]]
+name = "want"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
+dependencies = [
+ "log",
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.10.2+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b"
+dependencies = [
+ "bumpalo",
+ "lazy_static",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc"
+
+[[package]]
+name = "web-sys"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "winreg"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69"
+dependencies = [
+ "winapi",
+]
diff --git a/vendor/http-auth/Cargo.toml b/vendor/http-auth/Cargo.toml
new file mode 100644
index 000000000..8e7f6e17e
--- /dev/null
+++ b/vendor/http-auth/Cargo.toml
@@ -0,0 +1,75 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies.
+#
+# If you are reading this file be aware that the original Cargo.toml
+# will likely look very different (and much more reasonable).
+# See Cargo.toml.orig for the original contents.
+
+[package]
+edition = "2018"
+name = "http-auth"
+version = "0.1.6"
+description = "HTTP authentication: parse challenge lists, respond to Basic and Digest challenges. Likely to be extended with server support and additional auth schemes."
+readme = "README.md"
+keywords = ["http", "authentication", "digest", "basic"]
+categories = ["authentication", "parser-implementations", "web-programming::http-client"]
+license = "MIT/Apache-2.0"
+repository = "https://github.com/scottlamb/http-auth"
+resolver = "2"
+[package.metadata.docs.rs]
+all-features = true
+rustdoc-args = ["--cfg", "docsrs"]
+
+[[example]]
+name = "reqwest"
+required-features = ["http"]
+[dependencies.base64]
+version = "0.13.0"
+optional = true
+
+[dependencies.digest]
+version = "0.10"
+optional = true
+
+[dependencies.hex]
+version = "0.4"
+optional = true
+
+[dependencies.http]
+version = "0.2.5"
+optional = true
+
+[dependencies.log]
+version = "0.4"
+optional = true
+
+[dependencies.md-5]
+version = "0.10"
+optional = true
+
+[dependencies.memchr]
+version = "2.4.1"
+
+[dependencies.rand]
+version = "0.8.4"
+optional = true
+
+[dependencies.sha2]
+version = "0.10"
+optional = true
+[dev-dependencies.pretty_assertions]
+version = "1.0.0"
+
+[dev-dependencies.reqwest]
+version = "0.11.6"
+features = ["blocking"]
+
+[features]
+basic-scheme = ["base64"]
+default = ["basic-scheme", "digest-scheme"]
+digest-scheme = ["digest", "hex", "md-5", "rand", "sha2"]
+trace = ["log"]
diff --git a/vendor/http-auth/LICENSE-APACHE.txt b/vendor/http-auth/LICENSE-APACHE.txt
new file mode 100644
index 000000000..1c8b3b7b8
--- /dev/null
+++ b/vendor/http-auth/LICENSE-APACHE.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2021 Scott Lamb <slamb@slamb.org>
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/vendor/http-auth/LICENSE-MIT.txt b/vendor/http-auth/LICENSE-MIT.txt
new file mode 100644
index 000000000..878837b67
--- /dev/null
+++ b/vendor/http-auth/LICENSE-MIT.txt
@@ -0,0 +1,20 @@
+The MIT License (MIT)
+Copyright (c) 2021 Scott Lamb <slamb@slamb.org>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/vendor/http-auth/README.md b/vendor/http-auth/README.md
new file mode 100644
index 000000000..b44c5ed15
--- /dev/null
+++ b/vendor/http-auth/README.md
@@ -0,0 +1,99 @@
+[![crates.io](https://img.shields.io/crates/v/http-auth)](https://crates.io/crates/http-auth)
+[![Released API docs](https://docs.rs/http-auth/badge.svg)](https://docs.rs/http-auth/)
+[![CI](https://github.com/scottlamb/http-auth/workflows/CI/badge.svg)](https://github.com/scottlamb/http-auth/actions?query=workflow%3ACI)
+
+Rust library for HTTP authentication. Parses challenge lists, responds
+to `Basic` and `Digest` challenges. Likely to be extended with server
+support and additional auth schemes.
+
+HTTP authentication is 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.
+
+## Status
+
+Young but well-tested. The API may change to improve ergonomics and
+functionality. New functionality is likely to be added. PRs welcome!
+
+## Goals
+
+In order:
+
+1. **sound.** Currently no `unsafe` blocks in `http-auth` itself. All
+ dependencies are common, trusted crates.
+3. **correct.** Precisely implements the specifications except where noted.
+ Fuzz tests verify the hand-written parser never panics and matches a
+ nom-based reference implementation.
+4. **light-weight.** Minimal dependencies; uses Cargo features so callers can
+ avoid them when undesired. Simple code that minimizes monomorphization
+ bloat. Small data structures; eg `http_auth::DigestClient` currently weighs
+ in at 32 bytes plus one allocation for all string fields.
+6. **complete.** Implements both parsing and responding to challenges.
+ (Currently only supports the client side and responding to the most common
+ `Basic` and `Digest` schemes; future expansion is likely.)
+7. **ergonomic.** Creating a client for responding to a password challenge is
+ a one-liner from a string header or a
+ [`http::header::GetAll`](https://docs.rs/http/0.2.5/http/header/struct.GetAll.html).
+8. **fast enough.** HTTP authentication is a small part of a real program, and
+ `http-auth`'s CPU usage should never be noticeable. For `Digest`'s
+ cryptographic operations, it uses popular optimized crates. In other
+ respects, `http-auth` is likely at least as efficient as other HTTP
+ authentication crates, although I have no reason to believe their
+ performance is problematic.
+
+## Why a new crate?
+
+There are at least a couple other available crates relating to HTTP
+authentication. You may prefer them. Here's why `http-auth`'s author decided
+not to use them.
+
+### [`www-authenticate`](https://crates.io/crates/www-authenticate)
+
+* sound: `www-authenticate` has some unsound `transmute`s to static lifetime.
+ (These likely aren't hard to fix though.)
+* light-weight: `www-authenticate` depends on `hyperx` and `unicase`, large
+ dependencies which many useful programs don't include.
+* complete: `www-authenticate` only supports parsing of challenge lists, not
+ responding to them.
+
+### [`digest_auth`](https://crates.io/crates/digest_auth)
+
+* complete: `digest_auth` only supports `Digest`. It can't parse multiple
+ challenges and will fail if given a list that starts with another scheme.
+ Thus, if the server follows the advice of
+ [RFC 7235 section 2.1](https://datatracker.ietf.org/doc/html/rfc7235) and
+ lists another scheme such as `Basic` first, `digest_auth`'s parsing is
+ insufficient.
+
+### `www-authenticate` + `digest_auth` together
+
+In addition to the `www-authenticate` caveats above, responding to password
+challenges by using both `www-authenticate` and `digest_auth` is not complete
+and ergonomic. The caller must do extra work:
+
+* explicitly consider both `Digest` and `Basic`, rather than using the
+ abstract `http_auth::PasswordClient` that chooses the challenge for you.
+* when responding to a `Digest` challenge, construct a matching
+ `digest_auth::WwwAuthenticateHeader` from the
+ `www_authenticate::DigestChallenge`.
+* when responding to a `Basic` challenge, do the encoding manually.
+
+## Author
+
+Scott Lamb &lt;slamb@slamb.org>
+
+## License
+
+SPDX-License-Identifier: [MIT](https://spdx.org/licenses/MIT.html) OR [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html)
+
+See [LICENSE-MIT.txt](LICENSE-MIT.txt) or [LICENSE-APACHE](LICENSE-APACHE.txt),
+respectively.
diff --git a/vendor/http-auth/build.rs b/vendor/http-auth/build.rs
new file mode 100644
index 000000000..3cedde171
--- /dev/null
+++ b/vendor/http-auth/build.rs
@@ -0,0 +1,136 @@
+// Copyright (C) 2021 Scott Lamb <slamb@slamb.org>
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+//! Builds `char_class_table.bin`, a table of byte values to the character
+//! classes the respective bytes are part of. Most classes are referenced from
+//! [RFC 7235 Appendix B: Imported ABNF](https://datatracker.ietf.org/doc/html/rfc7235#appendix-B)
+//! or [RFC 7235 Appendix C: Collected ABNF](https://datatracker.ietf.org/doc/html/rfc7235#appendix-C).
+
+// Must match lib.rs declaration exactly.
+const C_TCHAR: u8 = 1;
+const C_QDTEXT: u8 = 2;
+const C_ESCAPABLE: u8 = 4;
+const C_OWS: u8 = 8;
+const C_ATTR: u8 = 16;
+
+/// Returns if the byte is a `tchar` as defined in
+/// [RFC 7230 section 3.2.6](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6).
+fn is_tchar(b: u8) -> bool {
+ // tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
+ // / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
+ // / DIGIT / ALPHA
+ // ; any VCHAR, except delimiters
+ matches!(b,
+ b'!'
+ | b'#'
+ | b'$'
+ | b'%'
+ | b'&'
+ | b'\''
+ | b'*'
+ | b'+'
+ | b'-'
+ | b'.'
+ | b'^'
+ | b'_'
+ | b'`'
+ | b'|'
+ | b'~'
+ | b'0'..=b'9'
+ | b'a'..=b'z'
+ | b'A'..=b'Z')
+}
+
+/// Returns true if the byte is a valid `qdtext` (excluding `obs-text`), as defined in
+/// [RFC 7230 section 3.2.6](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6).
+///
+/// ```text
+/// quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
+/// qdtext = HTAB / SP /%x21 / %x23-5B / %x5D-7E / obs-text
+/// obs-text = %x80-FF
+/// quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
+/// VCHAR = %x21-7E
+/// ; visible (printing) characters
+/// ```
+fn is_qdtext(b: u8) -> bool {
+ matches!(b, b'\t' | b' ' | 0x21 | 0x23..=0x5B | 0x5D..=0x7E)
+}
+
+/// Returns true if the byte is a valid end of a `quoted-pair`, as defined in
+/// [RFC 7230 section 3.2.6](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6).
+fn is_escapable(b: u8) -> bool {
+ matches!(b, b'\t' | b' ' | 0x21..=0x7E | 0x80..=0xFF)
+}
+
+/// Returns true if the byte is a valid `attr-char` as defined in
+/// [RFC 5987 section 3.2.1](https://datatracker.ietf.org/doc/html/rfc5987#section-3.2.1).
+///
+/// ```text
+/// attr-char = ALPHA / DIGIT
+/// / "!" / "#" / "$" / "&" / "+" / "-" / "."
+/// / "^" / "_" / "`" / "|" / "~"
+/// ; token except ( "*" / "'" / "%" )
+/// ```
+fn is_attr(b: u8) -> bool {
+ matches!(b,
+ b'a'..=b'z'
+ | b'A'..=b'Z'
+ | b'0'..=b'9'
+ | b'!'
+ | b'#'
+ | b'$'
+ | b'&'
+ | b'+'
+ | b'-'
+ | b'.'
+ | b'^'
+ | b'_'
+ | b'`'
+ | b'|'
+ | b'~')
+}
+
+/// Returns true if the byte is valid optional whitespace as in [RFC 7230 section
+/// 3.2.3](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.3).
+///
+/// ```text
+/// OWS = *( SP / HTAB )
+/// ; optional whitespace
+/// ```
+fn is_ows(b: u8) -> bool {
+ matches!(b, b' ' | b'\t')
+}
+
+fn main() {
+ // This build script depends only on itself.
+ // https://doc.rust-lang.org/cargo/reference/build-scripts.html#cargorerun-if-changedpath
+ println!("cargo:rerun-if-changed=build.rs");
+
+ let mut table = [0u8; 128];
+ for (i, e) in table.iter_mut().enumerate() {
+ let b = i as u8;
+ let mut classes = 0;
+ if is_tchar(b) {
+ classes |= C_TCHAR;
+ }
+ if is_qdtext(b) {
+ classes |= C_QDTEXT;
+ }
+ if is_escapable(b) {
+ classes |= C_ESCAPABLE;
+ }
+ if is_ows(b) {
+ classes |= C_OWS;
+ }
+ if is_attr(b) {
+ classes |= C_ATTR;
+ }
+ *e = classes;
+ }
+
+ let mut out_path = std::path::PathBuf::new();
+ let out_dir = std::env::var("OUT_DIR").unwrap();
+ out_path.push(out_dir);
+ out_path.push("char_class_table.bin");
+ std::fs::write(&out_path, table).unwrap();
+}
diff --git a/vendor/http-auth/examples/reqwest.rs b/vendor/http-auth/examples/reqwest.rs
new file mode 100644
index 000000000..f9173a509
--- /dev/null
+++ b/vendor/http-auth/examples/reqwest.rs
@@ -0,0 +1,67 @@
+// Copyright (C) 2021 Scott Lamb <slamb@slamb.org>
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+//! Verbose example of making authenticated requests with the `reqwest` crate.
+
+use std::convert::TryFrom;
+
+use reqwest::header::HeaderValue;
+
+fn main() {
+ let args: Vec<String> = std::env::args().collect();
+ let (url, username, password) = match &args[..] {
+ [_program, url, username, password] => (url, username, password),
+ [program, ..] => {
+ eprintln!("expected {} URL USERNAME PASSWORD", program);
+ std::process::exit(1);
+ }
+ [] => panic!("no commandline arguments, not even argv[0]"),
+ };
+ let url = reqwest::Url::try_from(url.as_str()).unwrap();
+
+ // Create a client which doesn't follow redirects. The URI used below won't
+ // be correct with reqwest's automatic redirect handling.
+ let client = reqwest::blocking::Client::builder()
+ .redirect(reqwest::redirect::Policy::none())
+ .build()
+ .unwrap();
+ let first_resp = client.get(url.clone()).send().unwrap();
+ if first_resp.status() != reqwest::StatusCode::UNAUTHORIZED {
+ eprintln!(
+ "Server returned status {} without authentication!",
+ first_resp.status()
+ );
+ std::process::exit(1);
+ }
+
+ let mut pw_client = http_auth::PasswordClient::try_from(
+ first_resp
+ .headers()
+ .get_all(reqwest::header::WWW_AUTHENTICATE),
+ )
+ .unwrap();
+ println!("Password challenge client: {:#?}", &pw_client);
+ let authorization = pw_client
+ .respond(&http_auth::PasswordParams {
+ username,
+ password,
+
+ // Note that URI is typically a path.
+ uri: url.path(),
+ method: reqwest::Method::GET.as_str(),
+ body: Some(&[]),
+ })
+ .unwrap();
+ println!("Authorization: {}", &authorization);
+ let mut authorization = HeaderValue::try_from(authorization).unwrap();
+ authorization.set_sensitive(true);
+ let second_resp = client
+ .get(url)
+ .header(reqwest::header::AUTHORIZATION, authorization)
+ .send()
+ .unwrap();
+ println!(
+ "After authorization, server returned status {}",
+ second_resp.status()
+ );
+}
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();
+ }
+}