diff --git a/Cargo.lock b/Cargo.lock index fff06dbaa3..e52290ae5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,41 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array 0.14.7", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.8.11" @@ -141,6 +176,18 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" +[[package]] +name = "as-slice" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45403b49e3954a4b8428a0ac21a4b7afadccf92bfd96273f1a58cd4812496ae0" +dependencies = [ + "generic-array 0.12.4", + "generic-array 0.13.3", + "generic-array 0.14.7", + "stable_deref_trait", +] + [[package]] name = "assert-json-diff" version = "2.0.2" @@ -339,6 +386,15 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -357,7 +413,7 @@ version = "0.13.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8bce4948d2520386c6d92a6ea2d472300257702242e5a1d01d6add52bd2e7c1" dependencies = [ - "bindgen", + "bindgen 0.72.1", "cc", "cmake", "dunce", @@ -446,6 +502,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.21.7" @@ -488,13 +550,36 @@ dependencies = [ "serde", ] +[[package]] +name = "bindgen" +version = "0.65.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" +dependencies = [ + "bitflags 1.3.2", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.87", + "which", +] + [[package]] name = "bindgen" version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags", + "bitflags 2.6.0", "cexpr", "clang-sys", "itertools", @@ -503,7 +588,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 2.1.1", "shlex", "syn 2.0.87", ] @@ -514,15 +599,27 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ - "bit-vec", + "bit-vec 0.6.3", ] +[[package]] +name = "bit-vec" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f59bbe95d4e52a6398ec21238d31577f2b28a9d86807f06ca59d191d8440d0bb" + [[package]] name = "bit-vec" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.6.0" @@ -569,7 +666,7 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -693,6 +790,16 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" +[[package]] +name = "buffered-io" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5698b2eda4613b62f3aa3119805df1ca6739e00167a2600b3a234ac49b14803" +dependencies = [ + "embedded-io", + "embedded-io-async", +] + [[package]] name = "build_common" version = "35.0.0" @@ -893,6 +1000,16 @@ dependencies = [ "half", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -1039,6 +1156,12 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "const_format" version = "0.2.34" @@ -1209,13 +1332,25 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array 0.14.7", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ - "generic-array", + "generic-array 0.14.7", "typenum", ] @@ -1240,6 +1375,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "current_platform" version = "0.2.0" @@ -1594,6 +1738,16 @@ dependencies = [ "uuid", ] +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + [[package]] name = "deranged" version = "0.4.0" @@ -1650,6 +1804,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -1658,7 +1813,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags", + "bitflags 2.6.0", "objc2", ] @@ -1712,6 +1867,81 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array 0.14.7", + "group", + "hkdf", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "embedded-io-async" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff09972d4073aa8c299395be75161d582e7629cd663171d62af73c8d50dba3f" +dependencies = [ + "embedded-io", +] + +[[package]] +name = "embedded-nal" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56a28be191a992f28f178ec338a0bf02f63d7803244add736d026a471e6ed77" +dependencies = [ + "nb", +] + +[[package]] +name = "embedded-nal-async" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76959917cd2b86f40a98c28dd5624eddd1fa69d746241c8257eac428d83cb211" +dependencies = [ + "embedded-io-async", + "embedded-nal", +] + +[[package]] +name = "embedded-tls" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6efb76fdd004a4ef787640177237b83449e6c5847765ea50bf15900061fd601" +dependencies = [ + "aes-gcm", + "atomic-polyfill", + "digest", + "embedded-io", + "embedded-io-async", + "generic-array 0.14.7", + "heapless 0.6.1", + "heapless 0.8.0", + "hkdf", + "hmac", + "p256", + "rand_core 0.6.4", + "sha2", + "typenum", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -1823,6 +2053,16 @@ dependencies = [ "simdutf8", ] +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "filetime" version = "0.2.25" @@ -2034,6 +2274,24 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f797e67af32588215eaaab8327027ee8e71b9dd0b2b26996aedf20c030fce309" +dependencies = [ + "typenum", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -2042,6 +2300,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -2069,6 +2328,16 @@ dependencies = [ "wasi 0.14.2+wasi-0.2.4", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gimli" version = "0.31.1" @@ -2124,6 +2393,17 @@ dependencies = [ "scroll", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "h2" version = "0.4.6" @@ -2163,6 +2443,24 @@ dependencies = [ "serde", ] +[[package]] +name = "hash32" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4041af86e63ac4298ce40e5cca669066e75b6f1aa3390fe2561ffa5e1d9f4cc" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -2238,6 +2536,28 @@ dependencies = [ "http", ] +[[package]] +name = "heapless" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634bd4d29cbf24424d0a4bfcbf80c6960129dc24424752a7d1d1390607023422" +dependencies = [ + "as-slice", + "generic-array 0.14.7", + "hash32 0.1.1", + "stable_deref_trait", +] + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32 0.3.1", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.4.1" @@ -2314,6 +2634,33 @@ dependencies = [ "tracing", ] +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "http" version = "1.1.0" @@ -2669,6 +3016,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array 0.14.7", +] + [[package]] name = "io-lifetimes" version = "1.0.11" @@ -2806,6 +3162,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.178" @@ -3196,7 +3558,7 @@ dependencies = [ "prost", "rand 0.8.5", "reqwest", - "rustc-hash", + "rustc-hash 2.1.1", "rustls", "rustls-platform-verifier", "serde", @@ -3314,6 +3676,21 @@ dependencies = [ "tracing", ] +[[package]] +name = "libdd-signal-safe-http-client" +version = "35.0.0" +dependencies = [ + "embedded-io", + "embedded-io-async", + "embedded-nal-async", + "low_dns", + "mbedtls", + "origin", + "reqwless", + "rustix 1.1.3", + "sha2", +] + [[package]] name = "libdd-telemetry" version = "5.0.1" @@ -3516,7 +3893,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags", + "bitflags 2.6.0", "libc", "redox_syscall", ] @@ -3545,6 +3922,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -3577,6 +3960,12 @@ dependencies = [ "value-bag", ] +[[package]] +name = "low_dns" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36a7412cdd4748b235325047350779e03f6d3d2be7bae43e9666fb54d928a5e3" + [[package]] name = "lru" version = "0.16.4" @@ -3616,6 +4005,54 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "mbedtls" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96ab34e3a3b8eb0ed4dc157a65c001f5a9597e2a9aa580b7cab55c40cbdb375a" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "cc", + "cfg-if", + "mbedtls-platform-support", + "mbedtls-sys-auto", + "rs-libc", + "rustc_version", + "serde", + "serde_derive", + "yasna", +] + +[[package]] +name = "mbedtls-platform-support" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "844620df168baa0a4fd1ddde9805080fc3eed92c118707e1e3129598db260c94" +dependencies = [ + "cc", + "cfg-if", + "chrono", + "mbedtls-sys-auto", + "spin 0.5.2", +] + +[[package]] +name = "mbedtls-sys-auto" +version = "2.28.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67535817b18e8582abc2456a7c957f26d5a88ed59fb2fcf93e62da8c79387ada" +dependencies = [ + "bindgen 0.65.1", + "cc", + "cfg-if", + "cmake", + "lazy_static", + "libc", + "quote", + "syn 1.0.109", +] + [[package]] name = "md5" version = "0.7.0" @@ -3752,7 +4189,7 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4c25a3bb7d880e8eceab4822f3141ad0700d20f025991c1f03bd3d00219a5fc" dependencies = [ - "bitflags", + "bitflags 2.6.0", ] [[package]] @@ -3768,7 +4205,7 @@ dependencies = [ "httparse", "memchr", "mime", - "spin", + "spin 0.9.8", "version_check", ] @@ -3778,13 +4215,19 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +[[package]] +name = "nb" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d" + [[package]] name = "nix" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags", + "bitflags 2.6.0", "cfg-if", "cfg_aliases", "libc", @@ -3797,7 +4240,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags", + "bitflags 2.6.0", "cfg-if", "cfg_aliases", "libc", @@ -3813,6 +4256,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nourl" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa07b0722c63805057dec824444fdc814bdfd30d1c782a3a8f63bbcf67c4ed1c" + [[package]] name = "ntapi" version = "0.4.1" @@ -3831,6 +4280,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -3848,6 +4308,15 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -3872,7 +4341,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ - "bitflags", + "bitflags 2.6.0", "objc2", "objc2-foundation", ] @@ -3893,7 +4362,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags", + "bitflags 2.6.0", "dispatch2", "objc2", ] @@ -3904,7 +4373,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags", + "bitflags 2.6.0", "dispatch2", "objc2", "objc2-core-foundation", @@ -3937,7 +4406,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ - "bitflags", + "bitflags 2.6.0", "objc2", "objc2-core-foundation", "objc2-core-graphics", @@ -3955,7 +4424,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags", + "bitflags 2.6.0", "block2", "libc", "objc2", @@ -3968,7 +4437,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags", + "bitflags 2.6.0", "objc2", "objc2-core-foundation", ] @@ -3979,7 +4448,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags", + "bitflags 2.6.0", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -3991,7 +4460,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags", + "bitflags 2.6.0", "block2", "objc2", "objc2-cloud-kit", @@ -4052,12 +4521,29 @@ version = "11.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openssl-probe" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "origin" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692d98a33ca8355280e87c3dea4f62e3361c0fb64a76557d8d3988f070cd3c30" +dependencies = [ + "bitflags 2.6.0", + "linux-raw-sys 0.9.4", + "rustix 1.1.3", +] + [[package]] name = "os_info" version = "3.14.0" @@ -4074,6 +4560,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "elliptic-curve", + "primeorder", +] + [[package]] name = "page_size" version = "0.6.0" @@ -4128,6 +4624,12 @@ dependencies = [ "smallvec", ] +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -4249,6 +4751,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "portable-atomic" version = "1.9.0" @@ -4309,6 +4823,15 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "priority-queue" version = "2.1.1" @@ -4380,8 +4903,8 @@ version = "1.5.0" source = "git+https://github.com/bantonsson/proptest.git?branch=ban%2Favoid-libm-in-std#9f623fbab7a1a4da487551128c2bffeee2ed6b87" dependencies = [ "bit-set", - "bit-vec", - "bitflags", + "bit-vec 0.6.3", + "bitflags 2.6.0", "lazy_static", "num-traits", "rand 0.8.5", @@ -4693,7 +5216,7 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ - "bitflags", + "bitflags 2.6.0", ] [[package]] @@ -4789,6 +5312,26 @@ dependencies = [ "web-sys", ] +[[package]] +name = "reqwless" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb1be74cb817fa6dbda417110f575d9b9ad5488817f1eb65f2f6468fe6d5d663" +dependencies = [ + "base64 0.21.7", + "buffered-io", + "embedded-io", + "embedded-io-async", + "embedded-nal-async", + "embedded-tls", + "heapless 0.8.0", + "hex", + "httparse", + "nourl", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + [[package]] name = "resolv-conf" version = "0.7.6" @@ -4850,12 +5393,28 @@ dependencies = [ "rmp", ] +[[package]] +name = "rs-libc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ebd46ef448b3591b52a40970b56e0ab830a408c18a226181a44e1fa64fabcc1" +dependencies = [ + "cc", + "zeroize", +] + [[package]] name = "rustc-demangle" version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.1" @@ -4877,7 +5436,7 @@ version = "0.38.39" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "375116bee2be9ed569afe2154ea6a99dfdffd257f533f187498c2a8f5feaf4ee" dependencies = [ - "bitflags", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys 0.4.14", @@ -4890,7 +5449,7 @@ version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ - "bitflags", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys 0.11.0", @@ -5096,13 +5655,26 @@ version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array 0.14.7", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ - "bitflags", + "bitflags 2.6.0", "core-foundation", "core-foundation-sys", "libc", @@ -5445,6 +6017,12 @@ dependencies = [ "windows 0.51.1", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "spin" version = "0.9.8" @@ -6086,7 +6664,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags", + "bitflags 2.6.0", "bytes", "futures-util", "http", @@ -6254,6 +6832,16 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -6513,6 +7101,18 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.39", +] + [[package]] name = "widestring" version = "1.2.1" @@ -7023,7 +7623,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags", + "bitflags 2.6.0", ] [[package]] @@ -7055,6 +7655,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +[[package]] +name = "yasna" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79af3189e6b0484c9fd54208f8eeb8818cadee00ec81438b67a64c8e6f2f3694" +dependencies = [ + "bit-vec 0.5.1", + "num-bigint", +] + [[package]] name = "yoke" version = "0.7.4" diff --git a/Cargo.toml b/Cargo.toml index dc3e374492..98506468ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,7 @@ members = [ "libdd-tinybytes", "libdd-dogstatsd-client", "libdd-http-client", + "libdd-signal-safe-http-client", "libdd-agent-client", "libdd-log", "libdd-log-ffi", diff --git a/libdd-signal-safe-http-client/Cargo.toml b/libdd-signal-safe-http-client/Cargo.toml new file mode 100644 index 0000000000..7165d942a9 --- /dev/null +++ b/libdd-signal-safe-http-client/Cargo.toml @@ -0,0 +1,39 @@ +# Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "libdd-signal-safe-http-client" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +description = "no_std HTTP client primitives for preallocated transports" +homepage = "https://github.com/DataDog/libdatadog/tree/main/libdd-signal-safe-http-client" +repository = "https://github.com/DataDog/libdatadog/tree/main/libdd-signal-safe-http-client" + +[lib] +bench = false + +[lints] +workspace = true + +[features] +default = [] +alloc = ["reqwless/alloc"] +std = ["alloc", "embedded-io/std", "embedded-io-async/std", "mbedtls?/std"] +mbedtls = ["dep:mbedtls", "mbedtls/no_std_deps", "mbedtls/ssl", "mbedtls/x509"] +esp-mbedtls = [] + +[dependencies] +embedded-io = { version = "0.6", default-features = false } +embedded-io-async = { version = "0.6", default-features = false } +embedded-nal-async = { version = "0.8.0", default-features = false } +mbedtls = { version = "0.13.5", default-features = false, optional = true } +reqwless = { version = "0.13.0", default-features = false } + +[target.'cfg(target_os = "linux")'.dev-dependencies] +low_dns = { version = "0.3.0", default-features = false } +origin = { version = "0.26.2", default-features = false, features = ["origin-start"] } +rustix = { version = "1.1.3", default-features = false, features = ["event", "fs", "net", "stdio"] } +sha2 = { version = "0.10", default-features = false } diff --git a/libdd-signal-safe-http-client/examples/README.md b/libdd-signal-safe-http-client/examples/README.md new file mode 100644 index 0000000000..6ca3d74627 --- /dev/null +++ b/libdd-signal-safe-http-client/examples/README.md @@ -0,0 +1,47 @@ + + + +# Examples + +Build the Linux `origin` + `rustix` HTTP-only example for a linux-none target +without enabling this crate's `alloc`, `std`, TLS, or mbedtls features. The +example streams a fixed Alpine ISO over HTTP into SHA-256 and checks it against a +hardcoded digest. Name resolution checks `/etc/hosts` first, then uses a +`low_dns` A-record resolver configured from `/etc/resolv.conf`, including +`search`/`domain`, `options ndots:n`, CNAME chasing, and TCP fallback for +truncated UDP responses. + +```bash +RUSTFLAGS="-C target-feature=+crt-static -C relocation-model=static" \ + cargo +nightly-2026-02-08 build \ + -p libdd-signal-safe-http-client \ + --example http_only_no_std \ + --no-default-features \ + --target x86_64-unknown-linux-none \ + -Zbuild-std=core,compiler_builtins \ + -Zbuild-std-features=compiler-builtins-mem +``` + +The static relocation model avoids requiring Origin's experimental PIE +relocation path for a fully static executable. + +```bash +docker run --rm --platform linux/amd64 \ + -v "$PWD:/work" \ + -w /work \ + debian:bookworm-slim \ + /work/target/x86_64-unknown-linux-none/debug/examples/http_only_no_std +``` + +Build and run a scratch image: + +```bash +printf 'FROM scratch\nCOPY http_only_no_std /http_only_no_std\nENTRYPOINT ["/http_only_no_std"]\n' \ + | docker build --platform linux/amd64 \ + -t libdd-signal-safe-http-client:http-only-no-std-scratch \ + -f - \ + target/x86_64-unknown-linux-none/debug/examples + +docker run --rm --platform linux/amd64 \ + libdd-signal-safe-http-client:http-only-no-std-scratch +``` diff --git a/libdd-signal-safe-http-client/examples/http_only_no_std.rs b/libdd-signal-safe-http-client/examples/http_only_no_std.rs new file mode 100644 index 0000000000..09bd4e6cac --- /dev/null +++ b/libdd-signal-safe-http-client/examples/http_only_no_std.rs @@ -0,0 +1,30 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +#![cfg_attr(target_os = "linux", no_std)] +#![cfg_attr(target_os = "linux", no_main)] + +#[cfg(not(target_os = "linux"))] +fn main() {} + +#[cfg(target_os = "linux")] +#[path = "http_only_no_std/support.rs"] +mod support; + +#[cfg(target_os = "linux")] +#[unsafe(no_mangle)] +/// Origin calls this after taking over Linux process startup. +/// +/// # Safety +/// +/// `argc`, `argv`, and `envp` must be the initial process arguments provided by +/// the Linux program loader. +unsafe extern "C" fn origin_main(_argc: usize, _argv: *mut *mut u8, _envp: *mut *mut u8) -> i32 { + origin::program::immediate_exit(support::run()) +} + +#[cfg(target_os = "linux")] +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo<'_>) -> ! { + origin::program::trap() +} diff --git a/libdd-signal-safe-http-client/examples/http_only_no_std/dns.rs b/libdd-signal-safe-http-client/examples/http_only_no_std/dns.rs new file mode 100644 index 0000000000..ef5040eb06 --- /dev/null +++ b/libdd-signal-safe-http-client/examples/http_only_no_std/dns.rs @@ -0,0 +1,773 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use core::{ + net::{IpAddr, Ipv4Addr, SocketAddr}, + str, + sync::atomic::{AtomicU16, Ordering}, +}; + +use libdd_signal_safe_http_client::io::{ + embedded_io::ErrorKind, + embedded_nal_async::{AddrType, Dns}, +}; +use low_dns::{DnsQuestion, Header, HeaderKind, Name, Packet, Rdata, ResponseCode}; +use rustix::{ + event::{self, PollFd, PollFlags, Timespec}, + fd::OwnedFd, + fs::{self, Mode, OFlags}, + io, + net::{self, AddressFamily, RecvFlags, SendFlags, SocketType}, +}; + +const MAX_NAME_SERVERS: usize = 3; +const MAX_SEARCH_DOMAINS: usize = 6; +const MAX_CNAME_DEPTH: usize = 8; +const RESOLV_CONF_PATH: &str = "/etc/resolv.conf"; +const HOSTS_PATH: &str = "/etc/hosts"; +const RESOLV_CONF_BUFFER_LEN: usize = 2048; +const HOSTS_BUFFER_LEN: usize = 8192; +const DNS_PORT: u16 = 53; +const DEFAULT_NDOTS: u8 = 1; +const DNS_TIMEOUT: Timespec = Timespec { + tv_sec: 2, + tv_nsec: 0, +}; + +pub(super) struct RustixDnsResolver { + name_servers: [Option; MAX_NAME_SERVERS], + search_domains: [[u8; low_dns::name::MAX_NAME_LENGTH]; MAX_SEARCH_DOMAINS], + search_domain_lens: [usize; MAX_SEARCH_DOMAINS], + ndots: u8, + next_id: AtomicU16, +} + +impl RustixDnsResolver { + pub(super) fn from_resolv_conf() -> Self { + let mut resolver = Self { + name_servers: [None; MAX_NAME_SERVERS], + search_domains: [[0; low_dns::name::MAX_NAME_LENGTH]; MAX_SEARCH_DOMAINS], + search_domain_lens: [0; MAX_SEARCH_DOMAINS], + ndots: DEFAULT_NDOTS, + next_id: AtomicU16::new(0x4400), + }; + + let mut buffer = [0_u8; RESOLV_CONF_BUFFER_LEN]; + if let Ok(len) = read_file(RESOLV_CONF_PATH, &mut buffer) { + parse_resolv_conf(&buffer[..len], &mut resolver); + } + + resolver + } + + pub(super) fn name_server_count(&self) -> usize { + self.name_servers + .iter() + .filter(|name_server| name_server.is_some()) + .count() + } + + pub(super) fn search_domain_count(&self) -> usize { + self.search_domain_lens + .iter() + .filter(|len| **len != 0) + .count() + } + + pub(super) const fn ndots(&self) -> u8 { + self.ndots + } + + fn push_name_server(&mut self, addr: SocketAddr) { + for slot in &mut self.name_servers { + if slot.is_none() { + *slot = Some(addr); + return; + } + } + } + + const fn clear_search_domains(&mut self) { + self.search_domain_lens = [0; MAX_SEARCH_DOMAINS]; + } + + fn push_search_domain(&mut self, domain: &[u8]) { + let domain = strip_trailing_dot_bytes(domain); + if !valid_host_bytes(domain) { + return; + } + + for (slot, len) in self + .search_domains + .iter_mut() + .zip(self.search_domain_lens.iter_mut()) + { + if *len == 0 { + slot[..domain.len()].copy_from_slice(domain); + *len = domain.len(); + return; + } + } + } + + fn resolve_ipv4(&self, host: &str) -> Result { + validate_host(host)?; + + let mut hosts_buffer = [0_u8; HOSTS_BUFFER_LEN]; + let hosts_len = read_file(HOSTS_PATH, &mut hosts_buffer).unwrap_or_default(); + let hosts = &hosts_buffer[..hosts_len]; + let mut candidate_buffer = [0_u8; low_dns::name::MAX_NAME_LENGTH]; + let absolute_first = count_dots(host.as_bytes()) >= self.ndots || has_trailing_dot(host); + + if absolute_first { + if let Ok(ip) = self.resolve_candidate(strip_trailing_dot(host), hosts) { + return Ok(ip); + } + } + + if !has_trailing_dot(host) { + for domain in self.search_domains() { + let Some(candidate) = append_search_domain(host, domain, &mut candidate_buffer) + else { + continue; + }; + if let Ok(ip) = self.resolve_candidate(candidate, hosts) { + return Ok(ip); + } + } + } + + if !absolute_first { + return self.resolve_candidate(host, hosts); + } + + Err(ErrorKind::AddrNotAvailable) + } + + fn resolve_candidate(&self, host: &str, hosts: &[u8]) -> Result { + let mut current_buffer = [0_u8; low_dns::name::MAX_NAME_LENGTH]; + let mut cname_buffer = [0_u8; low_dns::name::MAX_NAME_LENGTH]; + let mut current_len = copy_host(host, &mut current_buffer)?; + + for _ in 0..MAX_CNAME_DEPTH { + let current = str::from_utf8(¤t_buffer[..current_len]) + .map_err(|_| ErrorKind::InvalidInput)?; + if let Some(ip) = resolve_hosts_ipv4(hosts, current) { + return Ok(ip); + } + + let mut saw_cname = false; + + for name_server in self.name_servers.iter().flatten() { + match self.query_a_or_cname(*name_server, current, &mut cname_buffer) { + Ok(QueryAnswer::A(ip)) => return Ok(ip), + Ok(QueryAnswer::Cname(len)) => { + current_buffer[..len].copy_from_slice(&cname_buffer[..len]); + current_len = len; + saw_cname = true; + break; + } + Err(_) => {} + } + } + + if !saw_cname { + return Err(ErrorKind::AddrNotAvailable); + } + } + + Err(ErrorKind::InvalidData) + } + + const fn search_domains(&self) -> SearchDomains<'_> { + SearchDomains { + domains: &self.search_domains, + lens: &self.search_domain_lens, + next: 0, + } + } + + fn query_a_or_cname( + &self, + name_server: SocketAddr, + host: &str, + cname_buffer: &mut [u8; low_dns::name::MAX_NAME_LENGTH], + ) -> Result { + let id = self.next_query_id(); + let mut query_name_buffer = Name::max_buffer(); + let mut query_packet_buffer = Packet::max_buffer(); + let query = build_a_query(id, host, &mut query_name_buffer, &mut query_packet_buffer); + + let fd = net::socket( + AddressFamily::INET, + SocketType::DGRAM, + Some(net::ipproto::UDP), + ) + .map_err(map_errno)?; + + net::sendto(&fd, query.as_bytes(), SendFlags::empty(), &name_server).map_err(map_errno)?; + wait_readable(&fd)?; + + let mut response_buffer = Packet::max_buffer(); + let (read, _, _) = + net::recvfrom(&fd, &mut response_buffer, RecvFlags::empty()).map_err(map_errno)?; + let response = + Packet::parse(&response_buffer[..read]).map_err(|_| ErrorKind::InvalidData)?; + if response.header().truncated() { + return Self::query_a_or_cname_tcp( + name_server, + id, + query.as_bytes(), + host, + cname_buffer, + ); + } + + parse_query_response(&response, id, host, cname_buffer) + } + + fn query_a_or_cname_tcp( + name_server: SocketAddr, + id: u16, + query: &[u8], + host: &str, + cname_buffer: &mut [u8; low_dns::name::MAX_NAME_LENGTH], + ) -> Result { + let fd = net::socket( + AddressFamily::INET, + SocketType::STREAM, + Some(net::ipproto::TCP), + ) + .map_err(map_errno)?; + net::connect(&fd, &name_server).map_err(map_errno)?; + send_dns_tcp_query(&fd, query)?; + wait_readable(&fd)?; + + let mut response_buffer = Packet::max_buffer(); + let read = recv_dns_tcp_response(&fd, &mut response_buffer)?; + let response = + Packet::parse(&response_buffer[..read]).map_err(|_| ErrorKind::InvalidData)?; + parse_query_response(&response, id, host, cname_buffer) + } + + fn next_query_id(&self) -> u16 { + let mut id = self.next_id.fetch_add(1, Ordering::Relaxed).wrapping_add(1); + if id == 0 { + id = 1; + } + id + } +} + +enum QueryAnswer { + A(Ipv4Addr), + Cname(usize), +} + +struct SearchDomains<'a> { + domains: &'a [[u8; low_dns::name::MAX_NAME_LENGTH]; MAX_SEARCH_DOMAINS], + lens: &'a [usize; MAX_SEARCH_DOMAINS], + next: usize, +} + +impl<'a> Iterator for SearchDomains<'a> { + type Item = &'a [u8]; + + fn next(&mut self) -> Option { + while self.next < MAX_SEARCH_DOMAINS { + let index = self.next; + self.next += 1; + let len = self.lens[index]; + if len != 0 { + return Some(&self.domains[index][..len]); + } + } + + None + } +} + +fn build_a_query<'a>( + id: u16, + host: &'a str, + name_buffer: &'a mut [u8; low_dns::name::MAX_NAME_LENGTH], + packet_buffer: &'a mut [u8; low_dns::packet::MAX_PACKET_SIZE], +) -> Packet<'a> { + let name = Name::from_str_into_buf(host, name_buffer); + Packet::builder(packet_buffer) + .header(Header::builder().id(id).recursion_desired(true).build()) + .question(DnsQuestion::a(name)) + .build() +} + +fn parse_query_response( + response: &Packet<'_>, + id: u16, + queried: &str, + cname_buffer: &mut [u8; low_dns::name::MAX_NAME_LENGTH], +) -> Result { + if response.header().kind() != HeaderKind::Response + || response.header().id() != id + || response.header().response_code() != ResponseCode::NoError + { + return Err(ErrorKind::AddrNotAvailable); + } + + let mut cname_len = None; + for answer in response.answers() { + if *answer.name() != *queried { + continue; + } + + match answer.rdata() { + Rdata::A { ip } => return Ok(QueryAnswer::A(*ip)), + Rdata::CNAME { name } => { + let target = name + .as_str(cname_buffer) + .map_err(|_| ErrorKind::InvalidData)?; + validate_host(target)?; + cname_len = Some(target.len()); + } + _ => {} + } + } + + cname_len + .map(QueryAnswer::Cname) + .ok_or(ErrorKind::AddrNotAvailable) +} + +fn send_dns_tcp_query(fd: &OwnedFd, query: &[u8]) -> Result<(), ErrorKind> { + let len = u16::try_from(query.len()).map_err(|_| ErrorKind::InvalidInput)?; + send_all(fd, &len.to_be_bytes())?; + send_all(fd, query) +} + +fn recv_dns_tcp_response( + fd: &OwnedFd, + response_buffer: &mut [u8; low_dns::packet::MAX_PACKET_SIZE], +) -> Result { + let mut len_buffer = [0_u8; 2]; + recv_exact(fd, &mut len_buffer)?; + + let len = usize::from(u16::from_be_bytes(len_buffer)); + if len > response_buffer.len() { + return Err(ErrorKind::InvalidData); + } + + recv_exact(fd, &mut response_buffer[..len])?; + Ok(len) +} + +fn send_all(fd: &OwnedFd, bytes: &[u8]) -> Result<(), ErrorKind> { + let mut written_total = 0_usize; + while written_total < bytes.len() { + match net::send(fd, &bytes[written_total..], SendFlags::empty()) { + Ok(0) => return Err(ErrorKind::WriteZero), + Ok(written) => { + written_total = written_total + .checked_add(written) + .ok_or(ErrorKind::OutOfMemory)?; + } + Err(io::Errno::INTR) => {} + Err(errno) => return Err(map_errno(errno)), + } + } + + Ok(()) +} + +fn recv_exact(fd: &OwnedFd, buffer: &mut [u8]) -> Result<(), ErrorKind> { + let mut read_total = 0_usize; + while read_total < buffer.len() { + wait_readable(fd)?; + match net::recv(fd, &mut buffer[read_total..], RecvFlags::empty()) { + Ok((0, _)) => return Err(ErrorKind::InvalidData), + Ok((read, _)) => { + read_total = read_total.checked_add(read).ok_or(ErrorKind::OutOfMemory)?; + } + Err(io::Errno::INTR) => {} + Err(errno) => return Err(map_errno(errno)), + } + } + + Ok(()) +} + +impl Dns for RustixDnsResolver { + type Error = ErrorKind; + + async fn get_host_by_name( + &self, + host: &str, + addr_type: AddrType, + ) -> Result { + match addr_type { + AddrType::IPv4 | AddrType::Either => self.resolve_ipv4(host).map(IpAddr::V4), + AddrType::IPv6 => Err(ErrorKind::Unsupported), + } + } + + async fn get_host_by_address( + &self, + _addr: IpAddr, + _result: &mut [u8], + ) -> Result { + Err(ErrorKind::Unsupported) + } +} + +fn read_file(path: &str, buffer: &mut [u8]) -> Result { + let fd = fs::openat( + fs::CWD, + path, + OFlags::RDONLY | OFlags::CLOEXEC, + Mode::empty(), + ) + .map_err(map_errno)?; + + let mut used = 0_usize; + while used < buffer.len() { + let read = io::read(&fd, &mut buffer[used..]).map_err(map_errno)?; + if read == 0 { + break; + } + used = used.checked_add(read).ok_or(ErrorKind::OutOfMemory)?; + } + + Ok(used) +} + +fn parse_resolv_conf(bytes: &[u8], resolver: &mut RustixDnsResolver) { + for line in Lines::new(bytes) { + let line = strip_comment(line); + let mut tokens = Tokens::new(line); + let Some(kind) = tokens.next() else { + continue; + }; + + match kind { + b"nameserver" => { + let Some(addr) = tokens.next().and_then(parse_ipv4) else { + continue; + }; + resolver.push_name_server(SocketAddr::new(IpAddr::V4(addr), DNS_PORT)); + } + b"search" => { + resolver.clear_search_domains(); + for domain in tokens { + resolver.push_search_domain(domain); + } + } + b"domain" => { + let Some(domain) = tokens.next() else { + continue; + }; + resolver.clear_search_domains(); + resolver.push_search_domain(domain); + } + b"options" => { + for option in tokens { + if let Some(ndots) = parse_ndots(option) { + resolver.ndots = ndots; + } + } + } + _ => {} + } + } +} + +fn resolve_hosts_ipv4(bytes: &[u8], host: &str) -> Option { + let host = strip_trailing_dot(host).as_bytes(); + for line in Lines::new(bytes) { + let line = strip_comment(line); + let mut tokens = Tokens::new(line); + let Some(ip) = tokens.next().and_then(parse_ipv4) else { + continue; + }; + + for name in tokens { + if host_bytes_eq(name, host) { + return Some(ip); + } + } + } + + None +} + +fn strip_comment(line: &[u8]) -> &[u8] { + for (index, byte) in line.iter().enumerate() { + if *byte == b'#' || *byte == b';' { + return &line[..index]; + } + } + + line +} + +fn parse_ipv4(bytes: &[u8]) -> Option { + let mut octets = [0_u8; 4]; + let mut part = 0_usize; + let mut value = 0_u16; + let mut has_digit = false; + + for byte in bytes { + if byte.is_ascii_digit() { + value = value + .checked_mul(10)? + .checked_add(u16::from(*byte - b'0'))?; + if value > u16::from(u8::MAX) { + return None; + } + has_digit = true; + } else if *byte == b'.' { + if !has_digit || part >= 3 { + return None; + } + octets[part] = u8::try_from(value).ok()?; + part += 1; + value = 0; + has_digit = false; + } else { + return None; + } + } + + if !has_digit || part != 3 { + return None; + } + octets[part] = u8::try_from(value).ok()?; + + Some(Ipv4Addr::from(octets)) +} + +fn parse_ndots(bytes: &[u8]) -> Option { + let rest = bytes.strip_prefix(b"ndots:")?; + let mut value = 0_u8; + let mut has_digit = false; + + for byte in rest { + if !byte.is_ascii_digit() { + return None; + } + value = value.checked_mul(10)?.checked_add(*byte - b'0')?; + has_digit = true; + } + + if has_digit { + Some(value.min(15)) + } else { + None + } +} + +fn validate_host(host: &str) -> Result<(), ErrorKind> { + if valid_host_bytes(strip_trailing_dot(host).as_bytes()) { + Ok(()) + } else { + Err(ErrorKind::InvalidInput) + } +} + +fn valid_host_bytes(bytes: &[u8]) -> bool { + let bytes = strip_trailing_dot_bytes(bytes); + if bytes.is_empty() || bytes.len() > low_dns::name::MAX_NAME_LENGTH { + return false; + } + + for label in bytes.split(|byte| *byte == b'.') { + if label.is_empty() || label.len() > low_dns::name::MAX_LABEL_LENGTH { + return false; + } + } + + true +} + +fn copy_host( + host: &str, + buffer: &mut [u8; low_dns::name::MAX_NAME_LENGTH], +) -> Result { + let host = strip_trailing_dot(host); + validate_host(host)?; + if host.len() > buffer.len() { + return Err(ErrorKind::InvalidInput); + } + + buffer[..host.len()].copy_from_slice(host.as_bytes()); + Ok(host.len()) +} + +fn append_search_domain<'a>( + host: &str, + domain: &[u8], + buffer: &'a mut [u8; low_dns::name::MAX_NAME_LENGTH], +) -> Option<&'a str> { + let host = strip_trailing_dot(host).as_bytes(); + let domain = strip_trailing_dot_bytes(domain); + let len = host.len().checked_add(1)?.checked_add(domain.len())?; + if host.is_empty() || domain.is_empty() || len > buffer.len() { + return None; + } + + buffer[..host.len()].copy_from_slice(host); + buffer[host.len()] = b'.'; + buffer[host.len() + 1..len].copy_from_slice(domain); + if !valid_host_bytes(&buffer[..len]) { + return None; + } + + str::from_utf8(&buffer[..len]).ok() +} + +fn count_dots(bytes: &[u8]) -> u8 { + let mut count = 0_u8; + for byte in bytes { + if *byte == b'.' { + count = count.saturating_add(1); + } + } + + count +} + +const fn has_trailing_dot(host: &str) -> bool { + matches!(host.as_bytes().last(), Some(b'.')) +} + +fn strip_trailing_dot(host: &str) -> &str { + match host.strip_suffix('.') { + Some(stripped) => stripped, + None => host, + } +} + +fn strip_trailing_dot_bytes(bytes: &[u8]) -> &[u8] { + if matches!(bytes.last(), Some(b'.')) { + &bytes[..bytes.len() - 1] + } else { + bytes + } +} + +fn host_bytes_eq(left: &[u8], right: &[u8]) -> bool { + let left = strip_trailing_dot_bytes(left); + let right = strip_trailing_dot_bytes(right); + left.len() == right.len() + && left + .iter() + .zip(right.iter()) + .all(|(left, right)| left.eq_ignore_ascii_case(right)) +} + +fn wait_readable(fd: &OwnedFd) -> Result<(), ErrorKind> { + let mut fds = [PollFd::new(fd, PollFlags::IN)]; + let ready = event::poll(&mut fds, Some(&DNS_TIMEOUT)).map_err(map_errno)?; + if ready == 0 { + return Err(ErrorKind::TimedOut); + } + + let revents = fds[0].revents(); + if revents.intersects(PollFlags::IN) { + Ok(()) + } else { + Err(ErrorKind::Other) + } +} + +const fn map_errno(errno: io::Errno) -> ErrorKind { + match errno { + io::Errno::INTR => ErrorKind::Interrupted, + io::Errno::CONNREFUSED => ErrorKind::ConnectionRefused, + io::Errno::CONNRESET => ErrorKind::ConnectionReset, + io::Errno::NOENT => ErrorKind::NotFound, + io::Errno::ADDRINUSE => ErrorKind::AddrInUse, + io::Errno::ADDRNOTAVAIL => ErrorKind::AddrNotAvailable, + io::Errno::INVAL => ErrorKind::InvalidInput, + io::Errno::TIMEDOUT => ErrorKind::TimedOut, + io::Errno::NOMEM => ErrorKind::OutOfMemory, + _ => ErrorKind::Other, + } +} + +struct Lines<'a> { + bytes: &'a [u8], +} + +impl<'a> Lines<'a> { + const fn new(bytes: &'a [u8]) -> Self { + Self { bytes } + } +} + +impl<'a> Iterator for Lines<'a> { + type Item = &'a [u8]; + + fn next(&mut self) -> Option { + if self.bytes.is_empty() { + return None; + } + + for (index, byte) in self.bytes.iter().enumerate() { + if *byte == b'\n' { + let line = &self.bytes[..index]; + self.bytes = &self.bytes[index + 1..]; + return Some(line); + } + } + + let line = self.bytes; + self.bytes = &[]; + Some(line) + } +} + +struct Tokens<'a> { + bytes: &'a [u8], +} + +impl<'a> Tokens<'a> { + const fn new(bytes: &'a [u8]) -> Self { + Self { bytes } + } +} + +impl<'a> Iterator for Tokens<'a> { + type Item = &'a [u8]; + + fn next(&mut self) -> Option { + self.bytes = trim_start(self.bytes); + if self.bytes.is_empty() { + return None; + } + + for (index, byte) in self.bytes.iter().enumerate() { + if is_space(*byte) { + let token = &self.bytes[..index]; + self.bytes = &self.bytes[index + 1..]; + return Some(token); + } + } + + let token = self.bytes; + self.bytes = &[]; + Some(token) + } +} + +const fn trim_start(mut bytes: &[u8]) -> &[u8] { + while let Some((byte, rest)) = bytes.split_first() { + if !is_space(*byte) { + break; + } + bytes = rest; + } + + bytes +} + +const fn is_space(byte: u8) -> bool { + matches!(byte, b' ' | b'\t' | b'\r') +} diff --git a/libdd-signal-safe-http-client/examples/http_only_no_std/downloads.rs b/libdd-signal-safe-http-client/examples/http_only_no_std/downloads.rs new file mode 100644 index 0000000000..934f547da1 --- /dev/null +++ b/libdd-signal-safe-http-client/examples/http_only_no_std/downloads.rs @@ -0,0 +1,21 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +const MAX_DOWNLOAD_BYTES: usize = 300 * 1024 * 1024; + +pub(super) const DOWNLOADS: &[Sha256Download<'_>] = &[Sha256Download { + url: "http://dl-cdn.alpinelinux.org/alpine/v3.24/releases/x86_64/alpine-virt-3.24.0-x86_64.iso", + max_len: MAX_DOWNLOAD_BYTES, + sha256: [ + 0x6c, 0xd1, 0xa3, 0x8a, 0xe0, 0x5c, 0xf9, 0x6a, 0x5d, 0x0c, 0xbb, 0x2d, 0xdd, 0x6c, 0x63, + 0x08, 0x34, 0xba, 0xbf, 0xec, 0xa1, 0xec, 0xc5, 0xd1, 0xf0, 0x5e, 0xc0, 0xb0, 0x6b, 0x88, + 0x61, 0x02, + ], +}]; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(super) struct Sha256Download<'a> { + pub(super) url: &'a str, + pub(super) max_len: usize, + pub(super) sha256: [u8; 32], +} diff --git a/libdd-signal-safe-http-client/examples/http_only_no_std/linux_net.rs b/libdd-signal-safe-http-client/examples/http_only_no_std/linux_net.rs new file mode 100644 index 0000000000..230de3d3b2 --- /dev/null +++ b/libdd-signal-safe-http-client/examples/http_only_no_std/linux_net.rs @@ -0,0 +1,98 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use core::net::SocketAddr; + +use libdd_signal_safe_http_client::io::{ + embedded_io::{ErrorKind, ErrorType}, + embedded_io_async::{Read, Write}, + embedded_nal_async::TcpConnect, +}; +use rustix::{ + fd::OwnedFd, + io::Errno, + net::{self, AddressFamily, RecvFlags, SendFlags, SocketType}, +}; + +pub(super) struct RustixTcpConnector; + +impl TcpConnect for RustixTcpConnector { + type Error = ErrorKind; + type Connection<'a> + = RustixTcpConnection + where + Self: 'a; + + async fn connect(&self, remote: SocketAddr) -> Result, Self::Error> { + let family = match remote { + SocketAddr::V4(_) => AddressFamily::INET, + SocketAddr::V6(_) => AddressFamily::INET6, + }; + let fd = + net::socket(family, SocketType::STREAM, Some(net::ipproto::TCP)).map_err(map_errno)?; + net::connect(&fd, &remote).map_err(map_errno)?; + + Ok(RustixTcpConnection { fd }) + } +} + +pub(super) struct RustixTcpConnection { + fd: OwnedFd, +} + +impl ErrorType for RustixTcpConnection { + type Error = ErrorKind; +} + +impl Read for RustixTcpConnection { + async fn read(&mut self, buffer: &mut [u8]) -> Result { + net::recv(&self.fd, buffer, RecvFlags::empty()) + .map(|(read, _)| read) + .map_err(map_errno) + } +} + +impl Write for RustixTcpConnection { + async fn write(&mut self, buffer: &[u8]) -> Result { + let written = net::send(&self.fd, buffer, SendFlags::empty()).map_err(map_errno)?; + if written == 0 && !buffer.is_empty() { + return Err(ErrorKind::WriteZero); + } + + Ok(written) + } + + async fn write_all(&mut self, mut buffer: &[u8]) -> Result<(), Self::Error> { + while !buffer.is_empty() { + let written = self.write(buffer).await?; + if written == 0 { + return Err(ErrorKind::WriteZero); + } + buffer = &buffer[written..]; + } + + Ok(()) + } + + async fn flush(&mut self) -> Result<(), Self::Error> { + Ok(()) + } +} + +const fn map_errno(errno: Errno) -> ErrorKind { + match errno { + Errno::INTR => ErrorKind::Interrupted, + Errno::CONNREFUSED => ErrorKind::ConnectionRefused, + Errno::CONNRESET => ErrorKind::ConnectionReset, + Errno::CONNABORTED => ErrorKind::ConnectionAborted, + Errno::NOTCONN => ErrorKind::NotConnected, + Errno::ADDRINUSE => ErrorKind::AddrInUse, + Errno::ADDRNOTAVAIL => ErrorKind::AddrNotAvailable, + Errno::PIPE => ErrorKind::BrokenPipe, + Errno::EXIST => ErrorKind::AlreadyExists, + Errno::INVAL => ErrorKind::InvalidInput, + Errno::TIMEDOUT => ErrorKind::TimedOut, + Errno::NOMEM => ErrorKind::OutOfMemory, + _ => ErrorKind::Other, + } +} diff --git a/libdd-signal-safe-http-client/examples/http_only_no_std/logger.rs b/libdd-signal-safe-http-client/examples/http_only_no_std/logger.rs new file mode 100644 index 0000000000..61ccd09442 --- /dev/null +++ b/libdd-signal-safe-http-client/examples/http_only_no_std/logger.rs @@ -0,0 +1,104 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use rustix::{io, stdio}; + +const HEX: &[u8; 16] = b"0123456789abcdef"; + +pub(super) struct Logger; + +impl Logger { + pub(super) fn line(message: &str) { + write_str(message); + newline(); + } + + pub(super) fn field_str(name: &str, value: &str) { + write_str(name); + write_str(": "); + write_str(value); + newline(); + } + + pub(super) fn field_usize(name: &str, value: usize) { + write_str(name); + write_str(": "); + write_usize(value); + newline(); + } + + pub(super) fn download_start(index: usize, total: usize, url: &str) { + write_str("download "); + write_usize(index); + write_str("/"); + write_usize(total); + write_str(": "); + write_str(url); + newline(); + } + + pub(super) fn http_status(status: u16) { + write_str("http status: "); + write_usize(usize::from(status)); + newline(); + } + + pub(super) fn progress(read: usize, expected: usize) { + write_str("streamed: "); + write_usize(read); + write_str("/"); + write_usize(expected); + write_str(" bytes"); + newline(); + } + + pub(super) fn sha256(digest: &[u8]) { + write_str("sha256: "); + for byte in digest { + let hi = usize::from(byte >> 4); + let lo = usize::from(byte & 0x0f); + write_bytes(&[HEX[hi], HEX[lo]]); + } + newline(); + } +} + +fn write_usize(mut value: usize) { + if value == 0 { + write_str("0"); + return; + } + + let mut buffer = [0_u8; 39]; + let mut next = buffer.len(); + while value != 0 { + next -= 1; + buffer[next] = b'0' + u8::try_from(value % 10).unwrap_or_default(); + value /= 10; + } + + write_bytes(&buffer[next..]); +} + +fn write_str(value: &str) { + write_bytes(value.as_bytes()); +} + +fn newline() { + write_bytes(b"\n"); +} + +fn write_bytes(mut bytes: &[u8]) { + while !bytes.is_empty() { + // SAFETY: This example is single-threaded and assumes the inherited + // stdout file descriptor has not been closed or reused. + let stdout = unsafe { stdio::stdout() }; + let Ok(written) = io::write(stdout, bytes) else { + return; + }; + if written == 0 { + return; + } + bytes = &bytes[written..]; + } +} diff --git a/libdd-signal-safe-http-client/examples/http_only_no_std/support.rs b/libdd-signal-safe-http-client/examples/http_only_no_std/support.rs new file mode 100644 index 0000000000..c5841a6888 --- /dev/null +++ b/libdd-signal-safe-http-client/examples/http_only_no_std/support.rs @@ -0,0 +1,61 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +mod dns; +mod downloads; +mod linux_net; +mod logger; +mod verifier; + +use core::{ + future::Future, + pin::pin, + task::{Context, Poll, Waker}, +}; + +use dns::RustixDnsResolver; +use downloads::DOWNLOADS; +use linux_net::RustixTcpConnector; +use logger::Logger; + +pub fn run() -> i32 { + Logger::line("libdd signal-safe HTTP example"); + Logger::line("runtime: origin + rustix"); + Logger::line("features: no_std, no_alloc, http only"); + Logger::line("dns: /etc/hosts + low_dns UDP/TCP resolver using /etc/resolv.conf"); + + let dns = RustixDnsResolver::from_resolv_conf(); + Logger::field_usize("nameservers", dns.name_server_count()); + Logger::field_usize("search domains", dns.search_domain_count()); + Logger::field_usize("ndots", usize::from(dns.ndots())); + Logger::field_usize("downloads", DOWNLOADS.len()); + + let tcp = RustixTcpConnector; + + match block_on(verifier::verify_downloads(&tcp, &dns, DOWNLOADS)) { + Ok(()) => { + Logger::line("result: ok"); + 0 + } + Err(error) => { + Logger::field_str("result", error.as_str()); + 1 + } + } +} + +fn block_on(future: F) -> F::Output +where + F: Future, +{ + let waker = Waker::noop(); + let mut context = Context::from_waker(waker); + let mut future = pin!(future); + + loop { + match future.as_mut().poll(&mut context) { + Poll::Ready(output) => return output, + Poll::Pending => core::hint::spin_loop(), + } + } +} diff --git a/libdd-signal-safe-http-client/examples/http_only_no_std/verifier.rs b/libdd-signal-safe-http-client/examples/http_only_no_std/verifier.rs new file mode 100644 index 0000000000..00b2f10d01 --- /dev/null +++ b/libdd-signal-safe-http-client/examples/http_only_no_std/verifier.rs @@ -0,0 +1,155 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use libdd_signal_safe_http_client::{ + io::{embedded_io_async::Read, embedded_nal_async::Dns}, + request::Method, + HttpClient, +}; +use sha2::{Digest, Sha256}; + +use super::{downloads::Sha256Download, linux_net::RustixTcpConnector, logger::Logger}; + +const PROGRESS_INTERVAL_BYTES: usize = 8 * 1024 * 1024; + +#[derive(Debug)] +pub(super) enum VerifyError { + Http, + UnexpectedStatus, + MissingLength, + TooLarge, + LengthMismatch, + ChecksumMismatch, +} + +impl VerifyError { + pub(super) const fn as_str(&self) -> &'static str { + match self { + Self::Http => "http error", + Self::UnexpectedStatus => "unexpected HTTP status", + Self::MissingLength => "missing content-length", + Self::TooLarge => "download too large", + Self::LengthMismatch => "content-length mismatch", + Self::ChecksumMismatch => "sha256 mismatch", + } + } +} + +pub(super) async fn verify_downloads( + tcp: &RustixTcpConnector, + dns: &D, + downloads: &[Sha256Download<'_>], +) -> Result<(), VerifyError> +where + D: Dns + Sync, +{ + let mut response_buffer = [0_u8; 4096]; + let mut body_buffer = [0_u8; 8192]; + let mut index = 0_usize; + + for download in downloads { + index += 1; + Logger::download_start(index, downloads.len(), download.url); + + let mut client = HttpClient::new(tcp, dns); + verify_download( + &mut client, + download, + &mut response_buffer, + &mut body_buffer, + ) + .await?; + } + + Ok(()) +} + +async fn verify_download( + client: &mut HttpClient<'_, RustixTcpConnector, D>, + download: &Sha256Download<'_>, + response_buffer: &mut [u8], + body_buffer: &mut [u8], +) -> Result<(), VerifyError> +where + D: Dns + Sync, +{ + Logger::line("request: GET"); + let mut request = client + .request(Method::GET, download.url) + .await + .map_err(|_| VerifyError::Http)?; + Logger::line("request: send"); + let response = request + .send(response_buffer) + .await + .map_err(|_| VerifyError::Http)?; + Logger::http_status(response.status.0); + if response.status.0 != 200 { + return Err(VerifyError::UnexpectedStatus); + } + + let expected_len = response.content_length.ok_or(VerifyError::MissingLength)?; + Logger::field_usize("content-length", expected_len); + if expected_len > download.max_len { + return Err(VerifyError::TooLarge); + } + + let mut body = response.body().reader(); + let mut hasher = Sha256::new(); + let mut total = 0_usize; + let mut next_progress = PROGRESS_INTERVAL_BYTES; + + loop { + let read = body + .read(body_buffer) + .await + .map_err(|_| VerifyError::Http)?; + if read == 0 { + break; + } + + total = total.checked_add(read).ok_or(VerifyError::TooLarge)?; + if total > download.max_len { + return Err(VerifyError::TooLarge); + } + + hasher.update(&body_buffer[..read]); + + if total >= next_progress || total == expected_len { + Logger::progress(total, expected_len); + while next_progress <= total { + let Some(next) = next_progress.checked_add(PROGRESS_INTERVAL_BYTES) else { + next_progress = usize::MAX; + break; + }; + next_progress = next; + } + } + } + + if total != expected_len { + return Err(VerifyError::LengthMismatch); + } + + let actual = hasher.finalize(); + Logger::sha256(&actual); + if !digest_matches(&actual, &download.sha256) { + return Err(VerifyError::ChecksumMismatch); + } + + Logger::line("download verified"); + Ok(()) +} + +fn digest_matches(actual: &[u8], expected: &[u8; 32]) -> bool { + if actual.len() != expected.len() { + return false; + } + + let mut diff = 0_u8; + for i in 0..expected.len() { + diff |= actual[i] ^ expected[i]; + } + + diff == 0 +} diff --git a/libdd-signal-safe-http-client/src/lib.rs b/libdd-signal-safe-http-client/src/lib.rs new file mode 100644 index 0000000000..3ff7019f07 --- /dev/null +++ b/libdd-signal-safe-http-client/src/lib.rs @@ -0,0 +1,87 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +#![deny(missing_docs)] +#![cfg_attr(not(any(test, feature = "std")), no_std)] +#![cfg_attr(not(test), deny(clippy::panic))] +#![cfg_attr(not(test), deny(clippy::unwrap_used))] +#![cfg_attr(not(test), deny(clippy::expect_used))] +#![cfg_attr(not(test), deny(clippy::todo))] +#![cfg_attr(not(test), deny(clippy::unimplemented))] + +//! `libdd-signal-safe-http-client` is a `no_std` facade over [`reqwless`]. +//! +//! The crate keeps allocation and `std` support opt-in. The default build uses +//! caller-provided buffers and transports only. HTTP request encoding and +//! response parsing come from `reqwless`; TLS integration is selected by feature: +//! +//! - `mbedtls` exposes generic `MbedTLS` bindings for callers that wrap their own `embedded-io` +//! transport. +//! - `esp-mbedtls` marks ESP mbedtls-backed integrations where callers provide the platform +//! backend. +//! +//! Async-signal-safety still depends on the transport, allocator, TLS backend, +//! and platform hooks supplied by the caller. + +/// HTTP client and connection types. +pub mod client { + pub use reqwless::client::{ + HttpClient, HttpConnection, HttpRequestHandle, HttpResource, HttpResourceRequestBuilder, + }; +} + +/// HTTP header helper types. +pub mod headers { + pub use reqwless::headers::*; +} + +/// Embedded I/O traits used by the client. +pub mod io { + pub use embedded_io; + pub use embedded_io_async; + pub use embedded_nal_async; +} + +/// HTTP request builders and body traits. +pub mod request { + pub use reqwless::request::*; +} + +/// HTTP response parsing and body reader types. +pub mod response { + pub use reqwless::response::*; +} + +/// TLS backend integration points. +pub mod tls { + /// Generic `MbedTLS` bindings. + #[cfg(feature = "mbedtls")] + pub mod mbedtls { + pub use ::mbedtls::*; + } + + /// Marker for an ESP mbedtls-backed transport. + #[cfg(feature = "esp-mbedtls")] + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct EspMbedTls; +} + +pub use client::{HttpClient, HttpConnection, HttpRequestHandle, HttpResource}; +pub use reqwless::{Error, TryBufRead}; + +#[cfg(test)] +mod tests { + use super::request::{Request, RequestBuilder}; + + #[test] + fn builds_reqwless_request_without_allocating() { + let headers = [("content-type", "application/json")]; + let request = Request::post("/v0.4/traces") + .host("localhost") + .headers(&headers) + .body(b"{}".as_slice()) + .build(); + + let _ = request; + } +} diff --git a/license-tool.toml b/license-tool.toml index 545da0c923..935bc1f9f6 100644 --- a/license-tool.toml +++ b/license-tool.toml @@ -6,3 +6,4 @@ # This crate is missing a license in its metadata. "stringmetrics" = { license = "MIT" } +"rs-libc" = { license = "MPL-2.0 AND MIT AND BSD-3-Clause AND Patrick-Powell" } diff --git a/tools/docker/Dockerfile.build b/tools/docker/Dockerfile.build index 31fa97614f..662164b848 100644 --- a/tools/docker/Dockerfile.build +++ b/tools/docker/Dockerfile.build @@ -87,6 +87,7 @@ COPY "libdd-log/Cargo.toml" "libdd-log/" COPY "libdd-log-ffi/Cargo.toml" "libdd-log-ffi/" COPY "libdd-dogstatsd-client/Cargo.toml" "libdd-dogstatsd-client/" COPY "libdd-http-client/Cargo.toml" "libdd-http-client/" +COPY "libdd-signal-safe-http-client/Cargo.toml" "libdd-signal-safe-http-client/" COPY "libdd-agent-client/Cargo.toml" "libdd-agent-client/" COPY "libdd-library-config-ffi/Cargo.toml" "libdd-library-config-ffi/" COPY "libdd-library-config/Cargo.toml" "libdd-library-config/"