http-auth-0.1.8/.cargo_vcs_info.json0000644000000001360000000000100127600ustar { "git": { "sha1": "7112b855c584c2bae93ca8cea8fcca151bb33f2b" }, "path_in_vcs": "" }http-auth-0.1.8/.github/workflows/check-license.py000075500000000000000000000032111046102023000202140ustar 00000000000000#!/usr/bin/env python3 # Copyright (C) 2021 Scott Lamb # SPDX-License-Identifier: MIT OR Apache-2.0 """Checks that expected header lines are present. Call in either of two modes: has-license.py FILE [...] check if all files with certain extensions have expected lines. This is useful in a CI action. has-license.py check if stdin has expected lines. This is useful in a pre-commit hook, as in git-format-staged --no-write --formatter '.../has-license.py' '*.rs' """ import re import sys # Filenames matching this regexp are expected to have the header lines. FILENAME_MATCHER = re.compile(r'.*\.rs$') MAX_LINE_COUNT = 10 EXPECTED_LINES = [ re.compile(r'Copyright \(C\) 20\d{2} Scott Lamb '), re.compile(r'SPDX-License-Identifier: MIT OR Apache-2\.0'), ] def has_license(f): """Returns if all of EXPECTED_LINES are present within the first MAX_LINE_COUNT lines of f.""" needed = set(EXPECTED_LINES) i = 0 for line in f: if i == 10: break i += 1 for e in needed: if e.search(line): needed.remove(e) break if not needed: return True return False def file_has_license(filename): with open(filename, 'r') as f: return has_license(f) def main(args): if not args: sys.exit(0 if has_license(sys.stdin) else 1) missing = [f for f in args if FILENAME_MATCHER.match(f) and not file_has_license(f)] if missing: print('The following files are missing expected copyright/license headers:', file=sys.stderr) print('\n'.join(missing), file=sys.stderr) sys.exit(1) if __name__ == '__main__': main(sys.argv[1:]) http-auth-0.1.8/.github/workflows/ci.yml000064400000000000000000000030101046102023000162550ustar 00000000000000name: CI on: [push, pull_request] env: CARGO_TERM_COLOR: always RUST_BACKTRACE: 1 jobs: rust: name: Test strategy: matrix: rust: - stable - 1.57 include: - rust: stable extra_components: rustfmt runs-on: ubuntu-20.04 steps: - name: Checkout uses: actions/checkout@v2 - name: Cache uses: actions/cache@v2 with: path: | ~/.cargo/registry ~/.cargo/git target key: ${{ matrix.rust }}-cargo-${{ hashFiles('**/Cargo.lock') }} - name: Install Rust uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: ${{ matrix.rust }} override: true components: ${{ matrix.extra_components }} - name: Test with all features run: cargo test --all-features --all-targets - name: Compile with no features run: cargo check --no-default-features - name: Check fuzz tests compile (but don't actually fuzz) run: cd fuzz && cargo check && cargo test - name: Check main crate formatting if: matrix.rust == 'stable' run: cargo fmt -- --check - name: Check fuzz crate formatting if: matrix.rust == 'stable' run: cd fuzz && cargo fmt -- --check license: name: Check copyright/license headers runs-on: ubuntu-20.04 steps: - name: Checkout uses: actions/checkout@v2 - run: find . -name target -prune -o -type f -print0 | xargs -0 .github/workflows/check-license.py http-auth-0.1.8/.gitignore000064400000000000000000000000101046102023000135270ustar 00000000000000/target http-auth-0.1.8/CHANGELOG.md000064400000000000000000000014611046102023000133630ustar 00000000000000## `v0.1.8` (2023-01-30) * upgrade `base64` dependency from 0.20 to 0.21. ## `v0.1.7` (2023-01-05) * bump minimum Rust version to 1.57. * upgrade `base64` dependency from 0.13 to 0.20. ## `v0.1.6` (2022-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 http-auth-0.1.8/Cargo.lock0000644000000713740000000000100107470ustar # 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.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "base64" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" [[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 0.10.2+wasi-snapshot-preview1", ] [[package]] name = "h2" version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" 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 0.4.8", ] [[package]] name = "http-auth" version = "0.1.8" 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.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[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.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c" dependencies = [ "bytes", "futures-channel", "futures-core", "futures-util", "h2", "http", "http-body", "httparse", "httpdate", "itoa 1.0.5", "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 = "itoa" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" [[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.139" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" [[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.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" dependencies = [ "libc", "log", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys", ] [[package]] name = "native-tls" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" dependencies = [ "lazy_static", "libc", "log", "openssl", "openssl-probe", "openssl-sys", "schannel", "security-framework", "security-framework-sys", "tempfile", ] [[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.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21eed90ec8570952d53b772ecf8f206aa1ec9a3d76b2521c56c42973f2d91ee9" dependencies = [ "base64", "bytes", "encoding_rs", "futures-core", "futures-util", "h2", "http", "http-body", "hyper", "hyper-tls", "ipnet", "js-sys", "log", "mime", "native-tls", "once_cell", "percent-encoding", "pin-project-lite", "serde", "serde_json", "serde_urlencoded", "tokio", "tokio-native-tls", "tower-service", "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 0.4.8", "ryu", "serde", ] [[package]] name = "serde_urlencoded" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", "itoa 1.0.5", "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.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" 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.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af" dependencies = [ "autocfg", "bytes", "libc", "memchr", "mio", "num_cpus", "pin-project-lite", "socket2", "windows-sys", ] [[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.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", "tracing", ] [[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 = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[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 = "windows-sys" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" [[package]] name = "windows_aarch64_msvc" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" [[package]] name = "windows_i686_gnu" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" [[package]] name = "windows_i686_msvc" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" [[package]] name = "windows_x86_64_gnu" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" [[package]] name = "windows_x86_64_gnullvm" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" [[package]] name = "windows_x86_64_msvc" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" [[package]] name = "winreg" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" dependencies = [ "winapi", ] http-auth-0.1.8/Cargo.toml0000644000000036630000000000100107660ustar # 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" rust-version = "1.57.0" name = "http-auth" version = "0.1.8" 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] rustdoc-args = [ "--cfg", "docsrs", ] all-features = true [[example]] name = "reqwest" required-features = ["http"] [dependencies.base64] version = "0.21.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"] http-auth-0.1.8/Cargo.toml.orig000064400000000000000000000033151046102023000144410ustar 00000000000000[package] name = "http-auth" version = "0.1.8" license = "MIT/Apache-2.0" readme = "README.md" description = "HTTP authentication: parse challenge lists, respond to Basic and Digest challenges. Likely to be extended with server support and additional auth schemes." keywords = ["http", "authentication", "digest", "basic"] edition = "2018" resolver = "2" categories = [ "authentication", "parser-implementations", "web-programming::http-client", ] repository = "https://github.com/scottlamb/http-auth" rust-version = "1.57.0" [features] default = ["basic-scheme", "digest-scheme"] # Enable code to respond to challenges of the given scheme. basic-scheme = ["base64"] digest-scheme = ["digest", "hex", "md-5", "rand", "sha2"] # Enable per-byte trace! calls in parsing (causing code bloat). This is only # meant for testing http-auth itself. trace = ["log"] [package.metadata.docs.rs] # https://docs.rs/about/metadata # To generate docs locally, run: RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features rustdoc-args = ["--cfg", "docsrs"] all-features = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] base64 = { version = "0.21.0", optional = true } digest = { version = "0.10", optional = true } hex = { version = "0.4", optional = true } http = { version = "0.2.5", optional = true } log = {version = "0.4", optional = true } md-5 = { version = "0.10", optional = true } memchr = "2.4.1" rand = { version = "0.8.4", optional = true } sha2 = { version = "0.10", optional = true } [dev-dependencies] pretty_assertions = "1.0.0" reqwest = { version = "0.11.6", features = ["blocking"] } [[example]] name = "reqwest" required-features = ["http"] http-auth-0.1.8/LICENSE-APACHE.txt000064400000000000000000000261371046102023000143230ustar 00000000000000 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. http-auth-0.1.8/LICENSE-MIT.txt000064400000000000000000000021061046102023000140210ustar 00000000000000The 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. http-auth-0.1.8/README.md000064400000000000000000000057101046102023000130320ustar 00000000000000[![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 Well-tested, suitable for production. 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. 2. **correct.** Precisely implements the specifications except where noted. Fuzz tests verify the hand-written parser never panics and matches a nom-based reference implementation. 3. **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. 4. **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.) 5. **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). 6. **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. ## 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. http-auth-0.1.8/design/20211020-new-crate.md000064400000000000000000000037561046102023000161300ustar 00000000000000# Write a new HTTP authentication crate Date: 2022-10-20 # Problem statement I'd like a crate for HTTP authentication that has the following goals (described more in [`http-auth`'s README](../README.md)): 1. sound 2. correct 3. light-weight 4. complete 5. ergonomic 6. fast enough ## Considered options * Write a new crate * Use/extend an existing crate The existing crates don't seem to match these goals partially well: ### [`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 "sound" and "light-weight" `www-authenticate` caveats above, responding to password challenges by using both `www-authenticate` and `digest_auth` is still incomplete and not 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. ## Decision Outcome Write the new `http-auth` crate. http-auth-0.1.8/design/README.md000064400000000000000000000001251046102023000142760ustar 00000000000000This directory records design decisions, in the [ADR style](https://adr.github.io/). http-auth-0.1.8/examples/reqwest.rs000064400000000000000000000043361046102023000154340ustar 00000000000000// 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() ); } http-auth-0.1.8/src/basic.rs000064400000000000000000000073171046102023000137760ustar 00000000000000// 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 { use base64::Engine as _; 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::engine::general_purpose::STANDARD.encode_string(&user_pass[..], &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=="); } } http-auth-0.1.8/src/digest.rs000064400000000000000000001023541046102023000141710ustar 00000000000000// 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::>()), ) } } http-auth-0.1.8/src/lib.rs000064400000000000000000000626601046102023000134650ustar 00000000000000// 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; mod table; 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; use crate::table::{char_classes, C_ESCAPABLE, C_OWS, C_QDTEXT, C_TCHAR}; #[cfg(feature = "digest-scheme")] use crate::table::C_ATTR; /// 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().copied()).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" ); } } http-auth-0.1.8/src/parser.rs000064400000000000000000000551101046102023000142030ustar 00000000000000// 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_args!( "{}(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(); } } http-auth-0.1.8/src/table.rs000064400000000000000000000076461046102023000140110ustar 00000000000000// Copyright (C) 2021 Scott Lamb // SPDX-License-Identifier: MIT OR Apache-2.0 //! Builds and offers lookup on 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). pub(crate) const C_TCHAR: u8 = 1; pub(crate) const C_QDTEXT: u8 = 2; pub(crate) const C_ESCAPABLE: u8 = 4; pub(crate) const C_OWS: u8 = 8; pub(crate) const C_ATTR: u8 = 16; static TABLE: [u8; 128] = build_table(); pub(crate) fn char_classes(b: u8) -> u8 { *TABLE.get(usize::from(b)).unwrap_or(&0) } const fn build_table() -> [u8; 128] { // It'd be nice to use array::from_fn here, but it wasn't stablized until Rust 1.63. let mut table = [0u8; 128]; let mut i = 0; while i < 128 { 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; } table[i] = classes; i += 1; } table } /// 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). const 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 /// ``` const 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). const 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 ( "*" / "'" / "%" ) /// ``` const 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 /// ``` const fn is_ows(b: u8) -> bool { matches!(b, b' ' | b'\t') }