From 10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 4 May 2024 14:41:41 +0200 Subject: Merging upstream version 1.70.0+dfsg2. Signed-off-by: Daniel Baumann --- vendor/http-auth/.cargo-checksum.json | 1 + vendor/http-auth/CHANGELOG.md | 31 + vendor/http-auth/Cargo.lock | 1092 +++++++++++++++++++++++++++++++++ vendor/http-auth/Cargo.toml | 75 +++ vendor/http-auth/LICENSE-APACHE.txt | 202 ++++++ vendor/http-auth/LICENSE-MIT.txt | 20 + vendor/http-auth/README.md | 99 +++ vendor/http-auth/build.rs | 136 ++++ vendor/http-auth/examples/reqwest.rs | 67 ++ vendor/http-auth/src/basic.rs | 114 ++++ vendor/http-auth/src/digest.rs | 909 +++++++++++++++++++++++++++ vendor/http-auth/src/lib.rs | 747 ++++++++++++++++++++++ vendor/http-auth/src/parser.rs | 593 ++++++++++++++++++ 13 files changed, 4086 insertions(+) create mode 100644 vendor/http-auth/.cargo-checksum.json create mode 100644 vendor/http-auth/CHANGELOG.md create mode 100644 vendor/http-auth/Cargo.lock create mode 100644 vendor/http-auth/Cargo.toml create mode 100644 vendor/http-auth/LICENSE-APACHE.txt create mode 100644 vendor/http-auth/LICENSE-MIT.txt create mode 100644 vendor/http-auth/README.md create mode 100644 vendor/http-auth/build.rs create mode 100644 vendor/http-auth/examples/reqwest.rs create mode 100644 vendor/http-auth/src/basic.rs create mode 100644 vendor/http-auth/src/digest.rs create mode 100644 vendor/http-auth/src/lib.rs create mode 100644 vendor/http-auth/src/parser.rs (limited to 'vendor/http-auth') 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 + + 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 + +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 <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 +// 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 +// 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 = 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 +// 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, +} + +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 { + 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 +// 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 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, + + // 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 { + 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 { + 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 { + 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 { + 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 { + 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 (`-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(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, _> = challenges.iter().map(DigestClient::try_from).collect(); + let mut ctxs = dbg!(ctxs.unwrap()); + assert_eq!(ctxs[1].realm(), "http-auth@example.org"); + assert_eq!(ctxs[1].domain(), ""); + assert_eq!( + ctxs[1].nonce(), + "7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v" + ); + assert_eq!( + ctxs[1].opaque(), + Some("FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS") + ); + assert_eq!(ctxs[1].stale(), false); + assert_eq!(ctxs[1].algorithm(), Algorithm::Md5); + assert_eq!(ctxs[1].qop().0, (Qop::Auth as u8) | (Qop::AuthInt as u8)); + assert_eq!(ctxs[1].nonce_count(), 0); + let params = crate::PasswordParams { + username: "Mufasa", + password: "Circle of Life", + uri: "/dir/index.html", + body: None, + method: "GET", + }; + assert_eq!( + &mut ctxs[0] + .respond_with_testing_cnonce( + ¶ms, + "f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ" + ) + .unwrap(), + "Digest username=\"Mufasa\", \ + realm=\"http-auth@example.org\", \ + uri=\"/dir/index.html\", \ + nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\", \ + algorithm=SHA-256, \ + nc=00000001, \ + cnonce=\"f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ\", \ + qop=auth, \ + response=\"753927fa0e85d155564e2e272a28d1802ca10daf4496794697cf8db5856cb6c1\", \ + opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\"" + ); + assert_eq!(ctxs[0].nc, 1); + assert_eq!( + &mut ctxs[1] + .respond_with_testing_cnonce( + ¶ms, + "f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ" + ) + .unwrap(), + "Digest username=\"Mufasa\", \ + realm=\"http-auth@example.org\", \ + uri=\"/dir/index.html\", \ + nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\", \ + algorithm=MD5, \ + nc=00000001, \ + cnonce=\"f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ\", \ + qop=auth, \ + response=\"8ca523f5e9506fed4657c9700eebdbec\", \ + opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\"" + ); + assert_eq!(ctxs[1].nc, 1); + } + + /// Tests a made-up example with `MD5-sess`. There's no example in the RFC, + /// and these values haven't been tested against any other implementation. + /// But having the test here ensures we don't accidentally change the + /// algorithm. + #[test] + fn md5_sess() { + let www_authenticate = "\ + Digest \ + realm=\"http-auth@example.org\", \ + qop=\"auth, auth-int\", \ + algorithm=MD5-sess, \ + nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\", \ + opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\""; + let challenges = dbg!(crate::parse_challenges(www_authenticate).unwrap()); + assert_eq!(challenges.len(), 1); + let ctxs: Result, _> = challenges.iter().map(DigestClient::try_from).collect(); + let mut ctxs = dbg!(ctxs.unwrap()); + assert_eq!(ctxs[0].realm(), "http-auth@example.org"); + assert_eq!(ctxs[0].domain(), ""); + assert_eq!( + ctxs[0].nonce(), + "7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v" + ); + assert_eq!( + ctxs[0].opaque(), + Some("FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS") + ); + assert_eq!(ctxs[0].stale(), false); + assert_eq!(ctxs[0].algorithm(), Algorithm::Md5); + assert_eq!(ctxs[0].session(), true); + assert_eq!(ctxs[0].qop().0, (Qop::Auth as u8) | (Qop::AuthInt as u8)); + assert_eq!(ctxs[0].nonce_count(), 0); + let params = crate::PasswordParams { + username: "Mufasa", + password: "Circle of Life", + uri: "/dir/index.html", + body: None, + method: "GET", + }; + assert_eq!( + &mut ctxs[0] + .respond_with_testing_cnonce( + ¶ms, + "f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ" + ) + .unwrap(), + "Digest username=\"Mufasa\", \ + realm=\"http-auth@example.org\", \ + uri=\"/dir/index.html\", \ + nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\", \ + algorithm=MD5-sess, \ + nc=00000001, \ + cnonce=\"f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ\", \ + qop=auth, \ + response=\"e783283f46242139c486a698fec7211d\", \ + opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\"" + ); + assert_eq!(ctxs[0].nc, 1); + } + + /// Tests the example from [RFC 7616 section 3.9.2: SHA-512-256, Charset, and + /// Userhash](https://datatracker.ietf.org/doc/html/rfc7616#section-3.9.2). + #[test] + fn sha512_256_charset() { + let www_authenticate = "\ + Digest \ + realm=\"api@example.org\", \ + qop=\"auth\", \ + algorithm=SHA-512-256, \ + nonce=\"5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK\", \ + opaque=\"HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS\", \ + charset=UTF-8, \ + userhash=true"; + let challenges = dbg!(crate::parse_challenges(www_authenticate).unwrap()); + assert_eq!(challenges.len(), 1); + let ctxs: Result, _> = challenges.iter().map(DigestClient::try_from).collect(); + let mut ctxs = dbg!(ctxs.unwrap()); + assert_eq!(ctxs.len(), 1); + assert_eq!(ctxs[0].realm(), "api@example.org"); + assert_eq!(ctxs[0].domain(), ""); + assert_eq!( + ctxs[0].nonce(), + "5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK" + ); + assert_eq!( + ctxs[0].opaque(), + Some("HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS") + ); + assert_eq!(ctxs[0].stale, false); + assert_eq!(ctxs[0].userhash, true); + assert_eq!(ctxs[0].algorithm, Algorithm::Sha512Trunc256); + assert_eq!(ctxs[0].qop.0, Qop::Auth as u8); + assert_eq!(ctxs[0].nc, 0); + let params = crate::PasswordParams { + username: "J\u{E4}s\u{F8}n Doe", + password: "Secret, or not?", + uri: "/doe.json", + body: None, + method: "GET", + }; + + // Note the username and response values in the RFC are *wrong*! + // https://www.rfc-editor.org/errata/eid4897 + assert_eq!( + &mut ctxs[0] + .respond_with_testing_cnonce( + ¶ms, + "NTg6RKcb9boFIAS3KrFK9BGeh+iDa/sm6jUMp2wds69v" + ) + .unwrap(), + "\ + Digest \ + username=\"793263caabb707a56211940d90411ea4a575adeccb7e360aeb624ed06ece9b0b\", \ + userhash=true, \ + realm=\"api@example.org\", \ + uri=\"/doe.json\", \ + nonce=\"5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK\", \ + algorithm=SHA-512-256, \ + nc=00000001, \ + cnonce=\"NTg6RKcb9boFIAS3KrFK9BGeh+iDa/sm6jUMp2wds69v\", \ + qop=auth, \ + response=\"3798d4131c277846293534c3edc11bd8a5e4cdcbff78b05db9d95eeb1cec68a5\", \ + opaque=\"HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS\"" + ); + assert_eq!(ctxs[0].nc, 1); + ctxs[0].userhash = false; + ctxs[0].nc = 0; + assert_eq!( + &mut ctxs[0] + .respond_with_testing_cnonce( + ¶ms, + "NTg6RKcb9boFIAS3KrFK9BGeh+iDa/sm6jUMp2wds69v" + ) + .unwrap(), + "\ + Digest \ + username*=UTF-8''J%C3%A4s%C3%B8n%20Doe, \ + realm=\"api@example.org\", \ + uri=\"/doe.json\", \ + nonce=\"5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK\", \ + algorithm=SHA-512-256, \ + nc=00000001, \ + cnonce=\"NTg6RKcb9boFIAS3KrFK9BGeh+iDa/sm6jUMp2wds69v\", \ + qop=auth, \ + response=\"3798d4131c277846293534c3edc11bd8a5e4cdcbff78b05db9d95eeb1cec68a5\", \ + opaque=\"HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS\"" + ); + assert_eq!(ctxs[0].nc, 1); + } + + #[test] + fn rfc2069() { + // https://datatracker.ietf.org/doc/html/rfc2069#section-2.4 + // The response there is wrong! See https://www.rfc-editor.org/errata/eid749 + let www_authenticate = "\ + Digest \ + realm=\"testrealm@host.com\", \ + nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", \ + opaque=\"5ccc069c403ebaf9f0171e9517f40e41\""; + let challenges = dbg!(crate::parse_challenges(www_authenticate).unwrap()); + assert_eq!(challenges.len(), 1); + let ctxs: Result, _> = challenges.iter().map(DigestClient::try_from).collect(); + let mut ctxs = dbg!(ctxs.unwrap()); + assert_eq!(ctxs.len(), 1); + assert_eq!(ctxs[0].qop.0, Qop::Auth as u8); + assert_eq!(ctxs[0].rfc2069_compat, true); + let params = crate::PasswordParams { + username: "Mufasa", + password: "CircleOfLife", + uri: "/dir/index.html", + body: None, + method: "GET", + }; + assert_eq!( + &mut ctxs[0] + .respond_with_testing_cnonce(¶ms, "unused") + .unwrap(), + "\ + Digest \ + username=\"Mufasa\", \ + realm=\"testrealm@host.com\", \ + uri=\"/dir/index.html\", \ + nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", \ + response=\"1949323746fe6a43ef61f9606e7febea\", \ + opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"", + ); + assert_eq!(ctxs[0].nc, 1); + } + + // See sizes with: cargo test -- --nocapture digest::tests::size + #[test] + fn size() { + // This type should have a niche. + assert_eq!( + dbg!(std::mem::size_of::()), + dbg!(std::mem::size_of::>()), + ) + } +} 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 +// 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>, +} + +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>, +); + +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 { + 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 { + #[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 { + 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 { + 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> for PasswordClient { + type Error = String; + + fn try_from(value: http::header::GetAll<'_, http::HeaderValue>) -> Result { + 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 { + 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, 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 { + 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 +// 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>, + 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>, + token_pos: Range, + 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, + }, + + /// Transitioned from `Equals` on initial `C_TCHAR`. + ParamUnquotedValue { + challenge: ChallengeRef<'i>, + key_pos: Range, + value_start: usize, + }, + + /// Transitioned from `Equals` on initial `"`. + ParamQuotedValue { + challenge: ChallengeRef<'i>, + key_pos: Range, + value_start: usize, + escapes: usize, + in_backslash: bool, + }, +} + +impl<'i> Iterator for ChallengeParser<'i> { + type Item = Result, Error<'i>>; + + fn next(&mut self) -> Option { + 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(); + } +} -- cgit v1.2.3