diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index df43cab..ed079a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,12 +34,16 @@ jobs: run: cargo clippy --all-targets --no-default-features -- -D warnings - name: Run clippy (discovery only) run: cargo clippy --all-targets --no-default-features --features discovery -- -D warnings + - name: Run clippy (http-config only) + run: cargo clippy --all-targets --no-default-features --features http-config -- -D warnings - name: Run tests (all features) run: cargo test --all-features - name: Run tests (no default features) run: cargo test --no-default-features - name: Run tests (discovery only) run: cargo test --no-default-features --features discovery + - name: Run tests (http-config only) + run: cargo test --no-default-features --features http-config - name: Run clippy (OTAP compatibility suite) env: CARGO_TARGET_DIR: target @@ -62,6 +66,8 @@ jobs: run: cargo check --all-targets --no-default-features - name: Check MSRV (discovery only) run: cargo check --all-targets --no-default-features --features discovery + - name: Check MSRV (http-config only) + run: cargo check --all-targets --no-default-features --features http-config go-fixtures: name: Go testdata sync check diff --git a/Cargo.lock b/Cargo.lock index 5d5e465..0208e1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -89,12 +89,47 @@ version = "59.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e87acb0c6bf6237fca7a50d7b51e801c98a18a5fe6f3bb34f7d77b85ca165388" +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "aws-lc-rs" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "048006790404103c488e6c5616b3e7e702a71292aadda08e8f7698a9e0d2eb53" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bfbe32ac36b3c428696ce315464181bc4029d689fd58e3f2a4e58c252242f52" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "benchfmt" version = "0.1.0" @@ -125,6 +160,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "375989decec1a0fa424442bed520fa12ede43a48475ce8c7417d2c69b32b5f6b" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -134,6 +171,23 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + [[package]] name = "chrono" version = "0.4.45" @@ -145,6 +199,25 @@ dependencies = [ "windows-link", ] +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "combine" +version = "4.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4356eba6976435498d05a311ee9fc486da3bc81c200d3f0d7dd9703b41913f92" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "const-random" version = "0.1.18" @@ -165,18 +238,54 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + [[package]] name = "crunchy" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "either" version = "1.19.0" @@ -196,7 +305,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -211,10 +320,14 @@ dependencies = [ "prost-build", "prost-types", "protox", + "reqwest", + "rustls", + "rustls-pki-types", "serde", "serde_json", "thiserror", "tokio", + "tokio-rustls", ] [[package]] @@ -247,12 +360,42 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + [[package]] name = "futures-core" version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + [[package]] name = "futures-task" version = "0.3.34" @@ -278,8 +421,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -290,10 +435,43 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", ] +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core", + "wasm-bindgen", +] + +[[package]] +name = "h2" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d29020232d6aa3fb1daca64c1127cf662cf97f254ae16c18c05b8ab635fc118" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "half" version = "2.7.1" @@ -327,6 +505,105 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + [[package]] name = "iana-time-zone" version = "0.1.65" @@ -351,6 +628,110 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe090a42a0d3140fed8c63263ed1fecc02422cb2f4bfe55cd8de2de1b8e105dd" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25d29b57c17c6528d0dc4c2141cfa2d08200465acb537a3909c8b9b8a4a984c0" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "404a3b8c486f205c87fe7f23ab6894d84a4ad76d6e4671ae1981ba7def87c599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20f0088bfb7441f7a37fa0c9e20cb1e9cba1cac6b9ce8cd2a6dc44294f8e8cbc" + +[[package]] +name = "icu_properties" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ea83d991c56158f7cf898bf86436c1dfe7c07ee82fe88951f2695c9f37a379f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98f29af82d02b163fe50161388350e743bb3107cc8e8c2f7314cb505faddfcf" + +[[package]] +name = "icu_provider" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413099315636c588ee8f011190b329b971a09a8fae9d157e01dcae8379468514" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" version = "2.14.2" @@ -361,6 +742,12 @@ dependencies = [ "hashbrown 0.17.1", ] +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + [[package]] name = "itertools" version = "0.14.0" @@ -376,6 +763,65 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bd1574e617a9a6c1f8f8181bd7e841e1f20e4277607ef28945765188d6284d4" +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + [[package]] name = "js-sys" version = "0.3.107" @@ -405,6 +851,12 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + [[package]] name = "log" version = "0.4.34" @@ -443,6 +895,12 @@ dependencies = [ "logos-codegen", ] +[[package]] +name = "lru-slab" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" + [[package]] name = "memchr" version = "2.8.3" @@ -479,7 +937,7 @@ checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -532,6 +990,18 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "openssl-probe" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e036b411754132fd6b74e94b74e9a13c5cf606fa93d1885c35860d3060820b1" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "petgraph" version = "0.8.3" @@ -550,7 +1020,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] -name = "prettyplease" +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "prettyplease" version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" @@ -658,6 +1143,63 @@ dependencies = [ "thiserror", ] +[[package]] +name = "quinn" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e750cca55fe4f0439a15d0bb529da9651e79993e8e72c61a899a36d462befbe" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af66907df18639dcf4db56ca65490cabc4b27a97dbadd96f2926cca73298f016" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + [[package]] name = "quote" version = "1.0.47" @@ -673,6 +1215,38 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core", +] + [[package]] name = "regex" version = "1.13.1" @@ -702,6 +1276,71 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +[[package]] +name = "reqwest" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" +dependencies = [ + "base64", + "bytes", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "1.1.6" @@ -712,7 +1351,82 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a073f5dc7469f984c52ad2752b63b0807745133b6de880b7b64c1ac4c48aec4" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f" + +[[package]] +name = "rustls-webpki" +version = "0.103.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] @@ -721,6 +1435,53 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cb23c424d8ee841bb4d813677ca237e2a610067d594fec130f87d6093495ab5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.229" @@ -770,12 +1531,34 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "slab" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "smallvec" +version = "1.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "149309498a5f7334b12ddae0cad8aba81ce785fe91755068867e017cf98fee46" + [[package]] name = "socket2" version = "0.6.5" @@ -783,9 +1566,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.119" @@ -808,6 +1603,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + [[package]] name = "tempfile" version = "3.27.0" @@ -817,7 +1632,7 @@ dependencies = [ "fastrand", "once_cell", "rustix", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -849,6 +1664,22 @@ dependencies = [ "crunchy", ] +[[package]] +name = "tinystr" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abc840c238154d58f9e86b2792261edfde565a17696e1cda1a449de27979801a" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + [[package]] name = "tokio" version = "1.54.1" @@ -861,7 +1692,7 @@ dependencies = [ "pin-project-lite", "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -875,6 +1706,100 @@ dependencies = [ "syn 3.0.9", ] +[[package]] +name = "tokio-rustls" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "unicode-ident" version = "1.0.27" @@ -887,12 +1812,64 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af3bd9d3e571c26fcce9d033b87ea5ab061b8d2ffe9b7a5675d810d68f03edf0" +dependencies = [ + "utf_types", +] + +[[package]] +name = "utf_types" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3b425286980a367278453f3b6d28cdcb134b574e5a35824a6ff5eebad50bb6" + [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -920,6 +1897,17 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e14e0327cb5368ad60ccb22d04a9827a693c95a926d8d125e9fa39e0baec501" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.130" @@ -952,6 +1940,44 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d179596ff5ec0f6de4db64d54011bde6289cc49603d3a6e4ce3f329207ab88" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e49d0e65abc00c142e8f9428da991da9f1a250c39be97459901eb12b9b49f62a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -1011,6 +2037,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -1020,12 +2055,105 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "yoke" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417ef3fcf6c300b7dbd25c85531a746931c02ad536f3cf4786ed1e88223340b3" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.63" @@ -1046,6 +2174,66 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58a2799cfe9a1670c7ea8edbd52ef8edb93cb37a7f83bf31ba9e35d9dae6a40" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3743ddbf47738d929cd1ae2e199d40d0162979dfe31aef8ae646d2bf00bf97a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + [[package]] name = "zmij" version = "1.0.23" diff --git a/Cargo.toml b/Cargo.toml index a577dfa..b2eae54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ default = ["arrow", "dto"] arrow = ["dep:arrow-array", "dep:arrow-buffer", "dep:arrow-schema"] discovery = ["dep:serde", "dep:serde_json", "dep:tokio"] dto = ["dep:prost", "dep:prost-types"] +http-config = ["dep:reqwest", "dep:serde", "dep:serde_json"] [dependencies] arrow-array = { version = "59", default-features = false, optional = true } @@ -24,6 +25,7 @@ arrow-buffer = { version = "59", default-features = false, optional = true } arrow-schema = { version = "59", default-features = false, optional = true } prost = { version = "0.14", optional = true } prost-types = { version = "0.14", optional = true } +reqwest = { version = "0.13", default-features = false, features = ["http2", "rustls"], optional = true } serde = { version = "1", features = ["derive"], optional = true } serde_json = { version = "1", optional = true } thiserror = "2" @@ -33,7 +35,10 @@ tokio = { version = "1", default-features = false, features = ["io-util", "net", benchfmt = { git = "https://github.com/bwplotka/benchfmt.rs" } prost-build = "=0.14.4" protox = "0.10" -tokio = { version = "1", default-features = false, features = ["macros", "rt"] } +rustls = { version = "0.23", default-features = false, features = ["aws-lc-rs", "std", "tls12"] } +rustls-pki-types = "1" +tokio = { version = "1", default-features = false, features = ["io-util", "macros", "net", "rt", "time"] } +tokio-rustls = { version = "0.26", default-features = false, features = ["tls12"] } [[bench]] name = "decode" diff --git a/src/http_config.rs b/src/http_config.rs new file mode 100644 index 0000000..5a70480 --- /dev/null +++ b/src/http_config.rs @@ -0,0 +1,1270 @@ +// Copyright 2026 The Prometheus Authors +// 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. + +//! Prometheus-compatible `HTTPClientConfig` schema, validation, and `reqwest` client helpers. + +use std::collections::BTreeMap; +use std::fmt; +use std::fs; +use std::path::Path; +use std::time::Duration; + +use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderName, HeaderValue}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +const REDACTED_SECRET: &str = ""; + +const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(10); +const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(30); + +const GRANT_TYPE_JWT_BEARER: &str = "urn:ietf:params:oauth:grant-type:jwt-bearer"; + +const RESERVED_HEADERS: &[&str] = &[ + "authorization", + "host", + "content-encoding", + "content-length", + "content-type", + "user-agent", + "connection", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "www-authenticate", + "accept-encoding", + "x-prometheus-remote-write-version", + "x-prometheus-remote-read-version", + "x-prometheus-scrape-timeout-seconds", + "x-amz-date", + "x-amz-security-token", + "x-amz-content-sha256", +]; + +const fn default_true() -> bool { + true +} + +/// Error returned when validating or constructing an [`HttpClientConfig`]. +#[derive(Debug, thiserror::Error)] +pub enum HttpConfigError { + /// Configuration validation failed. + #[error("{0}")] + InvalidConfig(String), + /// I/O error reading a referenced credentials, certificate, or header file. + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), + /// HTTP client construction error. + #[error("HTTP error: {0}")] + Http(#[from] reqwest::Error), +} + +/// TLS protocol version matching Prometheus `config.TLSVersion`. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum TlsVersion { + /// TLS 1.0 (`"TLS10"`). + Tls10, + /// TLS 1.1 (`"TLS11"`). + Tls11, + /// TLS 1.2 (`"TLS12"`). + Tls12, + /// TLS 1.3 (`"TLS13"`). + Tls13, +} + +impl TlsVersion { + /// Returns the Prometheus string identifier for this TLS version. + pub const fn as_str(self) -> &'static str { + match self { + Self::Tls10 => "TLS10", + Self::Tls11 => "TLS11", + Self::Tls12 => "TLS12", + Self::Tls13 => "TLS13", + } + } + + const fn to_reqwest(self) -> reqwest::tls::Version { + match self { + Self::Tls10 => reqwest::tls::Version::TLS_1_0, + Self::Tls11 => reqwest::tls::Version::TLS_1_1, + Self::Tls12 => reqwest::tls::Version::TLS_1_2, + Self::Tls13 => reqwest::tls::Version::TLS_1_3, + } + } +} + +impl Serialize for TlsVersion { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_str(self.as_str()) + } +} + +impl<'de> Deserialize<'de> for TlsVersion { + fn deserialize>(deserializer: D) -> Result { + let s = String::deserialize(deserializer)?; + match s.as_str() { + "TLS10" => Ok(Self::Tls10), + "TLS11" => Ok(Self::Tls11), + "TLS12" => Ok(Self::Tls12), + "TLS13" => Ok(Self::Tls13), + _ => Err(serde::de::Error::custom(format!( + "unknown TLS version: {s}" + ))), + } + } +} + +/// HTTP Basic authentication configuration matching Prometheus `config.BasicAuth`. +#[derive(Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct BasicAuth { + /// Inline HTTP basic auth username. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub username: String, + /// Path to file containing the HTTP basic auth username. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub username_file: String, + /// Secret manager reference for the HTTP basic auth username. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub username_ref: String, + /// Inline HTTP basic auth password. + #[serde( + default, + skip_serializing_if = "String::is_empty", + serialize_with = "serialize_secret" + )] + pub password: String, + /// Path to file containing the HTTP basic auth password. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub password_file: String, + /// Secret manager reference for the HTTP basic auth password. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub password_ref: String, +} + +impl fmt::Debug for BasicAuth { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("BasicAuth") + .field("username", &self.username) + .field("username_file", &self.username_file) + .field("username_ref", &self.username_ref) + .field("password", &redact_str(&self.password)) + .field("password_file", &self.password_file) + .field("password_ref", &self.password_ref) + .finish() + } +} + +impl BasicAuth { + /// Joins relative file paths with `dir`. + pub fn set_directory(&mut self, dir: &Path) { + self.username_file = join_dir(dir, &self.username_file); + self.password_file = join_dir(dir, &self.password_file); + } + + fn resolve_username(&self) -> Result { + if !self.username_file.is_empty() { + Ok(fs::read_to_string(&self.username_file)?.trim().to_owned()) + } else { + Ok(self.username.clone()) + } + } + + fn resolve_password(&self) -> Result { + if !self.password_file.is_empty() { + Ok(fs::read_to_string(&self.password_file)?.trim().to_owned()) + } else { + Ok(self.password.clone()) + } + } +} + +/// HTTP `Authorization` header configuration matching Prometheus `config.Authorization`. +#[derive(Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Authorization { + /// Authentication scheme type (defaults to `"Bearer"`). + #[serde(rename = "type", default, skip_serializing_if = "String::is_empty")] + pub auth_type: String, + /// Inline credentials token. + #[serde( + default, + skip_serializing_if = "String::is_empty", + serialize_with = "serialize_secret" + )] + pub credentials: String, + /// Path to file containing the credentials token. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub credentials_file: String, + /// Secret manager reference for the credentials token. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub credentials_ref: String, +} + +impl fmt::Debug for Authorization { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Authorization") + .field("auth_type", &self.auth_type) + .field("credentials", &redact_str(&self.credentials)) + .field("credentials_file", &self.credentials_file) + .field("credentials_ref", &self.credentials_ref) + .finish() + } +} + +impl Authorization { + /// Joins relative file paths with `dir`. + pub fn set_directory(&mut self, dir: &Path) { + self.credentials_file = join_dir(dir, &self.credentials_file); + } + + fn resolve_header_value(&self) -> Result, HttpConfigError> { + let creds = if !self.credentials_file.is_empty() { + fs::read_to_string(&self.credentials_file)? + .trim() + .to_owned() + } else { + self.credentials.trim().to_owned() + }; + if creds.is_empty() { + return Ok(None); + } + let scheme = if self.auth_type.trim().is_empty() { + "Bearer" + } else { + self.auth_type.trim() + }; + Ok(Some(format!("{scheme} {creds}"))) + } +} + +/// TLS connection configuration matching Prometheus `config.TLSConfig`. +#[derive(Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct TlsConfig { + /// Inline CA certificate PEM text. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub ca: String, + /// Inline client certificate PEM text. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub cert: String, + /// Inline client private key PEM text. + #[serde( + default, + skip_serializing_if = "String::is_empty", + serialize_with = "serialize_secret" + )] + pub key: String, + /// Path to CA certificate PEM file. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub ca_file: String, + /// Path to client certificate PEM file. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub cert_file: String, + /// Path to client private key PEM file. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub key_file: String, + /// Secret manager reference for CA certificate. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub ca_ref: String, + /// Secret manager reference for client certificate. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub cert_ref: String, + /// Secret manager reference for client private key. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub key_ref: String, + /// ServerName used to verify the hostname on returned certificates. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub server_name: String, + /// Disables target certificate validation. + #[serde(default)] + pub insecure_skip_verify: bool, + /// Minimum acceptable TLS version. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub min_version: Option, + /// Maximum acceptable TLS version. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub max_version: Option, +} + +impl fmt::Debug for TlsConfig { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TlsConfig") + .field("ca", &self.ca) + .field("cert", &self.cert) + .field("key", &redact_str(&self.key)) + .field("ca_file", &self.ca_file) + .field("cert_file", &self.cert_file) + .field("key_file", &self.key_file) + .field("ca_ref", &self.ca_ref) + .field("cert_ref", &self.cert_ref) + .field("key_ref", &self.key_ref) + .field("server_name", &self.server_name) + .field("insecure_skip_verify", &self.insecure_skip_verify) + .field("min_version", &self.min_version) + .field("max_version", &self.max_version) + .finish() + } +} + +impl TlsConfig { + /// Joins relative file paths with `dir`. + pub fn set_directory(&mut self, dir: &Path) { + self.ca_file = join_dir(dir, &self.ca_file); + self.cert_file = join_dir(dir, &self.cert_file); + self.key_file = join_dir(dir, &self.key_file); + } + + /// Validates this TLS configuration according to Prometheus `TLSConfig.Validate`. + pub fn validate(&self) -> Result<(), HttpConfigError> { + if non_empty_count(&[&self.ca, &self.ca_file, &self.ca_ref]) > 1 { + return Err(HttpConfigError::InvalidConfig( + "at most one of ca, ca_file & ca_ref must be configured".to_owned(), + )); + } + if non_empty_count(&[&self.cert, &self.cert_file, &self.cert_ref]) > 1 { + return Err(HttpConfigError::InvalidConfig( + "at most one of cert, cert_file & cert_ref must be configured".to_owned(), + )); + } + if non_empty_count(&[&self.key, &self.key_file, &self.key_ref]) > 1 { + return Err(HttpConfigError::InvalidConfig( + "at most one of key and key_file must be configured".to_owned(), + )); + } + if self.using_client_cert() && !self.using_client_key() { + return Err(HttpConfigError::InvalidConfig( + "exactly one of key or key_file must be configured when a client certificate is configured".to_owned(), + )); + } + if self.using_client_key() && !self.using_client_cert() { + return Err(HttpConfigError::InvalidConfig( + "exactly one of cert or cert_file must be configured when a client key is configured".to_owned(), + )); + } + if let (Some(min), Some(max)) = (self.min_version, self.max_version) + && max < min + { + return Err(HttpConfigError::InvalidConfig( + "tls_config.max_version must be greater than or equal to tls_config.min_version if both are specified".to_owned(), + )); + } + Ok(()) + } + + fn using_client_cert(&self) -> bool { + !self.cert.is_empty() || !self.cert_file.is_empty() || !self.cert_ref.is_empty() + } + + fn using_client_key(&self) -> bool { + !self.key.is_empty() || !self.key_file.is_empty() || !self.key_ref.is_empty() + } + + fn apply_to_builder( + &self, + mut builder: reqwest::ClientBuilder, + ) -> Result { + self.validate()?; + if !self.ca_ref.is_empty() || !self.cert_ref.is_empty() || !self.key_ref.is_empty() { + return Err(HttpConfigError::InvalidConfig( + "cannot use secret ref without manager".to_owned(), + )); + } + if !self.server_name.is_empty() { + return Err(HttpConfigError::InvalidConfig( + "tls_config.server_name override is not yet supported".to_owned(), + )); + } + if self.insecure_skip_verify { + builder = builder.danger_accept_invalid_certs(true); + } + if !self.ca.is_empty() || !self.ca_file.is_empty() { + let ca_pem = if !self.ca.is_empty() { + self.ca.as_bytes().to_vec() + } else { + fs::read(&self.ca_file)? + }; + let certs = reqwest::Certificate::from_pem_bundle(&ca_pem)?; + if certs.is_empty() { + return Err(HttpConfigError::InvalidConfig( + "unable to parse CA certificate PEM".to_owned(), + )); + } + builder = builder.tls_certs_only(certs); + } + if self.using_client_cert() { + let cert_pem = if !self.cert.is_empty() { + self.cert.as_bytes().to_vec() + } else { + fs::read(&self.cert_file)? + }; + let key_pem = if !self.key.is_empty() { + self.key.as_bytes().to_vec() + } else { + fs::read(&self.key_file)? + }; + let mut combined = Vec::with_capacity(cert_pem.len() + 1 + key_pem.len()); + combined.extend_from_slice(&cert_pem); + if !combined.ends_with(b"\n") { + combined.push(b'\n'); + } + combined.extend_from_slice(&key_pem); + let identity = reqwest::Identity::from_pem(&combined)?; + builder = builder.identity(identity); + } + if let Some(min) = self.min_version { + builder = builder.min_tls_version(min.to_reqwest()); + } + if let Some(max) = self.max_version { + builder = builder.max_tls_version(max.to_reqwest()); + } + Ok(builder) + } +} + +/// HTTP proxy configuration matching Prometheus `config.ProxyConfig`. +#[derive(Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ProxyConfig { + /// HTTP proxy server URL. + #[serde( + default, + skip_serializing_if = "Option::is_none", + serialize_with = "serialize_opt_url" + )] + pub proxy_url: Option, + /// Comma-separated addresses that should bypass the proxy. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub no_proxy: String, + /// Reads proxy settings from environment variables (`HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`). + #[serde(default, skip_serializing_if = "is_false")] + pub proxy_from_environment: bool, + /// Headers sent to proxies during `CONNECT` requests. + #[serde( + default, + skip_serializing_if = "BTreeMap::is_empty", + serialize_with = "serialize_secret_header_map" + )] + pub proxy_connect_header: BTreeMap>, +} + +impl fmt::Debug for ProxyConfig { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let redacted_url = self.proxy_url.as_deref().map(redact_url); + let redacted_headers: BTreeMap<&str, Vec<&str>> = self + .proxy_connect_header + .iter() + .map(|(k, vals)| { + ( + k.as_str(), + vals.iter().map(|v| redact_str(v.as_str())).collect(), + ) + }) + .collect(); + f.debug_struct("ProxyConfig") + .field("proxy_url", &redacted_url) + .field("no_proxy", &self.no_proxy) + .field("proxy_from_environment", &self.proxy_from_environment) + .field("proxy_connect_header", &redacted_headers) + .finish() + } +} + +impl ProxyConfig { + /// Validates proxy settings according to Prometheus `ProxyConfig.Validate`. + pub fn validate(&self) -> Result<(), HttpConfigError> { + let has_proxy_url = self.proxy_url.as_deref().is_some_and(|u| !u.is_empty()); + if let Some(url) = &self.proxy_url + && !url.is_empty() + { + let parsed = reqwest::Url::parse(url).map_err(|e| { + HttpConfigError::InvalidConfig(format!("invalid proxy_url {url:?}: {e}")) + })?; + if !matches!(parsed.scheme(), "http" | "https") { + return Err(HttpConfigError::InvalidConfig(format!( + "invalid proxy_url scheme {:?} in {url:?}, expected \"http\" or \"https\"", + parsed.scheme() + ))); + } + } + if !self.proxy_connect_header.is_empty() && !self.proxy_from_environment && !has_proxy_url { + return Err(HttpConfigError::InvalidConfig( + "if proxy_connect_header is configured, proxy_url or proxy_from_environment must also be configured".to_owned(), + )); + } + if self.proxy_from_environment && has_proxy_url { + return Err(HttpConfigError::InvalidConfig( + "if proxy_from_environment is configured, proxy_url must not be configured" + .to_owned(), + )); + } + if self.proxy_from_environment && !self.no_proxy.is_empty() { + return Err(HttpConfigError::InvalidConfig( + "if proxy_from_environment is configured, no_proxy must not be configured" + .to_owned(), + )); + } + if !has_proxy_url && !self.no_proxy.is_empty() { + return Err(HttpConfigError::InvalidConfig( + "if no_proxy is configured, proxy_url must also be configured".to_owned(), + )); + } + Ok(()) + } +} + +/// OAuth2 client credentials configuration matching Prometheus `config.OAuth2`. +#[derive(Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct OAuth2 { + /// OAuth2 client ID. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub client_id: String, + /// Inline OAuth2 client secret. + #[serde( + default, + skip_serializing_if = "String::is_empty", + serialize_with = "serialize_secret" + )] + pub client_secret: String, + /// Path to file containing the OAuth2 client secret. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub client_secret_file: String, + /// Secret manager reference for the OAuth2 client secret. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub client_secret_ref: String, + /// Key ID for JWT bearer client certificate authentication. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub client_certificate_key_id: String, + /// Inline client certificate private key for JWT bearer grant. + #[serde( + default, + skip_serializing_if = "String::is_empty", + serialize_with = "serialize_secret" + )] + pub client_certificate_key: String, + /// File path to client certificate private key for JWT bearer grant. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub client_certificate_key_file: String, + /// Secret manager reference to client certificate private key for JWT bearer grant. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub client_certificate_key_ref: String, + /// OAuth2 grant type (`"client_credentials"` or `"urn:ietf:params:oauth:grant-type:jwt-bearer"`). + #[serde(default, skip_serializing_if = "String::is_empty")] + pub grant_type: String, + /// RSA signature algorithm (`"RS256"`, `"RS384"`, `"RS512"`). + #[serde(default, skip_serializing_if = "String::is_empty")] + pub signature_algorithm: String, + /// OAuth2 JWT issuer claim. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub iss: String, + /// OAuth2 JWT audience claim. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub audience: String, + /// Additional JWT claims. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub claims: BTreeMap, + /// OAuth2 scopes. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub scopes: Vec, + /// OAuth2 token endpoint URL. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub token_url: String, + /// Additional parameters appended to token endpoint requests. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub endpoint_params: BTreeMap, + /// TLS configuration for token requests. + #[serde(default, skip_serializing_if = "is_default_tls")] + pub tls_config: TlsConfig, + /// Proxy configuration for token requests. + #[serde(flatten)] + pub proxy_config: ProxyConfig, +} + +impl fmt::Debug for OAuth2 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("OAuth2") + .field("client_id", &self.client_id) + .field("client_secret", &redact_str(&self.client_secret)) + .field("client_secret_file", &self.client_secret_file) + .field("client_secret_ref", &self.client_secret_ref) + .field("client_certificate_key_id", &self.client_certificate_key_id) + .field( + "client_certificate_key", + &redact_str(&self.client_certificate_key), + ) + .field( + "client_certificate_key_file", + &self.client_certificate_key_file, + ) + .field( + "client_certificate_key_ref", + &self.client_certificate_key_ref, + ) + .field("grant_type", &self.grant_type) + .field("signature_algorithm", &self.signature_algorithm) + .field("iss", &self.iss) + .field("audience", &self.audience) + .field("claims", &self.claims) + .field("scopes", &self.scopes) + .field("token_url", &self.token_url) + .field("endpoint_params", &self.endpoint_params) + .field("tls_config", &self.tls_config) + .field("proxy_config", &self.proxy_config) + .finish() + } +} + +impl OAuth2 { + /// Joins relative file paths with `dir`. + pub fn set_directory(&mut self, dir: &Path) { + self.client_secret_file = join_dir(dir, &self.client_secret_file); + self.client_certificate_key_file = join_dir(dir, &self.client_certificate_key_file); + self.tls_config.set_directory(dir); + } +} + +/// Configuration for a single custom HTTP header matching Prometheus `config.Header`. +#[derive(Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct HttpHeader { + /// Plaintext header values. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub values: Vec, + /// Secret header values. + #[serde( + default, + skip_serializing_if = "Vec::is_empty", + serialize_with = "serialize_secret_vec" + )] + pub secrets: Vec, + /// Paths to files containing header values. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub files: Vec, +} + +impl fmt::Debug for HttpHeader { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let redacted_secrets: Vec<&str> = self.secrets.iter().map(|s| redact_str(s)).collect(); + f.debug_struct("HttpHeader") + .field("values", &self.values) + .field("secrets", &redacted_secrets) + .field("files", &self.files) + .finish() + } +} + +impl HttpHeader { + /// Joins relative file paths with `dir`. + pub fn set_directory(&mut self, dir: &Path) { + for file in &mut self.files { + *file = join_dir(dir, file); + } + } +} + +/// Custom HTTP headers configuration matching Prometheus `config.Headers`. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct HttpHeaders { + /// Map of header name to header value sources. + #[serde(flatten)] + pub headers: BTreeMap, +} + +impl HttpHeaders { + /// Joins relative file paths with `dir`. + pub fn set_directory(&mut self, dir: &Path) { + for header in self.headers.values_mut() { + header.set_directory(dir); + } + } + + /// Validates that no reserved Prometheus headers are configured. + pub fn validate(&self) -> Result<(), HttpConfigError> { + for name in self.headers.keys() { + let lower = name.to_ascii_lowercase(); + if RESERVED_HEADERS.contains(&lower.as_str()) { + return Err(HttpConfigError::InvalidConfig(format!( + "setting header {:?} is not allowed", + canonical_header_key(name) + ))); + } + } + Ok(()) + } +} + +/// HTTP client configuration matching Prometheus `config.HTTPClientConfig`. +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(from = "HttpClientConfigWire")] +pub struct HttpClientConfig { + /// HTTP basic authentication credentials. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub basic_auth: Option, + /// HTTP `Authorization` header credentials. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub authorization: Option, + /// OAuth2 client credentials configuration. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub oauth2: Option, + /// Inline bearer token (deprecated in Prometheus in favor of `authorization.credentials`). + #[serde( + default, + skip_serializing_if = "String::is_empty", + serialize_with = "serialize_secret" + )] + pub bearer_token: String, + /// Bearer token file path (deprecated in Prometheus in favor of `authorization.credentials_file`). + #[serde(default, skip_serializing_if = "String::is_empty")] + pub bearer_token_file: String, + /// TLS configuration. + #[serde(default, skip_serializing_if = "is_default_tls")] + pub tls_config: TlsConfig, + /// Specifies whether the client should follow HTTP 3xx redirects (defaults to `true`). + #[serde(default = "default_true")] + pub follow_redirects: bool, + /// Specifies whether HTTP/2 should be enabled (defaults to `true`). + #[serde(default = "default_true")] + pub enable_http2: bool, + /// HTTP proxy configuration. + #[serde(flatten)] + pub proxy_config: ProxyConfig, + /// Custom HTTP headers injected into requests. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub http_headers: Option, +} + +// Flat wire representation without nested `#[serde(flatten)]` so `HttpClientConfig` can be +// flattened inside outer SD/scrape configs while enforcing `deny_unknown_fields` on both. +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct HttpClientConfigWire { + #[serde(default)] + basic_auth: Option, + #[serde(default)] + authorization: Option, + #[serde(default)] + oauth2: Option, + #[serde(default)] + bearer_token: String, + #[serde(default)] + bearer_token_file: String, + #[serde(default)] + tls_config: TlsConfig, + #[serde(default = "default_true")] + follow_redirects: bool, + #[serde(default = "default_true")] + enable_http2: bool, + #[serde(default)] + proxy_url: Option, + #[serde(default)] + no_proxy: String, + #[serde(default)] + proxy_from_environment: bool, + #[serde(default)] + proxy_connect_header: BTreeMap>, + #[serde(default)] + http_headers: Option, +} + +impl From for HttpClientConfig { + fn from(w: HttpClientConfigWire) -> Self { + Self { + basic_auth: w.basic_auth, + authorization: w.authorization, + oauth2: w.oauth2, + bearer_token: w.bearer_token, + bearer_token_file: w.bearer_token_file, + tls_config: w.tls_config, + follow_redirects: w.follow_redirects, + enable_http2: w.enable_http2, + proxy_config: ProxyConfig { + proxy_url: w.proxy_url, + no_proxy: w.no_proxy, + proxy_from_environment: w.proxy_from_environment, + proxy_connect_header: w.proxy_connect_header, + }, + http_headers: w.http_headers, + } + } +} + +impl fmt::Debug for HttpClientConfig { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("HttpClientConfig") + .field("basic_auth", &self.basic_auth) + .field("authorization", &self.authorization) + .field("oauth2", &self.oauth2) + .field("bearer_token", &redact_str(&self.bearer_token)) + .field("bearer_token_file", &self.bearer_token_file) + .field("tls_config", &self.tls_config) + .field("follow_redirects", &self.follow_redirects) + .field("enable_http2", &self.enable_http2) + .field("proxy_config", &self.proxy_config) + .field("http_headers", &self.http_headers) + .finish() + } +} + +impl Default for HttpClientConfig { + fn default() -> Self { + Self { + basic_auth: None, + authorization: None, + oauth2: None, + bearer_token: String::new(), + bearer_token_file: String::new(), + tls_config: TlsConfig::default(), + follow_redirects: true, + enable_http2: true, + proxy_config: ProxyConfig::default(), + http_headers: None, + } + } +} + +impl HttpClientConfig { + /// Returns `true` if this configuration equals Prometheus `DefaultHTTPClientConfig`. + pub fn is_default(&self) -> bool { + *self == Self::default() + } + + /// Joins relative file paths with `dir`. + pub fn set_directory(&mut self, dir: &Path) { + self.tls_config.set_directory(dir); + if let Some(basic_auth) = &mut self.basic_auth { + basic_auth.set_directory(dir); + } + if let Some(auth) = &mut self.authorization { + auth.set_directory(dir); + } + if let Some(oauth2) = &mut self.oauth2 { + oauth2.set_directory(dir); + } + if let Some(headers) = &mut self.http_headers { + headers.set_directory(dir); + } + self.bearer_token_file = join_dir(dir, &self.bearer_token_file); + } + + /// Validates and normalizes this HTTP client configuration according to + /// Prometheus `HTTPClientConfig.Validate`. + pub fn validate(&mut self) -> Result<(), HttpConfigError> { + if !self.bearer_token.is_empty() && !self.bearer_token_file.is_empty() { + return Err(HttpConfigError::InvalidConfig( + "at most one of bearer_token & bearer_token_file must be configured".to_owned(), + )); + } + if (self.basic_auth.is_some() || self.oauth2.is_some()) + && (!self.bearer_token.is_empty() || !self.bearer_token_file.is_empty()) + { + return Err(HttpConfigError::InvalidConfig( + "at most one of basic_auth, oauth2, bearer_token & bearer_token_file must be configured".to_owned(), + )); + } + if let Some(basic_auth) = &self.basic_auth { + if non_empty_count(&[ + &basic_auth.username, + &basic_auth.username_file, + &basic_auth.username_ref, + ]) > 1 + { + return Err(HttpConfigError::InvalidConfig( + "at most one of basic_auth username, username_file & username_ref must be configured".to_owned(), + )); + } + if non_empty_count(&[ + &basic_auth.password, + &basic_auth.password_file, + &basic_auth.password_ref, + ]) > 1 + { + return Err(HttpConfigError::InvalidConfig( + "at most one of basic_auth password, password_file & password_ref must be configured".to_owned(), + )); + } + } + if let Some(auth) = &mut self.authorization { + if !self.bearer_token.is_empty() || !self.bearer_token_file.is_empty() { + return Err(HttpConfigError::InvalidConfig( + "authorization is not compatible with bearer_token & bearer_token_file" + .to_owned(), + )); + } + if non_empty_count(&[ + &auth.credentials, + &auth.credentials_file, + &auth.credentials_ref, + ]) > 1 + { + return Err(HttpConfigError::InvalidConfig( + "at most one of authorization credentials & credentials_file must be configured" + .to_owned(), + )); + } + auth.auth_type = auth.auth_type.trim().to_owned(); + if auth.auth_type.is_empty() { + auth.auth_type = "Bearer".to_owned(); + } + if auth.auth_type.eq_ignore_ascii_case("basic") { + return Err(HttpConfigError::InvalidConfig( + "authorization type cannot be set to \"basic\", use \"basic_auth\" instead" + .to_owned(), + )); + } + if self.basic_auth.is_some() || self.oauth2.is_some() { + return Err(HttpConfigError::InvalidConfig( + "at most one of basic_auth, oauth2 & authorization must be configured" + .to_owned(), + )); + } + } else { + if !self.bearer_token.is_empty() { + self.authorization = Some(Authorization { + auth_type: "Bearer".to_owned(), + credentials: std::mem::take(&mut self.bearer_token), + credentials_file: String::new(), + credentials_ref: String::new(), + }); + } + if !self.bearer_token_file.is_empty() { + self.authorization = Some(Authorization { + auth_type: "Bearer".to_owned(), + credentials: String::new(), + credentials_file: std::mem::take(&mut self.bearer_token_file), + credentials_ref: String::new(), + }); + } + } + + if let Some(oauth2) = &self.oauth2 { + if self.basic_auth.is_some() { + return Err(HttpConfigError::InvalidConfig( + "at most one of basic_auth, oauth2 & authorization must be configured" + .to_owned(), + )); + } + if oauth2.client_id.is_empty() { + return Err(HttpConfigError::InvalidConfig( + "oauth2 client_id must be configured".to_owned(), + )); + } + if oauth2.token_url.is_empty() { + return Err(HttpConfigError::InvalidConfig( + "oauth2 token_url must be configured".to_owned(), + )); + } + if oauth2.grant_type == GRANT_TYPE_JWT_BEARER { + if non_empty_count(&[ + &oauth2.client_certificate_key, + &oauth2.client_certificate_key_file, + &oauth2.client_certificate_key_ref, + ]) > 1 + { + return Err(HttpConfigError::InvalidConfig( + "at most one of oauth2 client_certificate_key, client_certificate_key_file & client_certificate_key_ref must be configured using grant-type=urn:ietf:params:oauth:grant-type:jwt-bearer".to_owned(), + )); + } + if !oauth2.signature_algorithm.is_empty() + && !matches!( + oauth2.signature_algorithm.as_str(), + "RS256" | "RS384" | "RS512" + ) + { + return Err(HttpConfigError::InvalidConfig( + "valid signature algorithms are RS256, RS384 and RS512".to_owned(), + )); + } + } else if non_empty_count(&[ + &oauth2.client_secret, + &oauth2.client_secret_file, + &oauth2.client_secret_ref, + ]) > 1 + { + return Err(HttpConfigError::InvalidConfig( + "at most one of oauth2 client_secret, client_secret_file & client_secret_ref must be configured using grant-type=client_credentials".to_owned(), + )); + } + oauth2.tls_config.validate()?; + oauth2.proxy_config.validate()?; + } + + self.tls_config.validate()?; + self.proxy_config.validate()?; + if let Some(headers) = &self.http_headers { + headers.validate()?; + } + Ok(()) + } + + /// Builds a [`reqwest::Client`] configured with the TLS, HTTP/2, redirect, timeout, and proxy + /// policies from this config. + pub fn build_client(&self) -> Result { + self.check_unsupported_runtime_auth()?; + self.proxy_config.validate()?; + if let Some(headers) = &self.http_headers { + headers.validate()?; + } + + let mut builder = reqwest::Client::builder() + .connect_timeout(DEFAULT_CONNECT_TIMEOUT) + .timeout(DEFAULT_REQUEST_TIMEOUT); + if !self.follow_redirects { + builder = builder.redirect(reqwest::redirect::Policy::none()); + } + if !self.enable_http2 { + builder = builder.http1_only(); + } + builder = self.tls_config.apply_to_builder(builder)?; + + let proxy_headers = build_proxy_header_map(&self.proxy_config.proxy_connect_header)?; + if self.proxy_config.proxy_from_environment { + if !proxy_headers.is_empty() { + return Err(HttpConfigError::InvalidConfig( + "proxy_connect_header with proxy_from_environment is not yet supported" + .to_owned(), + )); + } + } else if let Some(proxy_url) = &self.proxy_config.proxy_url + && !proxy_url.is_empty() + { + let mut proxy = reqwest::Proxy::all(proxy_url)?; + if !self.proxy_config.no_proxy.is_empty() { + let np = reqwest::NoProxy::from_string(&self.proxy_config.no_proxy).ok_or_else( + || { + HttpConfigError::InvalidConfig(format!( + "invalid no_proxy {:?}", + self.proxy_config.no_proxy + )) + }, + )?; + proxy = proxy.no_proxy(Some(np)); + } + if !proxy_headers.is_empty() { + proxy = proxy.headers(proxy_headers); + } + builder = builder.proxy(proxy); + } else { + builder = builder.no_proxy(); + } + Ok(builder.build()?) + } + + /// Applies authentication (`basic_auth`, `authorization`, `bearer_token`, `bearer_token_file`) + /// and custom `http_headers` to an outgoing [`reqwest::RequestBuilder`]. + pub fn apply_to_request( + &self, + mut req: reqwest::RequestBuilder, + ) -> Result { + self.check_unsupported_runtime_auth()?; + if let Some(basic_auth) = &self.basic_auth { + let username = basic_auth.resolve_username()?; + let password = basic_auth.resolve_password()?; + req = req.basic_auth( + username, + if password.is_empty() { + None + } else { + Some(password) + }, + ); + } else if let Some(auth) = &self.authorization { + if let Some(header_val) = auth.resolve_header_value()? { + req = req.header(AUTHORIZATION, sensitive_header_value(&header_val)?); + } + } else if !self.bearer_token.is_empty() { + let val = format!("Bearer {}", self.bearer_token.trim()); + req = req.header(AUTHORIZATION, sensitive_header_value(&val)?); + } else if !self.bearer_token_file.is_empty() { + let token = fs::read_to_string(&self.bearer_token_file)? + .trim() + .to_owned(); + if !token.is_empty() { + let val = format!("Bearer {token}"); + req = req.header(AUTHORIZATION, sensitive_header_value(&val)?); + } + } + + if let Some(headers) = &self.http_headers { + headers.validate()?; + for (name, header) in &headers.headers { + let hdr_name = HeaderName::from_bytes(name.as_bytes()).map_err(|e| { + HttpConfigError::InvalidConfig(format!( + "invalid HTTP header name {name:?}: {e}" + )) + })?; + for v in &header.values { + let hdr_val = HeaderValue::from_str(v).map_err(|e| { + HttpConfigError::InvalidConfig(format!( + "invalid HTTP header value for {name:?}: {e}" + )) + })?; + req = req.header(&hdr_name, hdr_val); + } + for v in &header.secrets { + req = req.header(&hdr_name, sensitive_header_value(v)?); + } + for file in &header.files { + let content = fs::read_to_string(file).map_err(|e| { + HttpConfigError::InvalidConfig(format!( + "unable to read headers file {file}: {e}" + )) + })?; + req = req.header(&hdr_name, sensitive_header_value(content.trim())?); + } + } + } + + Ok(req) + } + + fn check_unsupported_runtime_auth(&self) -> Result<(), HttpConfigError> { + if self.oauth2.is_some() { + return Err(HttpConfigError::InvalidConfig( + "oauth2 authentication is not yet supported at runtime".to_owned(), + )); + } + if let Some(basic_auth) = &self.basic_auth + && (!basic_auth.username_ref.is_empty() || !basic_auth.password_ref.is_empty()) + { + return Err(HttpConfigError::InvalidConfig( + "cannot use secret ref without manager".to_owned(), + )); + } + if let Some(auth) = &self.authorization + && !auth.credentials_ref.is_empty() + { + return Err(HttpConfigError::InvalidConfig( + "cannot use secret ref without manager".to_owned(), + )); + } + Ok(()) + } +} + +fn build_proxy_header_map( + headers: &BTreeMap>, +) -> Result { + let mut map = HeaderMap::new(); + for (name, values) in headers { + let hdr_name = HeaderName::from_bytes(name.as_bytes()).map_err(|e| { + HttpConfigError::InvalidConfig(format!( + "invalid proxy_connect_header name {name:?}: {e}" + )) + })?; + for value in values { + let mut hdr_val = HeaderValue::from_str(value).map_err(|e| { + HttpConfigError::InvalidConfig(format!( + "invalid proxy_connect_header value for {name:?}: {e}" + )) + })?; + hdr_val.set_sensitive(true); + map.append(&hdr_name, hdr_val); + } + } + Ok(map) +} + +fn sensitive_header_value(value: &str) -> Result { + let mut hdr_val = HeaderValue::from_str(value) + .map_err(|e| HttpConfigError::InvalidConfig(format!("invalid HTTP header value: {e}")))?; + hdr_val.set_sensitive(true); + Ok(hdr_val) +} + +fn redact_str(s: &str) -> &str { + if s.is_empty() { "" } else { REDACTED_SECRET } +} + +fn redact_url(raw: &str) -> String { + if let Ok(mut parsed) = reqwest::Url::parse(raw) + && parsed.password().is_some() + { + let _ = parsed.set_password(Some("xxxxx")); + return parsed.into(); + } + raw.to_owned() +} + +fn serialize_secret(value: &str, serializer: S) -> Result { + serializer.serialize_str(redact_str(value)) +} + +fn serialize_secret_vec( + values: &[String], + serializer: S, +) -> Result { + let redacted: Vec<&str> = values.iter().map(|v| redact_str(v)).collect(); + redacted.serialize(serializer) +} + +fn serialize_secret_header_map( + headers: &BTreeMap>, + serializer: S, +) -> Result { + let redacted: BTreeMap<&str, Vec<&str>> = headers + .iter() + .map(|(k, vals)| (k.as_str(), vals.iter().map(|v| redact_str(v)).collect())) + .collect(); + redacted.serialize(serializer) +} + +fn serialize_opt_url( + url: &Option, + serializer: S, +) -> Result { + match url { + Some(u) => serializer.serialize_some(&redact_url(u)), + None => serializer.serialize_none(), + } +} + +fn non_empty_count(values: &[&str]) -> usize { + values.iter().filter(|v| !v.is_empty()).count() +} + +fn is_false(b: &bool) -> bool { + !*b +} + +fn is_default_tls(tls: &TlsConfig) -> bool { + *tls == TlsConfig::default() +} + +fn join_dir(dir: &Path, path: &str) -> String { + if path.is_empty() { + return String::new(); + } + let p = Path::new(path); + if p.is_absolute() { + path.to_owned() + } else { + dir.join(p).to_string_lossy().into_owned() + } +} + +fn canonical_header_key(s: &str) -> String { + let mut out = String::with_capacity(s.len()); + let mut upper = true; + for ch in s.chars() { + if ch == '-' { + out.push('-'); + upper = true; + } else if upper { + out.extend(ch.to_uppercase()); + upper = false; + } else { + out.extend(ch.to_lowercase()); + } + } + out +} diff --git a/src/lib.rs b/src/lib.rs index f28a3ca..371e1a9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,6 +20,8 @@ pub mod discovery; #[cfg(feature = "dto")] pub mod dto; pub mod format; +#[cfg(feature = "http-config")] +pub mod http_config; // Layer 0 parsers remain crate-internal; `arrow` and `dto` expose the public decoding APIs. #[allow(dead_code, unused_imports)] pub(crate) mod parse; diff --git a/tests/http_config.rs b/tests/http_config.rs new file mode 100644 index 0000000..8bcdaf6 --- /dev/null +++ b/tests/http_config.rs @@ -0,0 +1,472 @@ +// Copyright 2026 The Prometheus Authors +// 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. + +//! Integration tests for Prometheus `HttpClientConfig` validation, secret redaction, and `reqwest` client construction. + +#![cfg(feature = "http-config")] + +use std::collections::BTreeMap; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::path::Path; + +use expfmt::http_config::{ + Authorization, BasicAuth, HttpClientConfig, HttpHeader, HttpHeaders, OAuth2, ProxyConfig, + TlsConfig, TlsVersion, +}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::TcpListener; + +#[test] +fn http_client_config_validation_set_directory_and_secret_redaction() { + assert!(HttpClientConfig::default().is_default()); + + // Unknown fields are rejected across ProxyConfig, OAuth2, and HttpClientConfig. + assert!(serde_json::from_str::(r#"{"proxy_urls": "http://x"}"#).is_err()); + assert!(serde_json::from_str::(r#"{"bearer_tokens": "abc"}"#).is_err()); + assert!( + serde_json::from_str::( + r#"{"client_id": "id", "token_url": "https://x", "proxy_url": "http://p:8080"}"# + ) + .is_ok() + ); + assert!( + serde_json::from_str::(r#"{"client_id": "id", "token_urls": "https://x"}"#) + .is_err() + ); + + // bearer_token + bearer_token_file conflict. + let mut both_bearer: HttpClientConfig = + serde_json::from_str(r#"{"bearer_token": "tok", "bearer_token_file": "/path/to/tok"}"#) + .unwrap(); + assert_eq!( + both_bearer.validate().unwrap_err().to_string(), + "at most one of bearer_token & bearer_token_file must be configured" + ); + + // Reserved HTTP header rejection. + let mut reserved_hdr: HttpClientConfig = + serde_json::from_str(r#"{"http_headers": {"Authorization": {"values": ["Bearer foo"]}}}"#) + .unwrap(); + assert_eq!( + reserved_hdr.validate().unwrap_err().to_string(), + "setting header \"Authorization\" is not allowed" + ); + + // Empty authorization.type defaults to "Bearer"; "basic" is rejected. + let mut auth_cfg = HttpClientConfig { + authorization: Some(Authorization { + auth_type: String::new(), + credentials: "my-token".to_owned(), + ..Authorization::default() + }), + ..HttpClientConfig::default() + }; + auth_cfg.validate().unwrap(); + assert_eq!(auth_cfg.authorization.unwrap().auth_type, "Bearer"); + + let mut basic_type = HttpClientConfig { + authorization: Some(Authorization { + auth_type: "Basic".to_owned(), + credentials: "abc".to_owned(), + ..Authorization::default() + }), + ..HttpClientConfig::default() + }; + assert!(basic_type.validate().is_err()); + + // TlsConfig and ProxyConfig negative validation checks. + assert!( + TlsConfig { + min_version: Some(TlsVersion::Tls13), + max_version: Some(TlsVersion::Tls12), + ..TlsConfig::default() + } + .validate() + .is_err() + ); + assert!( + TlsConfig { + cert: "cert".to_owned(), + ..TlsConfig::default() + } + .validate() + .is_err() + ); + assert!( + ProxyConfig { + proxy_url: Some("ftp://127.0.0.1:21".to_owned()), + ..ProxyConfig::default() + } + .validate() + .is_err() + ); + assert!( + ProxyConfig { + no_proxy: "localhost".to_owned(), + ..ProxyConfig::default() + } + .validate() + .is_err() + ); + + // set_directory resolves relative paths and preserves absolute paths across all structs. + let mut dir_cfg = HttpClientConfig { + bearer_token_file: "secrets/token".to_owned(), + basic_auth: Some(BasicAuth { + username_file: "secrets/user".to_owned(), + password_file: "/abs/pass".to_owned(), + ..BasicAuth::default() + }), + authorization: Some(Authorization { + credentials_file: "secrets/creds".to_owned(), + ..Authorization::default() + }), + tls_config: TlsConfig { + ca_file: "certs/ca.pem".to_owned(), + cert_file: "certs/client.crt".to_owned(), + key_file: "/abs/client.key".to_owned(), + ..TlsConfig::default() + }, + oauth2: Some(OAuth2 { + client_secret_file: "oauth/secret".to_owned(), + client_certificate_key_file: "oauth/jwt.key".to_owned(), + tls_config: TlsConfig { + ca_file: "oauth/ca.pem".to_owned(), + ..TlsConfig::default() + }, + ..OAuth2::default() + }), + http_headers: Some(HttpHeaders { + headers: BTreeMap::from([( + "X-Secret".to_owned(), + HttpHeader { + files: vec!["headers/tok.txt".to_owned(), "/abs/tok.txt".to_owned()], + ..HttpHeader::default() + }, + )]), + }), + ..HttpClientConfig::default() + }; + dir_cfg.set_directory(Path::new("/etc/prometheus")); + assert_eq!(dir_cfg.bearer_token_file, "/etc/prometheus/secrets/token"); + let ba = dir_cfg.basic_auth.unwrap(); + assert_eq!(ba.username_file, "/etc/prometheus/secrets/user"); + assert_eq!(ba.password_file, "/abs/pass"); + assert_eq!( + dir_cfg.authorization.unwrap().credentials_file, + "/etc/prometheus/secrets/creds" + ); + assert_eq!(dir_cfg.tls_config.ca_file, "/etc/prometheus/certs/ca.pem"); + assert_eq!( + dir_cfg.tls_config.cert_file, + "/etc/prometheus/certs/client.crt" + ); + assert_eq!(dir_cfg.tls_config.key_file, "/abs/client.key"); + let oa = dir_cfg.oauth2.unwrap(); + assert_eq!(oa.client_secret_file, "/etc/prometheus/oauth/secret"); + assert_eq!( + oa.client_certificate_key_file, + "/etc/prometheus/oauth/jwt.key" + ); + assert_eq!(oa.tls_config.ca_file, "/etc/prometheus/oauth/ca.pem"); + assert_eq!( + dir_cfg.http_headers.unwrap().headers["X-Secret"].files, + vec![ + "/etc/prometheus/headers/tok.txt".to_owned(), + "/abs/tok.txt".to_owned() + ] + ); + + // Secret redaction in Debug and Serialize. + let secret_cfg = HttpClientConfig { + basic_auth: Some(BasicAuth { + username: "alice".to_owned(), + password: "top-secret-password".to_owned(), + ..BasicAuth::default() + }), + bearer_token: "super-secret-bearer".to_owned(), + tls_config: TlsConfig { + key: "super-secret-private-key".to_owned(), + ..TlsConfig::default() + }, + proxy_config: ProxyConfig { + proxy_url: Some("http://proxyuser:proxypass@127.0.0.1:8080".to_owned()), + proxy_connect_header: BTreeMap::from([( + "Proxy-Token".to_owned(), + vec!["proxy-secret-val".to_owned()], + )]), + ..ProxyConfig::default() + }, + http_headers: Some(HttpHeaders { + headers: BTreeMap::from([( + "X-Api-Key".to_owned(), + HttpHeader { + secrets: vec!["header-secret-val".to_owned()], + ..HttpHeader::default() + }, + )]), + }), + ..HttpClientConfig::default() + }; + let dbg = format!("{secret_cfg:?}"); + let json = serde_json::to_string(&secret_cfg).unwrap(); + for leaked in [ + "top-secret-password", + "super-secret-bearer", + "super-secret-private-key", + "proxypass", + "proxy-secret-val", + "header-secret-val", + ] { + assert!(!dbg.contains(leaked), "Debug leaked {leaked}: {dbg}"); + assert!(!json.contains(leaked), "Serialize leaked {leaked}: {json}"); + } + assert!(json.contains("")); +} + +const TEST_CA_CERT_PEM: &str = "-----BEGIN CERTIFICATE-----\n\ +MIIBSzCB8qADAgECAgEBMAoGCCqGSM49BAMCMBwxGjAYBgNVBAMMEVRlc3QgRGlz\n\ +Y292ZXJ5IENBMCAXDTIwMDEwMTAwMDAwMFoYDzIxMjAwMTAxMDAwMDAwWjAcMRow\n\ +GAYDVQQDDBFUZXN0IERpc2NvdmVyeSBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEH\n\ +A0IABIh8inO1e0XFBqDBtcxv3BmM0MHgV2PZeF+BswiHPsQ63K6Xt/xGZS9ziRh8\n\ +bErqSNtv7W9shFuHxVmIuf+ROo+jIzAhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0P\n\ +AQH/BAQDAgGGMAoGCCqGSM49BAMCA0gAMEUCIQCXh/u32CC/j81T+3oEJnRR0P8A\n\ +ZCXcq24++2iU8RQHuAIgTvD813EN4j4Wd++SFXpG+evSLLk+fyJse98OIktvI/E=\n\ +-----END CERTIFICATE-----\n"; + +const TEST_SERVER_CERT_PEM: &str = "-----BEGIN CERTIFICATE-----\n\ +MIIBVzCB/aADAgECAgECMAoGCCqGSM49BAMCMBwxGjAYBgNVBAMMEVRlc3QgRGlz\n\ +Y292ZXJ5IENBMCAXDTIwMDEwMTAwMDAwMFoYDzIxMjAwMTAxMDAwMDAwWjAUMRIw\n\ +EAYDVQQDDAkxMjcuMC4wLjEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASgpvT7\n\ +pxvHJ9b0hfVl1OSzVw4wXXNbMgOcQuzTJyWIdPLOgN4uV3uYNUCGBoOmzZiSHQNV\n\ +kxosSi48IIZKN66CozYwNDAMBgNVHRMBAf8EAjAAMA8GA1UdEQQIMAaHBH8AAAEw\n\ +EwYDVR0lBAwwCgYIKwYBBQUHAwEwCgYIKoZIzj0EAwIDSQAwRgIhAL8MTVC7DzUW\n\ +S6c9vBHZmQpTHRttDCHL5pJuJjYfD6UIAiEA4Svgdadp5oYsNbiccD3xiOxd2f0C\n\ +gcCEe0UeZFy+qgs=\n\ +-----END CERTIFICATE-----\n"; + +const TEST_SERVER_KEY_PEM: &str = "-----BEGIN PRIVATE KEY-----\n\ +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQglO/xnuT6hNqAa1Au\n\ +8mCUOEYEuik/fdi8Fp1fJTvPI/mhRANCAASgpvT7pxvHJ9b0hfVl1OSzVw4wXXNb\n\ +MgOcQuzTJyWIdPLOgN4uV3uYNUCGBoOmzZiSHQNVkxosSi48IIZKN66C\n\ +-----END PRIVATE KEY-----\n"; + +const TEST_CLIENT_CERT_PEM: &str = "-----BEGIN CERTIFICATE-----\n\ +MIIBRzCB7qADAgECAgEDMAoGCCqGSM49BAMCMBwxGjAYBgNVBAMMEVRlc3QgRGlz\n\ +Y292ZXJ5IENBMCAXDTIwMDEwMTAwMDAwMFoYDzIxMjAwMTAxMDAwMDAwWjAWMRQw\n\ +EgYDVQQDDAtwcm9tLWNsaWVudDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABPaV\n\ +o6u67B6wO8kr/rbYTvsvDXIxURJaLinfvVeqsPW3SDYaUTM9f8tSa7xbvmBwLbmr\n\ +y1s7cN7v0Pfo5lYFU5CjJTAjMAwGA1UdEwEB/wQCMAAwEwYDVR0lBAwwCgYIKwYB\n\ +BQUHAwIwCgYIKoZIzj0EAwIDSAAwRQIgNdIcz83GyYDyCadBNBtP+jxPeCBBpQY4\n\ +c/Mzu1lyEr8CIQDZMeAQYUPozmcVavNzujS3wddD9dNSIpZBGsOd9p4Kdg==\n\ +-----END CERTIFICATE-----\n"; + +const TEST_CLIENT_KEY_PEM: &str = "-----BEGIN PRIVATE KEY-----\n\ +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgOcr6T6T9duo1QH4u\n\ +nR6BDYL+R0s2UDkIdWN9ry1Z60ihRANCAAT2laOruuwesDvJK/622E77Lw1yMVES\n\ +Wi4p371XqrD1t0g2GlEzPX/LUmu8W75gcC25q8tbO3De79D36OZWBVOQ\n\ +-----END PRIVATE KEY-----\n"; + +#[test] +fn build_client_rejects_unsupported_runtime_options() { + let mut oauth_http = HttpClientConfig { + oauth2: Some(OAuth2 { + client_id: "my-client".to_owned(), + token_url: "https://auth.example.com/token".to_owned(), + ..OAuth2::default() + }), + ..HttpClientConfig::default() + }; + oauth_http.validate().unwrap(); + assert!( + oauth_http + .build_client() + .unwrap_err() + .to_string() + .contains("oauth2") + ); + + let ref_http = HttpClientConfig { + basic_auth: Some(BasicAuth { + username_ref: "secret-ref".to_owned(), + ..BasicAuth::default() + }), + ..HttpClientConfig::default() + }; + assert!(ref_http.build_client().is_err()); + + let tls_ref_http = HttpClientConfig { + tls_config: TlsConfig { + ca_ref: "ca-secret".to_owned(), + ..TlsConfig::default() + }, + ..HttpClientConfig::default() + }; + assert!(tls_ref_http.build_client().is_err()); + + let sni_http = HttpClientConfig { + tls_config: TlsConfig { + server_name: "custom.sni.local".to_owned(), + ..TlsConfig::default() + }, + ..HttpClientConfig::default() + }; + assert!(sni_http.build_client().is_err()); + + let env_proxy_hdr = HttpClientConfig { + proxy_config: ProxyConfig { + proxy_from_environment: true, + proxy_connect_header: BTreeMap::from([( + "Proxy-Token".to_owned(), + vec!["secret".to_owned()], + )]), + ..ProxyConfig::default() + }, + ..HttpClientConfig::default() + }; + assert!(env_proxy_hdr.build_client().is_err()); +} + +#[test] +fn build_client_and_apply_to_request_https_mtls_and_localset() { + use rustls::pki_types::pem::PemObject; + use rustls::pki_types::{CertificateDer, PrivateKeyDer}; + + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let local = tokio::task::LocalSet::new(); + + local.block_on(&rt, async { + let dir = + std::env::temp_dir().join(format!("scrape-rs-http-cfg-test-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let user_file = dir.join("user.txt"); + let pass_file = dir.join("pass.txt"); + let hdr_file = dir.join("hdr.txt"); + std::fs::write(&user_file, "prom-user\n").unwrap(); + std::fs::write(&pass_file, "prom-pass\n").unwrap(); + std::fs::write(&hdr_file, "from-file\n").unwrap(); + + let mut client_roots = rustls::RootCertStore::empty(); + client_roots + .add(CertificateDer::from_pem_slice(TEST_CA_CERT_PEM.as_bytes()).unwrap()) + .unwrap(); + let verifier = rustls::server::WebPkiClientVerifier::builder(std::sync::Arc::new( + client_roots, + )) + .build() + .unwrap(); + let server_cert = CertificateDer::from_pem_slice(TEST_SERVER_CERT_PEM.as_bytes()).unwrap(); + let server_key = PrivateKeyDer::from_pem_slice(TEST_SERVER_KEY_PEM.as_bytes()).unwrap(); + let tls_cfg = rustls::ServerConfig::builder() + .with_client_cert_verifier(verifier) + .with_single_cert(vec![server_cert], server_key) + .unwrap(); + let acceptor = tokio_rustls::TlsAcceptor::from(std::sync::Arc::new(tls_cfg)); + + let listener = TcpListener::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)) + .await + .unwrap(); + let addr = listener.local_addr().unwrap(); + + tokio::task::spawn_local(async move { + let Ok((tcp, _)) = listener.accept().await else { + return; + }; + let Ok(mut tls_stream) = acceptor.accept(tcp).await else { + return; + }; + let mut req_buf = vec![0u8; 4096]; + let n = tls_stream.read(&mut req_buf).await.unwrap_or(0); + let req_str = String::from_utf8_lossy(&req_buf[..n]); + assert!( + req_str.lines().any(|l| l.eq_ignore_ascii_case( + "authorization: Basic cHJvbS11c2VyOnByb20tcGFzcw==" + )), + "missing basic auth in:\n{req_str}" + ); + assert!( + req_str + .lines() + .any(|l| l.eq_ignore_ascii_case("x-trace: plain-val")), + "missing plain header in:\n{req_str}" + ); + assert!( + req_str + .lines() + .any(|l| l.eq_ignore_ascii_case("x-trace: secret-val")), + "missing secret header in:\n{req_str}" + ); + assert!( + req_str + .lines() + .any(|l| l.eq_ignore_ascii_case("x-trace: from-file")), + "missing file header in:\n{req_str}" + ); + + let body = "ok"; + let resp = format!( + "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", + body.len(), + body + ); + let _ = tls_stream.write_all(resp.as_bytes()).await; + let _ = tls_stream.shutdown().await; + }); + + let mut cfg = HttpClientConfig { + basic_auth: Some(BasicAuth { + username_file: user_file.to_string_lossy().into_owned(), + password_file: pass_file.to_string_lossy().into_owned(), + ..BasicAuth::default() + }), + enable_http2: false, + follow_redirects: false, + tls_config: TlsConfig { + ca: TEST_CA_CERT_PEM.to_owned(), + cert: TEST_CLIENT_CERT_PEM.to_owned(), + key: TEST_CLIENT_KEY_PEM.to_owned(), + min_version: Some(TlsVersion::Tls12), + max_version: Some(TlsVersion::Tls13), + ..TlsConfig::default() + }, + http_headers: Some(HttpHeaders { + headers: BTreeMap::from([( + "X-Trace".to_owned(), + HttpHeader { + values: vec!["plain-val".to_owned()], + secrets: vec!["secret-val".to_owned()], + files: vec![hdr_file.to_string_lossy().into_owned()], + }, + )]), + }), + ..HttpClientConfig::default() + }; + cfg.validate().unwrap(); + let client = cfg.build_client().unwrap(); + + let body = tokio::task::spawn_local(async move { + let req = cfg + .apply_to_request(client.get(format!("https://{addr}/metrics"))) + .unwrap(); + req.send().await.unwrap().text().await.unwrap() + }) + .await + .unwrap(); + assert_eq!(body, "ok"); + + let _ = std::fs::remove_dir_all(&dir); + }); +}