diff --git a/Cargo.lock b/Cargo.lock index 0530d9b..66ea538 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,9 +34,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] @@ -64,9 +64,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android_system_properties" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" dependencies = [ "libc", ] @@ -129,15 +129,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.103" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "ar_archive_writer" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4087686b4b0a3427190bae57a1d9a478dbb2d40c5dc1bd6e2b6d797913bdd348" +checksum = "73cd58deff2140a0a8eae87e417bd01db68a33e148aa93d1e8cd837e55e312b6" dependencies = [ "object", ] @@ -160,6 +160,12 @@ dependencies = [ "rustversion", ] +[[package]] +name = "array-init" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" + [[package]] name = "arrayvec" version = "0.5.2" @@ -171,6 +177,9 @@ name = "arrayvec" version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" +dependencies = [ + "serde", +] [[package]] name = "ascii-canvas" @@ -190,50 +199,22 @@ dependencies = [ "term 1.2.1", ] -[[package]] -name = "asn1-rs" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" -dependencies = [ - "asn1-rs-derive 0.5.1", - "asn1-rs-impl", - "displaydoc", - "nom 7.1.3", - "num-traits", - "rusticata-macros", - "thiserror 1.0.69", - "time", -] - [[package]] name = "asn1-rs" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" dependencies = [ - "asn1-rs-derive 0.6.0", + "asn1-rs-derive", "asn1-rs-impl", "displaydoc", "nom 7.1.3", "num-traits", "rusticata-macros", - "thiserror 2.0.18", + "thiserror 2.0.20", "time", ] -[[package]] -name = "asn1-rs-derive" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", - "synstructure", -] - [[package]] name = "asn1-rs-derive" version = "0.6.0" @@ -282,9 +263,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" +checksum = "3976abdc8fe7d1133d43d304afd42abdf5bc3e1319d263d223bde07b5efc4be8" dependencies = [ "compression-codecs", "compression-core", @@ -329,7 +310,7 @@ version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" dependencies = [ - "event-listener 5.4.1", + "event-listener 5.4.2", "event-listener-strategy", "pin-project-lite", ] @@ -353,13 +334,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.89" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -404,9 +385,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.17.1" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" +checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" dependencies = [ "aws-lc-sys", "untrusted 0.7.1", @@ -415,9 +396,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.42.0" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" +checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" dependencies = [ "cc", "cmake", @@ -559,6 +540,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "base64ct" version = "1.8.3" @@ -572,26 +559,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" [[package]] -name = "binread" -version = "2.2.0" +name = "binrw" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16598dfc8e6578e9b597d9910ba2e73618385dc9f4b1d43dd92c349d6be6418f" +checksum = "6ad120d555272286c1017d25165ab8bd74806f13fc85b258484ec7e4ce75458f" dependencies = [ - "binread_derive", - "lazy_static", - "rustversion", + "array-init", + "binrw_derive", + "bytemuck", ] [[package]] -name = "binread_derive" -version = "2.1.0" +name = "binrw_derive" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d9672209df1714ee804b1f4d4f68c8eb2a90b1f7a07acf472f88ce198ef1fed" +checksum = "6df92e0e9baae4dc82c7bad7715ca40c0a5c71539057bf2ea04a5c29c980410b" dependencies = [ "either", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.119", ] [[package]] @@ -635,24 +622,30 @@ dependencies = [ [[package]] name = "bitfield" -version = "0.19.4" +version = "0.19.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21ba6517c6b0f2bf08be60e187ab64b038438f22dd755614d8fe4d4098c46419" +checksum = "b45721c9db4c7a20899d05efb7ad9235f50b256e980db30ffb229abf732934c3" dependencies = [ "bitfield-macros", ] [[package]] name = "bitfield-macros" -version = "0.19.4" +version = "0.19.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f48d6ace212fdf1b45fd6b566bb40808415344642b76c3224c07c8df9da81e97" +checksum = "c0cb6f3d4773a2107b94cbeccaa5b5f0b35a88389b5d522d13d659f64317b22d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.13.1" @@ -737,9 +730,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" dependencies = [ "memchr", "serde_core", @@ -757,6 +750,12 @@ version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + [[package]] name = "byteorder" version = "1.5.0" @@ -784,18 +783,18 @@ dependencies = [ "ahash", "hashbrown 0.15.5", "once_cell", - "thiserror 2.0.18", + "thiserror 2.0.20", "web-time", ] [[package]] name = "candid" -version = "0.10.32" +version = "0.10.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b649560badafa98cdf260958393f317c013b5aa819dba26f21cc3e26cae41dae" +checksum = "5cdba86ff862a2cc6b244f273a8713ffdf82b84254470e90bd029ee87c283f25" dependencies = [ "anyhow", - "binread", + "binrw", "byteorder", "candid_derive", "hex", @@ -813,9 +812,9 @@ dependencies = [ [[package]] name = "candid_derive" -version = "0.10.32" +version = "0.10.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f94f9df97dd04077b5a30e82dfb5a5f2eae1a6e8c9d6a98fd24351c8a333464" +checksum = "13501edc1f9c9f057d5d84171a13ee1fe4255705f9027a3fac64b5949cbdd170" dependencies = [ "lazy_static", "proc-macro2", @@ -825,9 +824,9 @@ dependencies = [ [[package]] name = "candid_parser" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e35c12ed409a6c02c6e204db49926c75ceeb622a4a994b828cdd3dea7dfec59" +checksum = "b4d1368a4edc90c239b652b675a99fb21e5ce600c7ef3da082a3fcdb07bd1e1f" dependencies = [ "anyhow", "candid", @@ -853,9 +852,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.67" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" +checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" dependencies = [ "find-msvc-tools", "jobserver", @@ -871,9 +870,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] name = "chacha20" @@ -964,9 +963,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.2" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd059f9da4f5c36b3787f65d38ccaab1cc315f07b01f89abc8359ee6a8205011" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" dependencies = [ "clap_builder", "clap_derive", @@ -974,9 +973,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.2" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" dependencies = [ "anstream", "anstyle", @@ -986,14 +985,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.1" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -1375,9 +1374,40 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.11.0" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[package]] +name = "defmt" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.20", +] [[package]] name = "der" @@ -1392,27 +1422,13 @@ dependencies = [ "zeroize", ] -[[package]] -name = "der-parser" -version = "9.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" -dependencies = [ - "asn1-rs 0.6.2", - "displaydoc", - "nom 7.1.3", - "num-bigint", - "num-traits", - "rusticata-macros", -] - [[package]] name = "der-parser" version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" dependencies = [ - "asn1-rs 0.7.2", + "asn1-rs", "displaydoc", "nom 7.1.3", "num-bigint", @@ -1569,13 +1585,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -1654,9 +1670,9 @@ dependencies = [ [[package]] name = "either" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" [[package]] name = "elliptic-curve" @@ -1739,11 +1755,10 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" -version = "5.4.1" +version = "5.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" dependencies = [ - "concurrent-queue", "parking", "pin-project-lite", ] @@ -1754,7 +1769,7 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ - "event-listener 5.4.1", + "event-listener 5.4.2", "pin-project-lite", ] @@ -1771,9 +1786,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "ff" @@ -1803,9 +1818,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" [[package]] name = "fixedbitset" @@ -1874,16 +1889,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "forwarded-header-value" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9" -dependencies = [ - "nonempty", - "thiserror 1.0.69", -] - [[package]] name = "fqdn" version = "0.5.2" @@ -1930,9 +1935,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" dependencies = [ "futures-channel", "futures-core", @@ -1945,9 +1950,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", "futures-sink", @@ -1955,15 +1960,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" dependencies = [ "futures-core", "futures-task", @@ -1972,9 +1977,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] name = "futures-lite" @@ -1991,13 +1996,13 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -2013,15 +2018,15 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-timer" @@ -2031,9 +2036,9 @@ checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-channel", "futures-core", @@ -2180,9 +2185,9 @@ dependencies = [ [[package]] name = "handlebars" -version = "6.4.3" +version = "6.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4633d16a2350341713c379d6d06a4b9e1845329386026a49ce4fd09c2f3b16f6" +checksum = "75c54236f9045c8004a77942bebc52145b4844639db934a5c70fe08617fbe61a" dependencies = [ "derive_builder", "log", @@ -2191,7 +2196,7 @@ dependencies = [ "pest_derive", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -2281,7 +2286,7 @@ checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183" dependencies = [ "async-trait", "aws-lc-rs", - "bitflags", + "bitflags 2.13.1", "bytes", "cfg-if", "data-encoding", @@ -2300,14 +2305,14 @@ dependencies = [ "rustls", "rustls-pki-types", "rustls-platform-verifier", - "thiserror 2.0.18", + "thiserror 2.0.20", "time", "tinyvec", "tokio", "tokio-rustls", "tracing", "url", - "webpki-roots 1.0.8", + "webpki-roots 1.0.9", ] [[package]] @@ -2317,7 +2322,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643" dependencies = [ "aws-lc-rs", - "bitflags", + "bitflags 2.13.1", "data-encoding", "idna", "ipnet", @@ -2327,7 +2332,7 @@ dependencies = [ "rand 0.10.2", "ring", "rustls-pki-types", - "thiserror 2.0.18", + "thiserror 2.0.20", "time", "tinyvec", "tracing", @@ -2356,11 +2361,11 @@ dependencies = [ "rustls", "smallvec", "system-configuration", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tokio-rustls", "tracing", - "webpki-roots 1.0.8", + "webpki-roots 1.0.9", ] [[package]] @@ -2383,9 +2388,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -2403,9 +2408,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" dependencies = [ "bytes", "futures-core", @@ -2434,18 +2439,18 @@ checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15" [[package]] name = "hybrid-array" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" dependencies = [ "typenum", ] [[package]] name = "hyper" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" dependencies = [ "atomic-waker", "bytes", @@ -2479,20 +2484,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.8", -] - -[[package]] -name = "hyper-timeout" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" -dependencies = [ - "hyper", - "hyper-util", - "pin-project-lite", - "tokio", - "tower-service", + "webpki-roots 1.0.9", ] [[package]] @@ -2576,7 +2568,7 @@ dependencies = [ "k256", "leb128", "p256", - "pem", + "pem 3.0.6", "pkcs8", "rand 0.10.2", "rangemap", @@ -2588,7 +2580,7 @@ dependencies = [ "serde_repr", "sha2 0.10.9", "stop-token", - "thiserror 2.0.18", + "thiserror 2.0.20", "time", "tokio", "tower-service", @@ -2608,7 +2600,7 @@ dependencies = [ "axum", "axum-extra", "axum-server", - "base64 0.22.1", + "base64 0.23.1", "bytes", "candid", "chacha20poly1305", @@ -2633,19 +2625,22 @@ dependencies = [ "hyper-util", "ic-agent", "ic-custom-domains-canister-api", + "ic-transport-types 0.49.2", "ic-verify-bls-signature", "indoc", "instant-acme", + "ipnet", "itertools 0.15.0", "mail-auth", "mail-parser", "mail-send", + "maxminddb", "mock-io", "mockall", "moka", "nix", "parse-size", - "pem", + "pem 4.0.0", "pocket-ic", "ppp", "prometheus", @@ -2653,7 +2648,7 @@ dependencies = [ "prost-types", "rand 0.8.7", "rand_regex", - "rcgen 0.14.8", + "rcgen 0.14.9", "regex", "reqwest 0.13.4", "rustls", @@ -2677,7 +2672,7 @@ dependencies = [ "systemstat", "tar", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tokio-io-timeout", "tokio-rustls", @@ -2686,7 +2681,6 @@ dependencies = [ "tower", "tower-http 0.7.0", "tower-service", - "tower_governor", "tracing", "tracing-subscriber", "url", @@ -2695,7 +2689,7 @@ dependencies = [ "uuid", "vrl", "webpki-root-certs", - "x509-parser 0.18.1", + "x509-parser", "zeroize", "zstd", ] @@ -2713,7 +2707,7 @@ dependencies = [ "ic0", "pin-project-lite", "serde", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -2788,7 +2782,7 @@ dependencies = [ "derive-new", "serde", "strum 0.28.0", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -2802,9 +2796,9 @@ dependencies = [ "hex-literal", "hkdf", "ic_principal", - "pem", + "pem 3.0.6", "rand 0.8.7", - "thiserror 2.0.18", + "thiserror 2.0.20", "zeroize", ] @@ -2865,7 +2859,7 @@ dependencies = [ "serde_cbor", "serde_repr", "sha2 0.10.9", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -2883,7 +2877,7 @@ dependencies = [ "serde_cbor", "serde_repr", "sha2 0.10.9", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -2937,9 +2931,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" dependencies = [ "displaydoc", "potential_utf", @@ -2951,9 +2945,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" dependencies = [ "displaydoc", "litemap", @@ -2964,9 +2958,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -2978,16 +2972,17 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" [[package]] name = "icu_properties" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" dependencies = [ + "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", @@ -2998,15 +2993,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" [[package]] name = "icu_provider" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +checksum = "92a7ed671a6aad807a8651a2e1782a6598fda9ce5185dd8158549e95a91c6428" dependencies = [ "displaydoc", "icu_locale_core", @@ -3111,12 +3106,12 @@ dependencies = [ "hyper", "hyper-rustls", "hyper-util", - "rcgen 0.14.8", + "rcgen 0.14.9", "rustls", "rustls-pki-types", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", ] @@ -3141,13 +3136,19 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" dependencies = [ "serde", ] +[[package]] +name = "ipnetwork" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf370abdafd54d13e54a620e8c3e1145f28e46cc9d704bc6d94414559df41763" + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -3205,6 +3206,59 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jiff" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link", +] + +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + +[[package]] +name = "jiff-static" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + [[package]] name = "jni" version = "0.22.4" @@ -3217,7 +3271,7 @@ dependencies = [ "jni-sys", "log", "simd_cesu8", - "thiserror 2.0.18", + "thiserror 2.0.20", "walkdir", "windows-link", ] @@ -3266,9 +3320,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.103" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" dependencies = [ "cfg-if", "futures-util", @@ -3327,9 +3381,9 @@ dependencies = [ [[package]] name = "kstat-rs" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27964e4632377753acb0898ce6f28770d50cbca1339200ae63d700cff97b5c2b" +checksum = "52d0786643a0b49f595bd1fc81e1c1aa7bad8555bc820c6892f4d28cb20cf210" dependencies = [ "libc", "thiserror 1.0.69", @@ -3419,9 +3473,9 @@ checksum = "c83bff1d572d6b9aeef67ddfc8448e4a3737909cb28e81f97c791b9018703e52" [[package]] name = "libc" -version = "0.2.186" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libm" @@ -3431,9 +3485,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.18" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +checksum = "28d0a00925a9f930d679b6789b721e3a7f9ed110f41b86d2497caa780c3a070a" dependencies = [ "libc", ] @@ -3452,9 +3506,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" [[package]] name = "lock_api" @@ -3536,9 +3590,9 @@ checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" [[package]] name = "mail-auth" -version = "0.11.2" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dc03ec0f6be1788623ee3d275940ca0053ced3154eb881ffe70edf2f43d3d41" +checksum = "faa246141b9a9194be268605d9db7228784a0eb347d155eeaa223181037693d0" dependencies = [ "aws-lc-rs", "flate2", @@ -3546,6 +3600,7 @@ dependencies = [ "getrandom 0.4.3", "hashify", "hickory-resolver", + "idna", "mail-builder", "mail-parser", "quick-xml", @@ -3567,9 +3622,9 @@ dependencies = [ [[package]] name = "mail-parser" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47785d444be4d32c1709171c6219a90f667c0ad0ffe68b4b179e794f31f4f9e8" +checksum = "4084ec5c2f90b341d0c70990e92a23b128f75ca14fc1dd5edd8fd5c9b417da4d" dependencies = [ "encoding_rs", "hashify", @@ -3607,6 +3662,19 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "maxminddb" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65e84ef32bcbf18a95548989e880db4af6fafd563463753afb4b9a149fb2782c" +dependencies = [ + "ipnetwork", + "log", + "memchr", + "serde", + "thiserror 2.0.20", +] + [[package]] name = "memchr" version = "2.8.3" @@ -3683,9 +3751,9 @@ dependencies = [ [[package]] name = "mockall" -version = "0.13.1" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2" +checksum = "1a6ceddfe3ce334925e96bf420fdb2dcee5bed6c632a168ece622676dadeaf8a" dependencies = [ "cfg-if", "downcast", @@ -3697,9 +3765,9 @@ dependencies = [ [[package]] name = "mockall_derive" -version = "0.13.1" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898" +checksum = "9cfe16fbe8a314aeec0b861ac24e60b1e123e97634bab045475b9d6a18416fd8" dependencies = [ "cfg-if", "proc-macro2", @@ -3709,9 +3777,9 @@ dependencies = [ [[package]] name = "moka" -version = "0.12.15" +version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" +checksum = "4293f18e7567a1caf3c584855554377025c65e0aa445344d04171f5ad63d19b9" dependencies = [ "crossbeam-channel", "crossbeam-epoch", @@ -3742,7 +3810,7 @@ version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags", + "bitflags 2.13.1", "cfg-if", "cfg_aliases", "libc", @@ -3776,12 +3844,6 @@ dependencies = [ "nom 8.0.0", ] -[[package]] -name = "nonempty" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" - [[package]] name = "nonzero_ext" version = "0.3.0" @@ -3861,9 +3923,9 @@ checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-integer" -version = "0.1.46" +version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" dependencies = [ "num-traits", ] @@ -3880,9 +3942,9 @@ dependencies = [ [[package]] name = "num-modular" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc41a1374056e9672221567958a66c16be12d0e2c1b408761e14d901c237d5e0" +checksum = "bd8e500409e6cd603b03e477c26a6caecdc27ac58979a53e881c75eafc079f44" [[package]] name = "num-order" @@ -3916,29 +3978,20 @@ dependencies = [ [[package]] name = "object" -version = "0.37.3" +version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" dependencies = [ "memchr", ] -[[package]] -name = "oid-registry" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" -dependencies = [ - "asn1-rs 0.6.2", -] - [[package]] name = "oid-registry" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" dependencies = [ - "asn1-rs 0.7.2", + "asn1-rs", ] [[package]] @@ -4080,6 +4133,16 @@ dependencies = [ "serde_core", ] +[[package]] +name = "pem" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d354a98a3d1251555de99e8fdd8afda05573c31b82f59063a7b0a29b5527f120" +dependencies = [ + "base64 0.23.1", + "serde_core", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -4097,9 +4160,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.7" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47627dd7305c6a2d6c8c6bcd24c5a4c17dbbf425f4f9c5313e724b38fc9782e9" +checksum = "5a07a60cc7a4d00c91f95c685609d1d2f79050e6804b70ebedd7650f0b839bcf" dependencies = [ "memchr", "ucd-trie", @@ -4107,9 +4170,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.7" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b4254325ecad416ab689e27ba51da03ba01a9632bc6e108f5fe7c3c4ad29d58" +checksum = "b3a83744a5c8455b8b3e0dc5031362780a347c878bdd11584d1a8984228cc88d" dependencies = [ "pest", "pest_generator", @@ -4117,9 +4180,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.7" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c4c0e91ead7a8f7acecbca6f003fc2e8282b1dbe2dd9c9d2f16aba42995e0a7" +checksum = "e0cd3451aa3de60d4b9a1e736885e4dea6b31617598026f12256ad566d63304a" dependencies = [ "pest", "pest_meta", @@ -4130,9 +4193,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.8.7" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9744bc48116fee06334924bb5f2bad41eed5e89bd26e29b0b799f9a3f82c210" +checksum = "e04d3a0849e241d7dfce834c83b1c5edc8622009e8dd51a12ba1927c32f05496" dependencies = [ "pest", ] @@ -4172,26 +4235,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" -[[package]] -name = "pin-project" -version = "1.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "pin-project-lite" version = "0.2.17" @@ -4232,9 +4275,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" [[package]] name = "plotters" @@ -4290,7 +4333,7 @@ dependencies = [ "strum 0.26.3", "strum_macros 0.26.4", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tracing", "tracing-appender", @@ -4325,15 +4368,24 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.13.1" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] [[package]] name = "potential_utf" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" dependencies = [ "zerovec", ] @@ -4427,9 +4479,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] @@ -4446,7 +4498,7 @@ dependencies = [ "memchr", "parking_lot", "protobuf", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -4503,9 +4555,9 @@ dependencies = [ [[package]] name = "psm" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea" +checksum = "4dcd034599e63b970727f70d79e02d62390a4a84f7c6b827c27c46d5ac3fa622" dependencies = [ "ar_archive_writer", "cc", @@ -4555,7 +4607,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tracing", "web-time", @@ -4578,7 +4630,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.18", + "thiserror 2.0.20", "tinyvec", "tracing", "web-time", @@ -4600,9 +4652,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.46" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -4716,9 +4768,9 @@ dependencies = [ [[package]] name = "rangemap" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" +checksum = "a611d15b50743feb4c76b7d03edcb0e64f399c26961e4efe6975bc398be6aa3d" [[package]] name = "raw-cpuid" @@ -4726,7 +4778,7 @@ version = "11.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" dependencies = [ - "bitflags", + "bitflags 2.13.1", ] [[package]] @@ -4756,7 +4808,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" dependencies = [ "aws-lc-rs", - "pem", + "pem 3.0.6", "rustls-pki-types", "time", "yasna 0.5.2", @@ -4764,15 +4816,15 @@ dependencies = [ [[package]] name = "rcgen" -version = "0.14.8" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57f6d249aad744e274e682777a50283a225a32705394ee6d5fcc01efa25e4055" +checksum = "091e7a8e7d86e6feb87a27ce8e2cba29d49eff9507afeebefab7eeb2ca667fb4" dependencies = [ "aws-lc-rs", - "pem", + "pem 3.0.6", "rustls-pki-types", "time", - "x509-parser 0.18.1", + "x509-parser", "yasna 0.6.0", ] @@ -4782,7 +4834,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.13.1", ] [[package]] @@ -4804,27 +4856,27 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] name = "ref-cast" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -4856,9 +4908,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.16" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -4925,7 +4977,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams 0.4.2", "web-sys", - "webpki-roots 1.0.8", + "webpki-roots 1.0.9", ] [[package]] @@ -5088,7 +5140,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags", + "bitflags 2.13.1", "errno", "libc", "linux-raw-sys", @@ -5097,9 +5149,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.42" +version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "aws-lc-rs", "brotli", @@ -5115,9 +5167,9 @@ dependencies = [ [[package]] name = "rustls-acme" -version = "0.15.3" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9c70a17ecb067d5067565a16a2e0f26a4a2ea0924f49739d558c45186facc75" +checksum = "64f24daec4ed0dfe6327bcb2b8045bf1d1cb4835e4f824d4cf487140f4b0cee4" dependencies = [ "async-io", "async-trait", @@ -5130,13 +5182,13 @@ dependencies = [ "futures-rustls", "http", "log", - "pem", + "pem 3.0.6", "rcgen 0.13.2", "serde", "serde_json", - "thiserror 2.0.18", - "webpki-roots 1.0.8", - "x509-parser 0.16.0", + "thiserror 2.0.20", + "webpki-roots 1.0.9", + "x509-parser", ] [[package]] @@ -5162,9 +5214,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ "web-time", "zeroize", @@ -5199,9 +5251,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a" dependencies = [ "aws-lc-rs", "ring", @@ -5265,9 +5317,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" dependencies = [ "dyn-clone", "ref-cast", @@ -5313,7 +5365,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags", + "bitflags 2.13.1", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -5342,9 +5394,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -5372,22 +5424,22 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -5403,11 +5455,10 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ - "indexmap 2.14.0", "itoa", "memchr", "serde", @@ -5428,13 +5479,13 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -5460,9 +5511,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.21.0" +version = "3.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +checksum = "ee78f1fbe43ac4a0e47aadb3dbd357b69eb0d3793e948624cd03dd2750ab1c0a" dependencies = [ "base64 0.22.1", "bs58", @@ -5470,8 +5521,9 @@ dependencies = [ "hex", "indexmap 1.9.3", "indexmap 2.14.0", + "jiff", "schemars 0.9.0", - "schemars 1.2.1", + "schemars 1.2.2", "serde_core", "serde_json", "serde_with_macros", @@ -5480,9 +5532,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.21.0" +version = "3.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +checksum = "8705578779c2b6bd90d84d66eb2e206b708b1a4d7b9f17641b293545bf1c7e46" dependencies = [ "darling 0.23.0", "proc-macro2", @@ -5511,7 +5563,7 @@ checksum = "c2ff74d7e7d1cc172f3a45adec74fbeee928d71df095b85aaaf66eb84e1e31e6" dependencies = [ "base64 0.22.1", "bitfield", - "bitflags", + "bitflags 2.13.1", "byteorder", "dirs", "hex", @@ -5650,9 +5702,9 @@ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "similar" -version = "3.1.1" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6505efef05804732ed8a3f2d4f279429eb485bd69d5b0cc6b19cc02005cda16" +checksum = "4f66ca1f7aca2474dc10c942eb22feffc897735f54cd1db90138c2fddb490987" dependencies = [ "bstr", ] @@ -5698,9 +5750,9 @@ checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "smtp-proto" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4570e312141d42f0cb8d4e532fc8ec96c805b308fcb9012d973558cccd9dee85" +checksum = "e365d0fff6ae5036364ab62f236fa669ce05acbd5878df32d3755d48cfc1491b" [[package]] name = "snafu" @@ -5766,9 +5818,9 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stacker" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "640c8cdd92b6b12f5bcb1803ca3bbf5ab96e5e6b6b96b9ab77dabe9e880b3190" +checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967" dependencies = [ "cc", "cfg-if", @@ -5876,9 +5928,9 @@ checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" [[package]] name = "syn" -version = "1.0.109" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" dependencies = [ "proc-macro2", "quote", @@ -5887,9 +5939,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.119" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" dependencies = [ "proc-macro2", "quote", @@ -5922,7 +5974,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags", + "bitflags 2.13.1", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -6029,11 +6081,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl 2.0.20", ] [[package]] @@ -6049,13 +6101,13 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -6069,9 +6121,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.53" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", "num-conv", @@ -6089,9 +6141,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.31" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" dependencies = [ "num-conv", "time-core", @@ -6108,9 +6160,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" dependencies = [ "displaydoc", "zerovec", @@ -6164,9 +6216,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.52.3" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", @@ -6191,13 +6243,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -6212,9 +6264,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" dependencies = [ "futures-core", "pin-project-lite", @@ -6234,9 +6286,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", @@ -6244,6 +6296,7 @@ dependencies = [ "futures-sink", "futures-util", "hashbrown 0.15.5", + "libc", "pin-project-lite", "slab", "tokio", @@ -6283,35 +6336,6 @@ dependencies = [ "winnow", ] -[[package]] -name = "tonic" -version = "0.14.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" -dependencies = [ - "async-trait", - "axum", - "base64 0.22.1", - "bytes", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-timeout", - "hyper-util", - "percent-encoding", - "pin-project", - "socket2", - "sync_wrapper", - "tokio", - "tokio-stream", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "tower" version = "0.5.3" @@ -6320,12 +6344,9 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", - "indexmap 2.14.0", "pin-project-lite", - "slab", "sync_wrapper", "tokio", - "tokio-util", "tower-layer", "tower-service", "tracing", @@ -6338,7 +6359,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "async-compression", - "bitflags", + "bitflags 2.13.1", "bytes", "futures-core", "futures-util", @@ -6360,7 +6381,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b11f75e912b0c2be01b63d8cf8057b8c3f97cf34abb3d431a3a4c8675498e233" dependencies = [ - "bitflags", + "bitflags 2.13.1", "bytes", "http", "http-body", @@ -6383,23 +6404,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" -[[package]] -name = "tower_governor" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44de9b94d849d3c46e06a883d72d408c2de6403367b39df2b1c9d9e7b6736fe6" -dependencies = [ - "axum", - "forwarded-header-value", - "governor", - "http", - "pin-project", - "thiserror 2.0.18", - "tonic", - "tower", - "tracing", -] - [[package]] name = "tracing" version = "0.1.44" @@ -6420,7 +6424,7 @@ checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" dependencies = [ "crossbeam-channel", "symlink", - "thiserror 2.0.18", + "thiserror 2.0.20", "time", "tracing-subscriber", ] @@ -6497,9 +6501,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "twox-hash" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" +checksum = "8464ec13c3691491391d9fce00f6416c9a48e46972f72d7865688be2080192c9" [[package]] name = "typed-arena" @@ -6669,9 +6673,16 @@ dependencies = [ "serde_json", "url", "utoipa", + "utoipa-swagger-ui-vendored", "zip 3.0.0", ] +[[package]] +name = "utoipa-swagger-ui-vendored" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2eebbbfe4093922c2b6734d7c679ebfebd704a0d7e56dfcb0d05818ce28977d" + [[package]] name = "uuid" version = "1.12.1" @@ -6774,9 +6785,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" dependencies = [ "cfg-if", "once_cell", @@ -6787,9 +6798,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.76" +version = "0.4.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" dependencies = [ "js-sys", "wasm-bindgen", @@ -6797,9 +6808,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6807,9 +6818,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" dependencies = [ "bumpalo", "proc-macro2", @@ -6820,9 +6831,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" dependencies = [ "unicode-ident", ] @@ -6855,9 +6866,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.103" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" dependencies = [ "js-sys", "wasm-bindgen", @@ -6875,9 +6886,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" dependencies = [ "rustls-pki-types", ] @@ -6888,14 +6899,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.8", + "webpki-roots 1.0.9", ] [[package]] name = "webpki-roots" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" dependencies = [ "rustls-pki-types", ] @@ -7106,9 +7117,9 @@ checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "writeable" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "wslpath" @@ -7128,38 +7139,21 @@ dependencies = [ "tls_codec", ] -[[package]] -name = "x509-parser" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" -dependencies = [ - "asn1-rs 0.6.2", - "data-encoding", - "der-parser 9.0.0", - "lazy_static", - "nom 7.1.3", - "oid-registry 0.7.1", - "rusticata-macros", - "thiserror 1.0.69", - "time", -] - [[package]] name = "x509-parser" version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" dependencies = [ - "asn1-rs 0.7.2", + "asn1-rs", "aws-lc-rs", "data-encoding", - "der-parser 10.0.0", + "der-parser", "lazy_static", "nom 7.1.3", - "oid-registry 0.8.1", + "oid-registry", "rusticata-macros", - "thiserror 2.0.18", + "thiserror 2.0.20", "time", ] @@ -7217,18 +7211,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.54" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.54" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" dependencies = [ "proc-macro2", "quote", @@ -7278,9 +7272,9 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" dependencies = [ "displaydoc", "yoke", @@ -7289,9 +7283,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.6" +version = "0.11.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +checksum = "94b5c6b5976d66c1d703c4fd17d3f5e43c8cedaacf604961b171adc7130896d8" dependencies = [ "yoke", "zerofrom", @@ -7300,13 +7294,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +checksum = "47402523226a02bfe5230160dc3ccc089aa6f6f19e7fcbb4e6f824bbb1b4aa62" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -7338,9 +7332,9 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b142a20ec14a91d5bc708c1dc21b080c550113d8aa77afa29635673a65dd02c5" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" [[package]] name = "zmij" diff --git a/Cargo.toml b/Cargo.toml index ae819ce..117061a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,13 +17,13 @@ categories = [] ahash = "0.8.11" anyhow = "1.0.93" arc-swap = "1.7.1" -arrayvec = "0.7" +arrayvec = { version = "0.7", features = ["serde"] } async-channel = "2.3.1" async-trait = "0.1.83" axum = "0.8.1" axum-extra = { version = "0.12.6", features = ["middleware"] } axum-server = "0.8.0" -base64 = "0.22.1" +base64 = "0.23.1" bytes = "1.10.0" candid = "0.10.10" candid_parser = "0.4" @@ -69,24 +69,27 @@ ic-cdk-timers = "1.0" ic-custom-domains-canister-api = { version = "0.2", path = "./custom-domains/api" } ic-http-types = "0.1.0" ic-stable-structures = "0.7.0" +ic-transport-types = "0.49" ic-verify-bls-signature = "0.6" +ipnet = { version = "2", features = ["serde"] } instant-acme = { version = "0.8.5", default-features = false, features = [ "aws-lc-rs", "hyper-rustls", ] } itertools = "0.15.0" -mail-auth = "0.11.0" +mail-auth = "0.12.0" mail-parser = { version = "0.11.3", features = ["full_encoding"] } mail-send = { version = "0.6.0", default-features = false, features = [ "builder", ] } -mockall = "0.13.0" +maxminddb = "0.29" +mockall = "0.15" mock-io = { version = "0.3.2", features = ["full"] } moka = { version = "0.12.15", features = ["sync"] } nix = { version = "0.31.0", features = ["signal"] } ppp = "2.3.0" parse-size = { version = "1.1.0", features = ["std"] } -pem = "3.0.5" +pem = "4" pocket-ic = "=12.0.0" prometheus = "0.14.0" prost = { version = "0.14.1" } @@ -155,13 +158,12 @@ tokio-io-timeout = "1.2.0" tokio-test = "0.4" tokio-util = { version = "0.7.12", features = ["full"] } tower = { version = "0.5.1", features = ["util"] } -tower_governor = { version = "0.8" } tower-service = "0.3.3" tower-http = { version = "0.7", features = ["trace"] } tracing = { version = "0.1.40", features = ["attributes"] } url = "2.5.3" utoipa = { version = "5.4.0", features = ["axum_extras"] } -utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] } +utoipa-swagger-ui = { version = "9.0.2", features = ["axum", "vendored"] } # DO NOT upgrade, this breaks monorepo compatibility # Read https://github.com/uuid-rs/uuid/releases/tag/1.13.0 uuid = { version = "=1.12.1", features = ["v7", "serde"] } diff --git a/ic-bn-lib/Cargo.toml b/ic-bn-lib/Cargo.toml index 0a17243..1f0a8e4 100644 --- a/ic-bn-lib/Cargo.toml +++ b/ic-bn-lib/Cargo.toml @@ -27,7 +27,11 @@ acme-dns = ["acme"] acme-alpn = ["acme", "dep:rustls-acme"] clients-hyper = ["dep:hyper-rustls"] custom-domains = ["acme-dns"] -custom-domains-openapi = ["custom-domains", "dep:utoipa", "dep:utoipa-swagger-ui"] +custom-domains-openapi = [ + "custom-domains", + "dep:utoipa", + "dep:utoipa-swagger-ui", +] cert-providers = ["custom-domains"] lb = [] pubsub = [] @@ -68,11 +72,14 @@ hyper-rustls = { workspace = true, optional = true } hyper-util = { workspace = true } ic-agent = { workspace = true } ic-custom-domains-canister-api = { workspace = true } +ic-transport-types = { workspace = true } indoc = { workspace = true } instant-acme = { workspace = true, optional = true } +ipnet = { workspace = true } itertools = { workspace = true } mail-auth = { workspace = true, optional = true } mail-parser = { workspace = true, optional = true } +maxminddb = { workspace = true } moka = { workspace = true } nix = { workspace = true } parse-size = { workspace = true } @@ -112,7 +119,6 @@ tokio-util = { workspace = true } tokio-rustls = { workspace = true } tokio-io-timeout = { workspace = true } tower = { workspace = true } -tower_governor = { workspace = true } tower-service = { workspace = true } tower-http = { workspace = true } tracing = { workspace = true } diff --git a/ic-bn-lib/src/custom_domains/backend/router.rs b/ic-bn-lib/src/custom_domains/backend/router.rs index f298fdf..e3b3a53 100644 --- a/ic-bn-lib/src/custom_domains/backend/router.rs +++ b/ic-bn-lib/src/custom_domains/backend/router.rs @@ -120,7 +120,7 @@ pub fn create_router( #[cfg(test)] mod tests { - use std::{str::FromStr, sync::Arc}; + use std::{net::IpAddr, str::FromStr, sync::Arc}; use axum::{ body::{Body, to_bytes}, @@ -132,18 +132,21 @@ mod tests { use serde_json::Value; use tower::{Service, util::ServiceExt}; - use crate::custom_domains::{ - backend::router::{RateLimitConfig, create_router}, - base::{ - traits::{ - repository::{MockRepository, RepositoryError}, - validation::{MockValidatesDomains, ValidationError}, - }, - types::{ - domain::{DomainStatus, RegistrationStatus}, - task::{InputTask, TaskKind}, + use crate::{ + custom_domains::{ + backend::router::{RateLimitConfig, create_router}, + base::{ + traits::{ + repository::{MockRepository, RepositoryError}, + validation::{MockValidatesDomains, ValidationError}, + }, + types::{ + domain::{DomainStatus, RegistrationStatus}, + task::{InputTask, TaskKind}, + }, }, }, + http::middleware::RemoteAddr, }; const BODY_LIMIT: usize = 5000; @@ -1028,7 +1031,7 @@ mod tests { let router = create_test_router_with_rate_limiter(mock_repository, mock_validator, rate_limits); - let test_ip = "192.168.1.100"; + let test_ip = IpAddr::from_str("192.168.1.100").unwrap(); // Create a service that maintains state between calls (needed for rate limiting) let mut service = router.into_service(); @@ -1036,14 +1039,14 @@ mod tests { let request1 = Request::builder() .method("POST") .uri("/v1/example1.org") - .header("x-real-ip", test_ip) + .extension(RemoteAddr(test_ip)) .body(Body::empty()) .unwrap(); let request2 = Request::builder() .method("POST") .uri("/v1/example2.org") - .header("x-real-ip", test_ip) + .extension(RemoteAddr(test_ip)) .body(Body::empty()) .unwrap(); @@ -1051,7 +1054,7 @@ mod tests { let request3 = Request::builder() .method("POST") .uri("/v1/example3.org") - .header("x-real-ip", test_ip) + .extension(RemoteAddr(test_ip)) .body(Body::empty()) .unwrap(); diff --git a/ic-bn-lib/src/custom_domains/base/types/worker.rs b/ic-bn-lib/src/custom_domains/base/types/worker.rs index 45fc74a..1aabca8 100644 --- a/ic-bn-lib/src/custom_domains/base/types/worker.rs +++ b/ic-bn-lib/src/custom_domains/base/types/worker.rs @@ -28,6 +28,7 @@ use tracing::{Span, debug, error, info, instrument, warn}; use x509_parser::{parse_x509_certificate, prelude::GeneralName}; use crate::{ + DurationDisplay, custom_domains::base::{ helpers::{format_error_chain, retry_async}, traits::{repository::Repository, time::UtcTimestamp, validation::ValidatesDomains}, @@ -185,7 +186,7 @@ impl Worker { info!("Certificate revocation starts now"); } else { info!( - delay_secs = delay.as_secs(), + delay = %delay.display(), "Certificate revocation scheduled" ); } @@ -304,7 +305,7 @@ impl Worker { }; info!( - duration = task_result.duration.as_secs(), + duration = %task_result.duration.display(), not_before = validity.as_ref().map(|x| &x.0), not_after = validity.as_ref().map(|x| &x.1), "Task execution succeeded" @@ -313,7 +314,7 @@ impl Worker { TaskOutcome::Failure(err) => { error!( - duration = task_result.duration.as_secs(), + duration = %task_result.duration.display(), error = ?err, "Task execution failed" ); @@ -410,7 +411,7 @@ impl Worker { Err(err) => { error!( error = ?err, - duration_secs = self.config.task_fetch_retry_interval.as_secs(), + duration = %self.config.task_fetch_retry_interval.display(), "Failed to fetch pending task, sleeping before retry" ); self.shared_metrics @@ -517,7 +518,7 @@ impl Worker { let attempts = err.attempts.to_string(); error!( - duration_secs = self.config.task_submit_timeout.as_secs(), + duration = %self.config.task_submit_timeout.display(), "Failed to submit task result after {attempts} attempts: {err:?}", ); @@ -544,7 +545,7 @@ impl Worker { /// Handles no available tasks, returning whether the worker should continue running async fn handle_no_tasks(&self) -> Result<(), WorkerStopped> { debug!( - duration_secs = self.config.polling_interval_no_tasks.as_secs(), + duration = %self.config.polling_interval_no_tasks.display(), "No pending tasks found, sleeping" ); diff --git a/ic-bn-lib/src/custom_domains/client.rs b/ic-bn-lib/src/custom_domains/client.rs index ae7a171..6123c1e 100644 --- a/ic-bn-lib/src/custom_domains/client.rs +++ b/ic-bn-lib/src/custom_domains/client.rs @@ -32,6 +32,7 @@ use tokio_util::sync::CancellationToken; use tracing::{info, instrument, warn}; use crate::{ + DurationDisplay, custom_domains::{ CustomDomain, ProvidesCustomDomains, base::{ @@ -49,8 +50,7 @@ use crate::{ }, ic_agent::Agent, tasks::Run, - tls::Pem, - tls::ProvidesCertificates, + tls::{Pem, ProvidesCertificates}, }; #[derive(new)] @@ -426,9 +426,9 @@ impl Run for CanisterClient { interval_refresh.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); warn!( - "Started polling every {}s, full refresh every {}s", - self.poll_interval.as_secs_f64(), - self.refresh_interval.as_secs_f64() + "Started polling every {}, full refresh every {}", + self.poll_interval.display(), + self.refresh_interval.display() ); loop { diff --git a/ic-bn-lib/src/http/cache.rs b/ic-bn-lib/src/http/cache.rs index 4e120df..a23b5a1 100644 --- a/ic-bn-lib/src/http/cache.rs +++ b/ic-bn-lib/src/http/cache.rs @@ -1391,7 +1391,7 @@ mod tests { assert_eq!(cache.len(), 4); } - #[tokio::test] + #[tokio::test(start_paused = true)] async fn test_proxy_cache_lock() { let cache = Arc::new( CacheBuilder::new(KeyExtractorTest) diff --git a/ic-bn-lib/src/http/middleware/mod.rs b/ic-bn-lib/src/http/middleware/mod.rs index b8a2a17..52cc909 100644 --- a/ic-bn-lib/src/http/middleware/mod.rs +++ b/ic-bn-lib/src/http/middleware/mod.rs @@ -1,65 +1,5 @@ pub mod rate_limiter; +pub mod request_meta; pub mod waf; -use std::{net::IpAddr, str::FromStr, sync::Arc}; - -use http::Request; - -use crate::http::{headers::X_REAL_IP, server::conn::ConnInfo}; - -/// Extracts IP address from `x-real-ip` header or `ConnInfo` extension -pub fn extract_ip_from_request(req: &Request) -> Option { - // Try to extract from the header first - req.headers() - .get(X_REAL_IP) - .and_then(|x| x.to_str().ok()) - .and_then(|x| IpAddr::from_str(x).ok()) - .or_else(|| { - // Then, if that failed, from the ConnInfo extension - req.extensions() - .get::>() - .map(|x| x.remote_addr.ip()) - }) -} - -#[cfg(test)] -mod test { - use std::net::SocketAddr; - - use crate::network::Addr; - - use super::*; - - #[test] - fn test_extract_ip_from_request() { - let addr1 = IpAddr::from_str("10.0.0.1").unwrap(); - let addr2 = IpAddr::from_str("192.168.0.1").unwrap(); - - let mut ci = ConnInfo::default(); - ci.remote_addr = Addr::Tcp(SocketAddr::new(addr1, 31337)); - let ci = Arc::new(ci); - - // Header takes precedence - let req = Request::builder() - .extension(ci.clone()) - .header(X_REAL_IP, addr2.to_string()) - .body("") - .unwrap(); - assert_eq!(extract_ip_from_request(&req), Some(addr2)); - - // Only ConnInfo - let req = Request::builder().extension(ci).body("").unwrap(); - assert_eq!(extract_ip_from_request(&req), Some(addr1)); - - // Only header - let req = Request::builder() - .header(X_REAL_IP, addr2.to_string()) - .body("") - .unwrap(); - assert_eq!(extract_ip_from_request(&req), Some(addr2)); - - // Neither - let req = Request::builder().body("").unwrap(); - assert_eq!(extract_ip_from_request(&req), None); - } -} +pub use request_meta::{RemoteAddr, RequestId}; diff --git a/ic-bn-lib/src/http/middleware/rate_limiter.rs b/ic-bn-lib/src/http/middleware/rate_limiter.rs index 69810ca..b3fabd8 100644 --- a/ic-bn-lib/src/http/middleware/rate_limiter.rs +++ b/ic-bn-lib/src/http/middleware/rate_limiter.rs @@ -1,44 +1,50 @@ use std::{ + future::ready, + hash::Hash, net::IpAddr, + num::NonZeroU32, sync::Arc, task::{Context, Poll}, time::Duration, }; -use ::governor::{clock::QuantaInstant, middleware::NoOpMiddleware}; use anyhow::{Error, anyhow}; -use axum::{body::Body, extract::Request, response::IntoResponse, response::Response}; +use arc_swap::ArcSwap; +use axum::{extract::Request, response::IntoResponse, response::Response}; use bytes::Bytes; use futures::future::BoxFuture; -use http::{HeaderName, HeaderValue, StatusCode}; -use tower::{Layer, Service}; -use tower_governor::{ - GovernorError, GovernorLayer, - governor::{Governor, GovernorConfig, GovernorConfigBuilder}, - key_extractor::{GlobalKeyExtractor, KeyExtractor}, +use governor::{ + Quota, + clock::{Clock, DefaultClock, Reference}, + middleware::NoOpMiddleware, + nanos::Nanos, + state::keyed::DashMapStateStore, }; +use http::{HeaderName, HeaderValue, StatusCode, header::RETRY_AFTER}; +use tower::{Layer, Service}; -use crate::{hname, http::middleware::extract_ip_from_request}; +use crate::{constant_time_eq, hname, http::middleware::RemoteAddr}; -pub type GovernorLayerAxum = GovernorLayer, Body>; +const BYPASS_TOKEN_HEADER: HeaderName = hname!("x-ratelimit-bypass-token"); -const BYPASS_HEADER: HeaderName = hname!("x-ratelimit-bypass-token"); +/// The `governor` rate limiter type that backs this middleware, generic over the clock so +/// that tests can inject `governor::clock::FakeRelativeClock` instead of the real-time +/// default clock. +type GovRateLimiter = + governor::RateLimiter, C, NoOpMiddleware<::Instant>>; -/// Constant-time comparison of the bypass header against the configured -/// token, so that a network timing side-channel can't be used to recover the -/// token byte-by-byte. -fn bypass_token_matches(hdr: &HeaderValue, token: &str) -> bool { - let a = hdr.as_bytes(); - let b = token.as_bytes(); +/// Converts a `governor` wait-time into a whole-second `Retry-After` value. Rounds down, but +/// never advertises less than 1 second. +fn retry_after_seconds(wait_time: Duration) -> u32 { + std::cmp::max(wait_time.as_secs(), 1) as u32 +} - if a.len() != b.len() { - return false; - } +/// Extracts a rate-limiting key from the request. Returns `None` if a key cannot be +/// determined, in which case the request is rejected rather than let through unlimited. +pub trait KeyExtractor: Clone + Send + Sync + 'static { + type Key: Clone + Eq + Hash + Send + Sync + 'static; - a.iter() - .zip(b.iter()) - .fold(0u8, |acc, (x, y)| acc | (x ^ y)) - == 0 + fn extract(&self, req: &Request) -> Option; } /// Extracts an IP from the request as a rate-limiting key @@ -48,25 +54,47 @@ pub struct IpKeyExtractor; impl KeyExtractor for IpKeyExtractor { type Key = IpAddr; - fn extract(&self, req: &Request) -> Result { - extract_ip_from_request(req).ok_or(GovernorError::UnableToExtractKey) + fn extract(&self, req: &Request) -> Option { + req.extensions().get::().map(|x| x.0) } } -/// Ratelimiter that implements Tower Service +/// Extracts a constant key so that all requests share a single rate-limiting bucket #[derive(Clone)] -pub struct RateLimiter { - governor: Governor, S, Body>, +pub struct GlobalKeyExtractor; + +impl KeyExtractor for GlobalKeyExtractor { + type Key = (); + + fn extract(&self, _req: &Request) -> Option { + Some(()) + } +} + +struct RateLimiterState { + key_extractor: K, + limiter: GovRateLimiter, + rate_limited_response: R, bypass_token: Option, + last_cleanup: ArcSwap, +} + +/// Ratelimiter that implements Tower Service +#[derive(Clone)] +pub struct RateLimiter { + state: Arc>, inner: S, } /// Implement Tower Service for RateLimiter -impl Service for RateLimiter +impl Service for RateLimiter where S: Service + Send + 'static, S::Future: Send + 'static, + S::Error: Send + 'static, K: KeyExtractor, + R: IntoResponse + Clone + Send + Sync + 'static, + C: Clock + Send + Sync + 'static, { type Response = S::Response; type Error = S::Error; @@ -77,13 +105,22 @@ where } fn call(&mut self, request: Request) -> Self::Future { + /// Stale entries cleanup interval - 5 minutes + const CLEANUP_INTERVAL: Nanos = Nanos::new(300_000_000_000); + // Check that bypass token is configured, header was sent and it matches let bypass = request .headers() - .get(BYPASS_HEADER) - .zip(self.bypass_token.as_ref()) - .map(|(hdr, token)| bypass_token_matches(hdr, token)) - == Some(true); + .get(BYPASS_TOKEN_HEADER) + .zip(self.state.bypass_token.as_ref()) + .is_some_and(|(hdr, token)| constant_time_eq(hdr.as_bytes(), token.as_bytes())); + + // Clean up stale entries from time to time + let now = self.state.limiter.clock().now(); + if now.duration_since(*self.state.last_cleanup.load_full()) > CLEANUP_INTERVAL { + self.state.last_cleanup.store(Arc::new(now)); + self.state.limiter.retain_recent(); + } // If bypassing - call the wrapped service directly if bypass { @@ -91,61 +128,54 @@ where return Box::pin(fut); } - // Otherwise go through Governor - let fut = self.governor.call(request); - Box::pin(fut) + // Fail if we can't determine a rate-limiting key for the request + let Some(key) = self.state.key_extractor.extract(&request) else { + let response = ( + StatusCode::INTERNAL_SERVER_ERROR, + "Unable to extract rate limiting key", + ) + .into_response(); + + return Box::pin(ready(Ok(response))); + }; + + match self.state.limiter.check_key(&key) { + Ok(()) => Box::pin(self.inner.call(request)), + + Err(not_until) => { + let wait_time = not_until.wait_time_from(self.state.limiter.clock().now()); + let retry_secs = retry_after_seconds(wait_time); + + let mut response = self.state.rate_limited_response.clone().into_response(); + let header_value = + HeaderValue::from_maybe_shared(Bytes::from(retry_secs.to_string())).unwrap(); + response.headers_mut().insert(RETRY_AFTER, header_value); + + Box::pin(ready(Ok(response))) + } + } } } /// Layer usable as an Axum middleware #[derive(Clone, derive_new::new)] -pub struct RateLimiterLayer { - config: Arc>>, - rate_limited_response: R, - bypass_token: Option, +pub struct RateLimiterLayer { + state: Arc>, } -impl Layer for RateLimiterLayer +impl Layer for RateLimiterLayer where S: Clone, K: KeyExtractor, R: IntoResponse + Clone + Send + Sync + 'static, + C: Clock + Send + Sync + 'static, { - type Service = RateLimiter; + type Service = RateLimiter; fn layer(&self, inner: S) -> Self::Service { - let rate_limited_response = self.rate_limited_response.clone(); - - let governor = Governor::new(inner.clone(), &self.config).error_handler(move |err| { - match err { - GovernorError::TooManyRequests { wait_time, headers: _ } => { - let mut response = rate_limited_response.clone().into_response(); - // Add Retry-After header using timing from governor - // wait_time is in milliseconds, convert to seconds (minimum 1 second) - let retry_secs = ((wait_time / 1000).max(1)) as u32; - let header_value = HeaderValue::from_maybe_shared(Bytes::from(retry_secs.to_string())).unwrap(); - response.headers_mut().insert(http::header::RETRY_AFTER, header_value); - response - }, - GovernorError::UnableToExtractKey => ( - StatusCode::INTERNAL_SERVER_ERROR, - "Unable to extract rate limiting key", - ) - .into_response(), - GovernorError::Other { code, msg, headers } => ( - StatusCode::INTERNAL_SERVER_ERROR, - format!( - "Rate limiter failed unexpectedly: code={code}, msg={msg:?}, headers={headers:?}" - ), - ) - .into_response() - } - }); - RateLimiter { - governor, - bypass_token: self.bypass_token.clone(), inner, + state: self.state.clone(), } } } @@ -190,28 +220,53 @@ pub fn layer( rate_limited_response: R, bypass_token: Option, ) -> Result, Error> { + layer_with_clock( + rps, + burst_size, + key_extractor, + rate_limited_response, + bypass_token, + DefaultClock::default(), + ) +} + +/// Create a ratelimiter with a provided key extractor and clock. This custom clock is there so +/// that tests can supply a `FakeRelativeClock` and drive the rate limiter's +/// time deterministically, without depending on real wall-clock delays. +fn layer_with_clock( + rps: u32, + burst_size: u32, + key_extractor: K, + rate_limited_response: R, + bypass_token: Option, + clock: C, +) -> Result, Error> { let period = Duration::from_secs(1) .checked_div(rps) .ok_or_else(|| anyhow!("RPS is zero"))?; - let config = GovernorConfigBuilder::default() - .period(period) - .burst_size(burst_size) - .key_extractor(key_extractor) - .finish() - .ok_or_else(|| anyhow!("unable to build governor config"))?; + let burst = NonZeroU32::new(burst_size).ok_or_else(|| anyhow!("burst size is zero"))?; - Ok(RateLimiterLayer::new( - Arc::new(config), - rate_limited_response, - bypass_token, - )) + let quota = Quota::with_period(period) + .ok_or_else(|| anyhow!("period is zero"))? + .allow_burst(burst); + + let limiter = GovRateLimiter::::dashmap_with_clock(quota, clock); + let last_cleanup = ArcSwap::new(limiter.clock().now().into()); + + Ok(RateLimiterLayer { + state: Arc::new(RateLimiterState { + key_extractor, + limiter, + rate_limited_response, + bypass_token, + last_cleanup, + }), + }) } #[cfg(test)] mod test { - use crate::http::server::conn::ConnInfo; - use super::*; use axum::{ @@ -221,9 +276,9 @@ mod test { response::IntoResponse, routing::post, }; + use governor::clock::FakeRelativeClock; use http::{Method, StatusCode}; - use std::{sync::Arc, time::Duration}; - use tokio::time::sleep; + use std::str::FromStr; use tower::Service; async fn handler(_request: Request) -> impl IntoResponse { @@ -232,24 +287,83 @@ mod test { async fn send_request( router: &mut Router, + ip: &str, ) -> Result, std::convert::Infallible> { - let conn_info = ConnInfo::default(); let mut request = Request::post("/").body(Body::from("".to_string())).unwrap(); - request.extensions_mut().insert(Arc::new(conn_info)); + request + .extensions_mut() + .insert(RemoteAddr(IpAddr::from_str(ip).unwrap())); router.call(request).await } - #[tokio::test] + fn retry_after_header(response: &http::Response) -> u32 { + response + .headers() + .get(http::header::RETRY_AFTER) + .expect("Retry-After header missing on 429 response") + .to_str() + .unwrap() + .parse() + .unwrap() + } + + #[test] + fn test_retry_after_seconds() { + // Rounds down, but never below 1 second + assert_eq!(retry_after_seconds(Duration::from_millis(0)), 1); + assert_eq!(retry_after_seconds(Duration::from_millis(500)), 1); + assert_eq!(retry_after_seconds(Duration::from_millis(999)), 1); + assert_eq!(retry_after_seconds(Duration::from_millis(1000)), 1); + assert_eq!(retry_after_seconds(Duration::from_millis(1999)), 1); + assert_eq!(retry_after_seconds(Duration::from_millis(2000)), 2); + assert_eq!(retry_after_seconds(Duration::from_millis(2999)), 2); + assert_eq!(retry_after_seconds(Duration::from_secs(10)), 10); + } + + #[test] + fn test_layer_rejects_zero_rps() { + assert!( + layer( + 0, + 5, + IpKeyExtractor, + (StatusCode::TOO_MANY_REQUESTS, "foo"), + None, + ) + .is_err() + ); + } + + #[test] + fn test_layer_rejects_zero_burst_size() { + assert!( + layer( + 5, + 0, + IpKeyExtractor, + (StatusCode::TOO_MANY_REQUESTS, "foo"), + None, + ) + .is_err() + ); + } + + // Uses a `FakeRelativeClock` so token refills are driven by explicit + // advances rather than sleeps + #[tokio::test(start_paused = true)] async fn test_rate_limiter_rps_limit() { let rps = 5; let burst_size = 5; // how many requests can go through at once (without delay) + let period = Duration::from_secs(1) / rps; // time for one token to refill - let rate_limiter_mw = layer( + let clock = FakeRelativeClock::default(); + let rate_limiter_mw = layer_with_clock( rps, burst_size, IpKeyExtractor, (StatusCode::TOO_MANY_REQUESTS, "foo"), None, + clock.clone(), ) .expect("failed to build middleware"); @@ -257,66 +371,122 @@ mod test { .route("/", post(handler)) .layer(rate_limiter_mw); - // Test cases: (delay_ms, expected_status) - let delay_for_token_ms = 230; // when a token should become available ~ 1000ms/rps=200ms (we add some delta=30 ms to avoid flakiness) - let test_cases = vec![ - // Initial burst of 5 requests should succeed and fills full burst capacity - (0, StatusCode::OK), - (0, StatusCode::OK), - (0, StatusCode::OK), - (0, StatusCode::OK), - (0, StatusCode::OK), - // For 6th request no tokens left => 429 - (0, StatusCode::TOO_MANY_REQUESTS), - // Wait for 1 token to be available - (delay_for_token_ms, StatusCode::OK), - // Bucket is empty again, request should fail - (0, StatusCode::TOO_MANY_REQUESTS), - // Wait for 2 tokens to be available, next 2 requests succeed - (2 * delay_for_token_ms, StatusCode::OK), - (0, StatusCode::OK), - // Bucket is empty again, request should fail - (0, StatusCode::TOO_MANY_REQUESTS), - // Wait for 5 tokens, next 5 requests succeed - (5 * delay_for_token_ms, StatusCode::OK), - (0, StatusCode::OK), - (0, StatusCode::OK), - (0, StatusCode::OK), - (0, StatusCode::OK), - // Bucket is empty again, requests should fail - (0, StatusCode::TOO_MANY_REQUESTS), - (0, StatusCode::TOO_MANY_REQUESTS), - ]; - - // Execute all tests - for (idx, (delay_ms, expected_status)) in test_cases.into_iter().enumerate() { - if delay_ms > 0 { - sleep(Duration::from_millis(delay_ms)).await; - } - let result = send_request(&mut app).await.unwrap(); - assert_eq!(result.status(), expected_status, "test {idx} failed"); - - // Verify Retry-After header is present on rate-limited responses - if expected_status == StatusCode::TOO_MANY_REQUESTS { - let retry_after = result.headers().get(http::header::RETRY_AFTER); - assert!( - retry_after.is_some(), - "test {idx}: Retry-After header missing on 429 response" - ); - - // Verify the header value is a valid number and reasonable (between 1 and 10 seconds) - if let Some(header_value) = retry_after { - let retry_secs: u32 = header_value.to_str().unwrap().parse().unwrap(); - assert!( - (1..=10).contains(&retry_secs), - "test {idx}: Retry-After value {retry_secs} is outside expected range [1, 10]" - ); - } - } + // Initial burst of 5 requests should succeed and fills full burst capacity + for _ in 0..5 { + let result = send_request(&mut app, "1.1.1.1").await.unwrap(); + assert_eq!(result.status(), StatusCode::OK); + } + + // For 6th request no tokens left => 429 + let result = send_request(&mut app, "1.1.1.1").await.unwrap(); + assert_eq!(result.status(), StatusCode::TOO_MANY_REQUESTS); + assert_eq!(retry_after_header(&result), 1); + + // Advance the fake clock by exactly one token's worth of time (no real waiting) + clock.advance(period); + let result = send_request(&mut app, "1.1.1.1").await.unwrap(); + assert_eq!(result.status(), StatusCode::OK); + + // Bucket is empty again, request should fail + let result = send_request(&mut app, "1.1.1.1").await.unwrap(); + assert_eq!(result.status(), StatusCode::TOO_MANY_REQUESTS); + + // Advance for 2 tokens to be available, next 2 requests succeed + clock.advance(2 * period); + for _ in 0..2 { + let result = send_request(&mut app, "1.1.1.1").await.unwrap(); + assert_eq!(result.status(), StatusCode::OK); + } + + // Bucket is empty again, request should fail + let result = send_request(&mut app, "1.1.1.1").await.unwrap(); + assert_eq!(result.status(), StatusCode::TOO_MANY_REQUESTS); + + // Advance for 5 tokens (full burst), next 5 requests succeed + clock.advance(5 * period); + for _ in 0..5 { + let result = send_request(&mut app, "1.1.1.1").await.unwrap(); + assert_eq!(result.status(), StatusCode::OK); + } + + // Bucket is empty again, requests should fail + for _ in 0..2 { + let result = send_request(&mut app, "1.1.1.1").await.unwrap(); + assert_eq!(result.status(), StatusCode::TOO_MANY_REQUESTS); } } - #[tokio::test] + #[tokio::test(start_paused = true)] + async fn test_rate_limiter_retry_after_decreases_as_clock_advances() { + let clock = FakeRelativeClock::default(); + let rate_limiter_mw = layer_with_clock( + 1, + 1, + IpKeyExtractor, + (StatusCode::TOO_MANY_REQUESTS, "foo"), + None, + clock.clone(), + ) + .expect("failed to build middleware"); + + let mut app = Router::new() + .route("/", post(handler)) + .layer(rate_limiter_mw); + + // Consume the only token + let result = send_request(&mut app, "1.1.1.1").await.unwrap(); + assert_eq!(result.status(), StatusCode::OK); + + // Full period (1s) remaining + let result = send_request(&mut app, "1.1.1.1").await.unwrap(); + assert_eq!(result.status(), StatusCode::TOO_MANY_REQUESTS); + assert_eq!(retry_after_header(&result), 1); + + // Once the period has fully elapsed the token is available again + clock.advance(Duration::from_secs(1)); + let result = send_request(&mut app, "1.1.1.1").await.unwrap(); + assert_eq!(result.status(), StatusCode::OK); + } + + #[tokio::test(start_paused = true)] + async fn test_rate_limiter_per_key_independence() { + let clock = FakeRelativeClock::default(); + let rate_limiter_mw = layer_with_clock( + 1, + 1, + IpKeyExtractor, + (StatusCode::TOO_MANY_REQUESTS, "foo"), + None, + clock.clone(), + ) + .expect("failed to build middleware"); + + let mut app = Router::new() + .route("/", post(handler)) + .layer(rate_limiter_mw); + + // IP A consumes its only token and gets rate-limited + assert_eq!( + send_request(&mut app, "1.1.1.1").await.unwrap().status(), + StatusCode::OK + ); + assert_eq!( + send_request(&mut app, "1.1.1.1").await.unwrap().status(), + StatusCode::TOO_MANY_REQUESTS + ); + + // IP B has its own independent bucket and is unaffected + assert_eq!( + send_request(&mut app, "2.2.2.2").await.unwrap().status(), + StatusCode::OK + ); + assert_eq!( + send_request(&mut app, "2.2.2.2").await.unwrap().status(), + StatusCode::TOO_MANY_REQUESTS + ); + } + + #[tokio::test(start_paused = true)] async fn test_rate_limiter_returns_server_error() { let rps = 1; let burst_size = 1; @@ -343,26 +513,7 @@ mod test { assert_eq!(body, b"Unable to extract rate limiting key"); } - #[test] - fn test_bypass_token_matches() { - let token = "top_secret_token"; - assert!(bypass_token_matches( - &HeaderValue::from_static("top_secret_token"), - token - )); - assert!(!bypass_token_matches( - &HeaderValue::from_static("not_very_secret"), - token - )); - // Different length must not match either, and must not panic. - assert!(!bypass_token_matches( - &HeaderValue::from_static("short"), - token - )); - assert!(!bypass_token_matches(&HeaderValue::from_static(""), token)); - } - - #[tokio::test] + #[tokio::test(start_paused = true)] async fn test_rate_limiter_bypass_token() { let rate_limiter_mw = layer( 1, @@ -401,7 +552,7 @@ mod test { for _ in 0..100 { let req = Request::builder() .method(Method::POST) - .header(BYPASS_HEADER, "top_secret_token") + .header(BYPASS_TOKEN_HEADER, "top_secret_token") .body(Body::empty()) .unwrap(); let res = app.call(req).await.unwrap(); @@ -412,11 +563,96 @@ mod test { for _ in 0..100 { let req = Request::builder() .method(Method::POST) - .header(BYPASS_HEADER, "not_very_secret") + .header(BYPASS_TOKEN_HEADER, "not_very_secret") .body(Body::empty()) .unwrap(); let res = app.call(req).await.unwrap(); assert_eq!(res.status(), StatusCode::TOO_MANY_REQUESTS); } } + + #[tokio::test(start_paused = true)] + async fn test_rate_limiter_cleans_up_stale_entries() { + let clock = FakeRelativeClock::default(); + let rate_limiter_mw = layer_with_clock( + 1, + 1, + IpKeyExtractor, + (StatusCode::TOO_MANY_REQUESTS, "foo"), + None, + clock.clone(), + ) + .expect("failed to build middleware"); + + // Keep a handle to the underlying governor limiter so we can observe how many + // keys it's tracking, independently of the middleware wrapping it. + let state = rate_limiter_mw.state.clone(); + let limiter = &state.limiter; + + let mut app = Router::new() + .route("/", post(handler)) + .layer(rate_limiter_mw); + + // Two distinct keys get tracked in the limiter's state + send_request(&mut app, "1.1.1.1").await.unwrap(); + send_request(&mut app, "2.2.2.2").await.unwrap(); + assert_eq!(limiter.len(), 2); + + // Advancing by less than the cleanup interval (10 minutes) leaves stale + // entries in place, even though their buckets have long since refilled. + clock.advance(Duration::from_secs(60)); + send_request(&mut app, "3.3.3.3").await.unwrap(); + assert_eq!(limiter.len(), 3); + + // Once the cleanup interval has elapsed, the next request triggers a sweep + // that drops entries indistinguishable from a fresh bucket -- i.e. all three + // previously-seen keys -- before the new key is inserted. + clock.advance(Duration::from_secs(600)); + send_request(&mut app, "4.4.4.4").await.unwrap(); + assert_eq!(limiter.len(), 1); + } + + #[tokio::test(start_paused = true)] + async fn test_layer_global_wrapper() { + let rate_limiter_mw = + layer_global(2, 2, (StatusCode::TOO_MANY_REQUESTS, "foo"), None).unwrap(); + + let mut app = Router::new() + .route("/", post(handler)) + .layer(rate_limiter_mw); + + for _ in 0..2 { + let req = Request::builder() + .method(Method::POST) + .body(Body::empty()) + .unwrap(); + let res = app.call(req).await.unwrap(); + assert_eq!(res.status(), StatusCode::OK); + } + + let req = Request::builder() + .method(Method::POST) + .body(Body::empty()) + .unwrap(); + let res = app.call(req).await.unwrap(); + assert_eq!(res.status(), StatusCode::TOO_MANY_REQUESTS); + } + + #[tokio::test(start_paused = true)] + async fn test_layer_by_ip_wrapper() { + let rate_limiter_mw = + layer_by_ip(2, 2, (StatusCode::TOO_MANY_REQUESTS, "foo"), None).unwrap(); + + let mut app = Router::new() + .route("/", post(handler)) + .layer(rate_limiter_mw); + + for _ in 0..2 { + let result = send_request(&mut app, "1.1.1.1").await.unwrap(); + assert_eq!(result.status(), StatusCode::OK); + } + + let result = send_request(&mut app, "1.1.1.1").await.unwrap(); + assert_eq!(result.status(), StatusCode::TOO_MANY_REQUESTS); + } } diff --git a/ic-bn-lib/src/http/middleware/request_meta.rs b/ic-bn-lib/src/http/middleware/request_meta.rs new file mode 100644 index 0000000..852d775 --- /dev/null +++ b/ic-bn-lib/src/http/middleware/request_meta.rs @@ -0,0 +1,695 @@ +use std::{ + fmt::Display, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + ops::Deref, + path::PathBuf, + str::FromStr, + sync::Arc, +}; + +use anyhow::Context; +use arrayvec::ArrayString; +use axum::{ + extract::{Request, State}, + middleware::Next, + response::Response, +}; +use bytes::Bytes; +use http::{HeaderMap, header::HeaderValue}; +use ipnet::IpNet; +use maxminddb::geoip2; +use serde::{Deserialize, Serialize}; + +use crate::{ + Error, + http::{ + headers::{X_REAL_IP, X_REQUEST_ID}, + server::conn::ConnInfo, + }, + uuid::Uuid, +}; + +/// Subnet list that covers the whole IPv4+IPv6 address space +pub const ALL_NETWORKS: [IpNet; 2] = [ + IpNet::new_assert(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0), + IpNet::new_assert(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0), +]; + +/// Client address +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct RemoteAddr(pub IpAddr); + +impl Deref for RemoteAddr { + type Target = IpAddr; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Display for RemoteAddr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +/// Request ID (UUID) +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct RequestId(pub Uuid); + +impl Deref for RequestId { + type Target = Uuid; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Display for RequestId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +/// Two-letter country code. +/// See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)] +pub struct CountryCode(pub ArrayString<2>); + +impl Deref for CountryCode { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.0.as_str() + } +} + +impl Display for CountryCode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +/// Looks up the client's country using his IP address +pub struct GeoIp { + db: maxminddb::Reader>, +} + +impl GeoIp { + /// Creates a new GeoIp instance from a provided database + pub fn new(db_path: &PathBuf) -> Result { + Ok(Self { + db: maxminddb::Reader::open_readfile(db_path).context("unable to load GeoIP DB")?, + }) + } + + /// Looks up the country code from an IP + pub fn lookup(&self, ip: IpAddr) -> Option { + let country: Option = self.db.lookup(ip).ok()?.decode().ok()?; + // Country code should always fit into 2-letter ArrayString. + // If for whatever reason it does not - return None. + Some(CountryCode(country?.country.iso_code?.try_into().ok()?)) + } +} + +/// State for [`middleware`] +pub struct RequestMetaState { + /// Optional GeoIP database + geoip: Option, + + /// Trust incoming headers from these subnets for the purpose of IP address extraction. + /// If not set - headers will not be used. + trust_ip_from: Vec, + + /// Trust incoming headers from these subnets for the purpose of Request ID extraction. + /// If not set - headers will not be used and a new Request ID will be generated. + trust_request_id_from: Vec, +} + +impl RequestMetaState { + /// Creates a new [`RequestMetaState`] + pub const fn new(trust_ip_from: Vec, trust_request_id_from: Vec) -> Self { + Self { + geoip: None, + trust_ip_from, + trust_request_id_from, + } + } + + /// Creates a new [`RequestMetaState`] with a GeoIP DB + pub fn new_with_geoip( + trust_ip_from: Vec, + trust_request_id_from: Vec, + geoip_db_path: Option, + ) -> Result { + let geoip = if let Some(v) = geoip_db_path { + Some(GeoIp::new(&v).context("unable to init GeoIP")?) + } else { + None + }; + + Ok(Self { + geoip, + trust_ip_from, + trust_request_id_from, + }) + } + + /// Extracts remote IP address from the `x-real-ip` header if remote is trusted & header exists + fn extract_ip(&self, headers: &HeaderMap, network_address: &IpAddr) -> Option { + // Do we trust this address? + self.trust_ip_from + .iter() + .any(|net| net.contains(network_address)) + .then(|| { + // If yes - get the IP from the headers if it's there + headers + .get(&X_REAL_IP) + .and_then(|x| x.to_str().ok()) + .and_then(|x| IpAddr::from_str(x).ok()) + }) + .flatten() + } + + /// Extracts a request ID from the `x-request-id` header if the remote is trusted and the header exists. + fn extract_request_id( + &self, + headers: &HeaderMap, + network_address: &IpAddr, + ) -> Option { + self.trust_request_id_from + .iter() + .any(|net| net.contains(network_address)) + .then(|| { + headers + .get(X_REQUEST_ID) + .and_then(|x| x.to_str().ok()) + .and_then(|x| Uuid::from_str(x).ok()) + .map(RequestId) + }) + .flatten() + } +} + +/// Extracts the metadata (request ID & client's IP address) about the request & inserts it as extensions +pub async fn middleware( + State(state): State>, + mut request: Request, + next: Next, +) -> Response { + let network_address = request + .extensions() + .get::>() + .map(|x| x.remote_addr.ip()); + + // Extract request ID if trusted or generate a new one + let request_id = network_address + .and_then(|ip| state.extract_request_id(request.headers(), &ip)) + .unwrap_or_else(|| RequestId(Uuid::now_v7())); + + // Extract client's IP falling back to the network_address if we can't + let remote_addr = network_address + .and_then(|ip| state.extract_ip(request.headers(), &ip)) + .or(network_address) + .map(RemoteAddr); + + let country_code = remote_addr.and_then(|v| { + request.extensions_mut().insert(v); + + // Look up country code if GeoIP is enabled + state.geoip.as_ref().and_then(|x| x.lookup(v.0)) + }); + + if let Some(v) = country_code { + request.extensions_mut().insert(v); + } + if let Some(v) = remote_addr { + // SAFETY: Any IP is a valid HeaderValue + request.headers_mut().insert( + X_REAL_IP, + HeaderValue::from_maybe_shared(Bytes::from(v.to_string())).unwrap(), + ); + } + + // SAFETY: UUID is a valid HeaderValue + let request_id_hdr = + HeaderValue::from_maybe_shared(Bytes::from(request_id.to_string())).unwrap(); + request.extensions_mut().insert(request_id); + request + .headers_mut() + .insert(X_REQUEST_ID, request_id_hdr.clone()); + + let mut response = next.run(request).await; + response.headers_mut().insert(X_REQUEST_ID, request_id_hdr); + response.extensions_mut().insert(request_id); + + if let Some(v) = remote_addr { + response.extensions_mut().insert(v); + } + + if let Some(v) = country_code { + response.extensions_mut().insert(v); + } + + response +} + +#[cfg(test)] +mod test { + use std::net::{Ipv4Addr, SocketAddr}; + + use axum::{ + Router, body::Body, middleware::from_fn_with_state, response::IntoResponse, routing::get, + }; + use http::StatusCode; + use tower::Service; + + use super::*; + use crate::{hname, http::server::conn::ConnInfo, hval, network::Addr}; + + const X_TEST_REQUEST_ID: &str = "x-test-request-id"; + const X_TEST_REMOTE_ADDR: &str = "x-test-remote-addr"; + const X_TEST_COUNTRY_CODE: &str = "x-test-country-code"; + const X_TEST_REAL_IP: &str = "x-test-real-ip"; + + // Known entries in the MaxMind test DB + const IP_KNOWN: Ipv4Addr = Ipv4Addr::new(89, 160, 20, 112); + const COUNTRY_KNOWN: &str = "SE"; + const IP_UNKNOWN: Ipv4Addr = Ipv4Addr::new(10, 10, 10, 10); + + fn test_db_path() -> PathBuf { + PathBuf::from(concat!( + env!("CARGO_MANIFEST_DIR"), + "/test-data/geoip-test-db.mmdb" + )) + } + + #[test] + fn lookup_known_ip_returns_country_code() { + let geoip = GeoIp::new(&test_db_path()).unwrap(); + assert_eq!( + geoip.lookup(IpAddr::V4(IP_KNOWN)).unwrap().0.as_str(), + COUNTRY_KNOWN + ); + } + + #[test] + fn lookup_unknown_ip_returns_none() { + let geoip = GeoIp::new(&test_db_path()).unwrap(); + assert!(geoip.lookup(IpAddr::V4(IP_UNKNOWN)).is_none()); + } + + #[test] + fn extract_ip_ignores_header_from_untrusted_source() { + let state = RequestMetaState::new(vec![], vec![]); + let network_address = IpAddr::V4(Ipv4Addr::new(203, 0, 113, 1)); + + let mut headers = HeaderMap::new(); + headers.insert(X_REAL_IP, hval!("1.2.3.4")); + + assert_eq!(state.extract_ip(&headers, &network_address), None); + } + + #[test] + fn extract_ip_uses_header_from_trusted_source() { + let trusted: IpNet = "203.0.113.0/24".parse().unwrap(); + let state = RequestMetaState::new(vec![trusted], vec![]); + let network_address = IpAddr::V4(Ipv4Addr::new(203, 0, 113, 1)); + + let mut headers = HeaderMap::new(); + headers.insert(X_REAL_IP, hval!("1.2.3.4")); + + assert_eq!( + state.extract_ip(&headers, &network_address), + Some(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4))) + ); + } + + #[test] + fn extract_ip_trusted_source_but_header_missing() { + let trusted: IpNet = "203.0.113.0/24".parse().unwrap(); + let state = RequestMetaState::new(vec![trusted], vec![]); + let network_address = IpAddr::V4(Ipv4Addr::new(203, 0, 113, 1)); + + assert_eq!(state.extract_ip(&HeaderMap::new(), &network_address), None); + } + + #[test] + fn extract_ip_trusted_source_but_header_invalid() { + let trusted: IpNet = "203.0.113.0/24".parse().unwrap(); + let state = RequestMetaState::new(vec![trusted], vec![]); + let network_address = IpAddr::V4(Ipv4Addr::new(203, 0, 113, 1)); + + let mut headers = HeaderMap::new(); + headers.insert(X_REAL_IP, hval!("not-an-ip")); + + assert_eq!(state.extract_ip(&headers, &network_address), None); + } + + #[test] + fn extract_ip_source_outside_trusted_subnet() { + let trusted: IpNet = "203.0.113.0/24".parse().unwrap(); + let state = RequestMetaState::new(vec![trusted], vec![]); + let network_address = IpAddr::V4(Ipv4Addr::new(198, 51, 100, 1)); + + let mut headers = HeaderMap::new(); + headers.insert(X_REAL_IP, hval!("1.2.3.4")); + + assert_eq!(state.extract_ip(&headers, &network_address), None); + } + + #[test] + fn extract_request_id_ignores_header_from_untrusted_source() { + let state = RequestMetaState::new(vec![], vec![]); + let network_address = IpAddr::V4(Ipv4Addr::new(203, 0, 113, 1)); + let id = Uuid::now_v7(); + + let mut headers = HeaderMap::new(); + headers.insert( + X_REQUEST_ID, + HeaderValue::from_str(&id.to_string()).unwrap(), + ); + + assert_eq!(state.extract_request_id(&headers, &network_address), None); + } + + #[test] + fn extract_request_id_uses_header_from_trusted_source() { + let trusted: IpNet = "203.0.113.0/24".parse().unwrap(); + let state = RequestMetaState::new(vec![], vec![trusted]); + let network_address = IpAddr::V4(Ipv4Addr::new(203, 0, 113, 1)); + let id = Uuid::now_v7(); + + let mut headers = HeaderMap::new(); + headers.insert( + X_REQUEST_ID, + HeaderValue::from_str(&id.to_string()).unwrap(), + ); + + assert_eq!( + state.extract_request_id(&headers, &network_address), + Some(RequestId(id)) + ); + } + + #[test] + fn extract_request_id_trusted_source_but_header_missing() { + let trusted: IpNet = "203.0.113.0/24".parse().unwrap(); + let state = RequestMetaState::new(vec![], vec![trusted]); + let network_address = IpAddr::V4(Ipv4Addr::new(203, 0, 113, 1)); + + assert_eq!( + state.extract_request_id(&HeaderMap::new(), &network_address), + None + ); + } + + #[test] + fn extract_request_id_trusted_source_but_header_invalid() { + let trusted: IpNet = "203.0.113.0/24".parse().unwrap(); + let state = RequestMetaState::new(vec![], vec![trusted]); + let network_address = IpAddr::V4(Ipv4Addr::new(203, 0, 113, 1)); + + let mut headers = HeaderMap::new(); + headers.insert(X_REQUEST_ID, hval!("not-a-uuid")); + + assert_eq!(state.extract_request_id(&headers, &network_address), None); + } + + #[test] + fn extract_request_id_source_outside_trusted_subnet() { + let trusted: IpNet = "203.0.113.0/24".parse().unwrap(); + let state = RequestMetaState::new(vec![], vec![trusted]); + let network_address = IpAddr::V4(Ipv4Addr::new(198, 51, 100, 1)); + let id = Uuid::now_v7(); + + let mut headers = HeaderMap::new(); + headers.insert( + X_REQUEST_ID, + HeaderValue::from_str(&id.to_string()).unwrap(), + ); + + assert_eq!(state.extract_request_id(&headers, &network_address), None); + } + + // ---- middleware ---- + + /// Echoes the `RequestId`/`RemoteAddr`/`CountryCode` extensions the middleware attached + /// to the *request* back as response headers, so tests can tell those apart from the + /// extensions/headers the middleware separately attaches to the *response*. + async fn echo_handler(req: Request) -> impl IntoResponse { + let request_id = req.extensions().get::().copied(); + let remote_addr = req.extensions().get::().copied(); + let country_code = req.extensions().get::().copied(); + let x_real_ip = req.headers().get(X_REAL_IP).cloned(); + + let mut resp = StatusCode::OK.into_response(); + if let Some(v) = request_id { + resp.headers_mut().insert( + hname!(X_TEST_REQUEST_ID), + HeaderValue::from_str(&v.0.to_string()).unwrap(), + ); + } + if let Some(v) = remote_addr { + resp.headers_mut().insert( + hname!(X_TEST_REMOTE_ADDR), + HeaderValue::from_str(&v.0.to_string()).unwrap(), + ); + } + if let Some(v) = country_code { + resp.headers_mut().insert( + hname!(X_TEST_COUNTRY_CODE), + HeaderValue::from_str(&v.0).unwrap(), + ); + } + if let Some(v) = x_real_ip { + resp.headers_mut().insert(hname!(X_TEST_REAL_IP), v); + } + resp + } + + fn app(state: RequestMetaState) -> Router { + Router::new() + .route("/", get(echo_handler)) + .layer(from_fn_with_state(Arc::new(state), middleware)) + } + + fn with_conn_info(mut req: Request, ip: IpAddr) -> Request { + let conn_info = ConnInfo { + remote_addr: Addr::Tcp(SocketAddr::new(ip, 4433)), + ..Default::default() + }; + req.extensions_mut().insert(Arc::new(conn_info)); + req + } + + #[tokio::test] + async fn middleware_without_conn_info_generates_request_id_and_no_remote_addr() { + let mut app = app(RequestMetaState::new(vec![], vec![])); + let req = Request::builder().body(Body::empty()).unwrap(); + + let resp = app.call(req).await.unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + // A fresh, valid request ID was generated & attached both to the request + // (echoed back by the handler) and to the response. + let resp_request_id_hdr = resp + .headers() + .get(&X_REQUEST_ID) + .unwrap() + .to_str() + .unwrap() + .to_string(); + assert!(Uuid::from_str(&resp_request_id_hdr).is_ok()); + assert_eq!( + resp.headers() + .get(X_TEST_REQUEST_ID) + .unwrap() + .to_str() + .unwrap(), + resp_request_id_hdr + ); + assert_eq!( + resp.extensions().get::().unwrap().0.to_string(), + resp_request_id_hdr + ); + + // No connection info => no remote address extracted at all. + assert!(resp.headers().get(X_TEST_REMOTE_ADDR).is_none()); + assert!(resp.extensions().get::().is_none()); + + // No GeoIP configured => no country code either. + assert!(resp.headers().get(X_TEST_COUNTRY_CODE).is_none()); + assert!(resp.extensions().get::().is_none()); + } + + #[tokio::test] + async fn middleware_untrusted_source_falls_back_to_network_address() { + let mut app = app(RequestMetaState::new(vec![], vec![])); + let network_ip = IpAddr::V4(Ipv4Addr::new(203, 0, 113, 5)); + + let req = Request::builder() + .header(X_REAL_IP, "1.2.3.4") + .body(Body::empty()) + .unwrap(); + let req = with_conn_info(req, network_ip); + + let resp = app.call(req).await.unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + // x-real-ip is ignored (untrusted) - falls back to the raw network address. + assert_eq!( + resp.headers() + .get(X_TEST_REMOTE_ADDR) + .unwrap() + .to_str() + .unwrap(), + network_ip.to_string() + ); + assert_eq!(resp.extensions().get::().unwrap().0, network_ip); + + // No GeoIP configured => no country code either. + assert!(resp.headers().get(X_TEST_COUNTRY_CODE).is_none()); + assert!(resp.extensions().get::().is_none()); + } + + #[tokio::test] + async fn middleware_trusted_source_uses_header_ip_and_header_request_id() { + let trusted: IpNet = "203.0.113.0/24".parse().unwrap(); + let mut app = app(RequestMetaState::new(vec![trusted], vec![trusted])); + + let network_ip = IpAddr::V4(Ipv4Addr::new(203, 0, 113, 5)); + let header_ip = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)); + let header_request_id = Uuid::now_v7(); + + let req = Request::builder() + .header(X_REAL_IP, header_ip.to_string()) + .header(X_REQUEST_ID, header_request_id.to_string()) + .body(Body::empty()) + .unwrap(); + let req = with_conn_info(req, network_ip); + + let resp = app.call(req).await.unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + assert_eq!( + resp.headers().get(&X_REQUEST_ID).unwrap().to_str().unwrap(), + header_request_id.to_string() + ); + assert_eq!( + resp.extensions().get::().unwrap().0, + header_request_id + ); + + assert_eq!( + resp.headers() + .get(X_TEST_REMOTE_ADDR) + .unwrap() + .to_str() + .unwrap(), + header_ip.to_string() + ); + assert_eq!(resp.extensions().get::().unwrap().0, header_ip); + assert_eq!( + resp.headers() + .get(X_TEST_REAL_IP) + .unwrap() + .to_str() + .unwrap(), + header_ip.to_string() + ); + + // No GeoIP configured => no country code either. + assert!(resp.headers().get(X_TEST_COUNTRY_CODE).is_none()); + assert!(resp.extensions().get::().is_none()); + } + + #[tokio::test] + async fn middleware_trusted_source_falls_back_when_headers_missing() { + let trusted: IpNet = "203.0.113.0/24".parse().unwrap(); + let mut app = app(RequestMetaState::new(vec![trusted], vec![trusted])); + + let network_ip = IpAddr::V4(Ipv4Addr::new(203, 0, 113, 5)); + let req = Request::builder().body(Body::empty()).unwrap(); + let req = with_conn_info(req, network_ip); + + let resp = app.call(req).await.unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + // Trusted, but no headers present => fresh request ID, remote address falls + // back to the raw network address. + let resp_request_id_hdr = resp + .headers() + .get(&X_REQUEST_ID) + .unwrap() + .to_str() + .unwrap() + .to_string(); + assert!(Uuid::from_str(&resp_request_id_hdr).is_ok()); + + assert_eq!( + resp.headers() + .get(X_TEST_REMOTE_ADDR) + .unwrap() + .to_str() + .unwrap(), + network_ip.to_string() + ); + + // No GeoIP configured => no country code either. + assert!(resp.headers().get(X_TEST_COUNTRY_CODE).is_none()); + assert!(resp.extensions().get::().is_none()); + } + + #[tokio::test] + async fn middleware_geoip_disabled_skips_country_lookup() { + let mut app = app(RequestMetaState::new(vec![], vec![])); + let req = Request::builder().body(Body::empty()).unwrap(); + let req = with_conn_info(req, IpAddr::V4(IP_KNOWN)); + + let resp = app.call(req).await.unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + // GeoIP not configured on the state => no lookup happens even for a known IP. + assert!(resp.headers().get(X_TEST_COUNTRY_CODE).is_none()); + assert!(resp.extensions().get::().is_none()); + } + + #[tokio::test] + async fn middleware_geoip_unknown_ip_no_country_code() { + let state = RequestMetaState::new_with_geoip(vec![], vec![], Some(test_db_path())).unwrap(); + let mut app = app(state); + + let req = Request::builder().body(Body::empty()).unwrap(); + let req = with_conn_info(req, IpAddr::V4(IP_UNKNOWN)); + + let resp = app.call(req).await.unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + assert!(resp.headers().get(X_TEST_COUNTRY_CODE).is_none()); + assert!(resp.extensions().get::().is_none()); + } + + #[tokio::test] + async fn middleware_geoip_known_ip_attaches_country_code_to_request_and_response() { + let state = RequestMetaState::new_with_geoip(vec![], vec![], Some(test_db_path())).unwrap(); + let mut app = app(state); + + let req = Request::builder().body(Body::empty()).unwrap(); + let req = with_conn_info(req, IpAddr::V4(IP_KNOWN)); + + let resp = app.call(req).await.unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!( + resp.headers() + .get(X_TEST_COUNTRY_CODE) + .unwrap() + .to_str() + .unwrap(), + COUNTRY_KNOWN + ); + assert_eq!( + resp.extensions().get::().unwrap().0.as_str(), + COUNTRY_KNOWN + ); + } +} diff --git a/ic-bn-lib/src/http/middleware/waf.rs b/ic-bn-lib/src/http/middleware/waf.rs index 4ef4fe6..716c824 100644 --- a/ic-bn-lib/src/http/middleware/waf.rs +++ b/ic-bn-lib/src/http/middleware/waf.rs @@ -42,7 +42,11 @@ use tracing::warn; use url::Url; use crate::{ - http::{Error, client::Client, middleware::extract_ip_from_request}, + http::{ + Error, + client::Client, + middleware::{RemoteAddr, request_meta::CountryCode}, + }, tasks::Run, }; @@ -195,6 +199,8 @@ pub struct RequestMatcher { #[serde_as(as = "Option>")] pub methods: Option>, pub headers: Option>, + #[serde_as(as = "Option")] + pub country_code: Option, } impl PartialEq for RequestMatcher { @@ -211,6 +217,7 @@ impl PartialEq for RequestMatcher { .map(|x| x.clone().into_iter().sorted().collect::>()) && self.host.as_ref().map(|x| x.as_str()) == other.host.as_ref().map(|x| x.as_str()) && self.path.as_ref().map(|x| x.as_str()) == other.path.as_ref().map(|x| x.as_str()) + && self.country_code.as_ref().map(|x| x.as_str()) == other.country_code.as_ref().map(|x| x.as_str()) } } impl Eq for RequestMatcher {} @@ -250,6 +257,19 @@ impl RequestMatcher { return false; } + // Check if country code matches + if let Some(v) = &self.country_code { + // If country code matching is requested, + // but no CountryCode is in the request - then we fail the match + let Some(country_code) = req.extensions().get::() else { + return false; + }; + + if !v.is_match(country_code) { + return false; + } + } + // Check if any methods match if let Some(v) = &self.methods && !v.iter().contains(req.method()) @@ -380,11 +400,11 @@ impl RateLimitType { // Allow if we fail to extract IP. // It shouldn't happen ever under normal workload // and it's probably better to allow the request in this case. - let Some(ip) = extract_ip_from_request(req) else { + let Some(ip) = req.extensions().get::() else { return RateLimitDecision::Pass; }; - (v.clock(), v.check_key(&ip)) + (v.clock(), v.check_key(ip)) } }; @@ -818,9 +838,19 @@ impl Run for WafLayer { #[cfg(test)] mod test { + #![allow(clippy::trivial_regex)] + + use std::sync::atomic::{AtomicUsize, Ordering}; + use axum::{Router, body::Body}; use serde_json::json; + use crate::{ + hname, + http::client::{Client, MockClient}, + hval, regex, + }; + use super::*; #[test] @@ -850,6 +880,10 @@ mod test { assert!(StatusRange::from_str("-500").is_err()); assert!(StatusRange::from_str("101-600").is_err()); assert!(StatusRange::from_str("199-100").is_err()); + // `to` segment present but not a number + assert!(StatusRange::from_str("100-abc").is_err()); + // Equal boundaries should be rejected, not just `to < from` + assert!(StatusRange::from_str("300-300").is_err()); let range = StatusRange::from_str("200-499").unwrap(); @@ -867,7 +901,20 @@ mod test { } #[test] - fn test_request() { + fn test_parse_ruleset_formats() { + // Valid JSON should parse directly, without needing the YAML fallback + let json = json!({ "requests": [] }).to_string(); + assert!(parse_ruleset(json.as_bytes()).is_ok()); + + // Valid YAML is picked up by the fallback + assert!(parse_ruleset(b"requests: []\n").is_ok()); + + // Invalid as both JSON and YAML representations of a Ruleset + assert!(parse_ruleset(b"{not valid as either format").is_err()); + } + + #[test] + fn test_request_matcher_deserialize() { let rule = json!({ "methods": ["GET", "OPTIONS"], "headers": [ @@ -882,6 +929,7 @@ mod test { ], "host": "^lala", "path": "^/foo", + "country_code": "^(CH|DE)$", }) .to_string(); @@ -892,80 +940,355 @@ mod test { methods: Some(vec![Method::GET, Method::OPTIONS]), headers: Some(vec![ HeaderMatcher { - name: HeaderName::from_static("foo"), - regex: Regex::from_str("^bar.*$").unwrap(), + name: hname!("foo"), + regex: regex!("^bar.*$"), }, HeaderMatcher { - name: HeaderName::from_static("dead"), - regex: Regex::from_str("^beef.*$").unwrap(), + name: hname!("dead"), + regex: regex!("^beef.*$"), } ]), - host: Some(Regex::from_str("^lala").unwrap()), - path: Some(Regex::from_str("^/foo").unwrap()), + host: Some(regex!("^lala")), + path: Some(regex!("^/foo")), + country_code: Some(regex!("^(CH|DE)$")), } ); + } + + #[test] + fn test_request_matcher_empty() { + // A matcher with every field unset matches any request + let rule = RequestMatcher { + host: None, + path: None, + methods: None, + headers: None, + country_code: None, + }; - // Test full matches let req = Request::builder() - .header("foo", "barfuss") - .header("dead", "beefbeef") - .method(Method::GET) + .method(Method::TRACE) + .uri("https://anything.example/whatever") + .body("") + .unwrap(); + assert!(rule.evaluate(&req)); + } + + #[test] + fn test_request_matcher_host() { + let rule = RequestMatcher { + host: Some(regex!("^lala")), + path: None, + methods: None, + headers: None, + country_code: None, + }; + + // HTTP/2+: host comes from the URI authority + let req = Request::builder() .version(Version::HTTP_2) .uri("https://lala/foo") .body("") .unwrap(); assert!(rule.evaluate(&req)); + let req = Request::builder() + .version(Version::HTTP_2) + .uri("https://other/foo") + .body("") + .unwrap(); + assert!(!rule.evaluate(&req)); + + // HTTP/1.x: host comes from the `Host` header, not the URI for http_ver in [Version::HTTP_09, Version::HTTP_10, Version::HTTP_11] { let req = Request::builder() - .header("foo", "barfuss") - .header("dead", "beefbeef") + .version(http_ver) .header("host", "lala") + .uri("https://other/foo") + .body("") + .unwrap(); + assert!(rule.evaluate(&req)); + + // No Host header -> resolves to an empty string, fails to match + let req = Request::builder() .version(http_ver) - .method(Method::OPTIONS) .uri("https://lala/foo") .body("") .unwrap(); + assert!(!rule.evaluate(&req)); + } + } + + #[test] + fn test_request_matcher_path() { + let rule = RequestMatcher { + host: None, + path: Some(regex!("^/foo")), + methods: None, + headers: None, + country_code: None, + }; + + let req = Request::builder().uri("https://lala/foo").body("").unwrap(); + assert!(rule.evaluate(&req)); + + let req = Request::builder() + .uri("https://lala/foo/bar") + .body("") + .unwrap(); + assert!(rule.evaluate(&req)); + + let req = Request::builder().uri("https://lala/bar").body("").unwrap(); + assert!(!rule.evaluate(&req)); + } + + #[test] + fn test_request_matcher_methods() { + let rule = RequestMatcher { + host: None, + path: None, + methods: Some(vec![Method::GET, Method::OPTIONS]), + headers: None, + country_code: None, + }; + + for m in [Method::GET, Method::OPTIONS] { + let req = Request::builder().method(m).body("").unwrap(); assert!(rule.evaluate(&req)); } + for m in [Method::POST, Method::DELETE] { + let req = Request::builder().method(m).body("").unwrap(); + assert!(!rule.evaluate(&req)); + } + } + + #[test] + fn test_request_matcher_headers() { + let rule = RequestMatcher { + host: None, + path: None, + methods: None, + headers: Some(vec![ + HeaderMatcher { + name: hname!("foo"), + regex: regex!("^bar.*$"), + }, + HeaderMatcher { + name: hname!("dead"), + regex: regex!("^beef.*$"), + }, + ]), + country_code: None, + }; + + // All header rules match let req = Request::builder() - .header("dead", "beefbeef") .header("foo", "barfuss") - .version(Version::HTTP_2) - .method(Method::OPTIONS) - .uri("https://lala/foo") + .header("dead", "beefbeef") .body("") .unwrap(); assert!(rule.evaluate(&req)); - // Test partial matches (no match) + // One header value fails its regex let req = Request::builder() - .header("foo", "barfuss") + .header("foo", "nope") .header("dead", "beefbeef") - .method(Method::POST) - .uri("https://lala/foo") .body("") .unwrap(); assert!(!rule.evaluate(&req)); + // One required header is missing entirely let req = Request::builder() .header("foo", "barfuss") - .header("dead", "beefbeef") + .body("") + .unwrap(); + assert!(!rule.evaluate(&req)); + } + + #[test] + fn test_request_matcher_country_code() { + let rule = RequestMatcher { + host: None, + path: None, + methods: Some(vec![Method::GET]), + headers: Some(vec![HeaderMatcher { + name: hname!("foo"), + regex: regex!("^bar$"), + }]), + country_code: Some(regex!("^(CH|DE)$")), + }; + + let build = |cc: &str, method: Method, header: &str| { + Request::builder() + .method(method) + .header("foo", header) + .extension(CountryCode(cc.try_into().unwrap())) + .body("") + .unwrap() + }; + + // Every field matches + assert!(rule.evaluate(&build("CH", Method::GET, "bar"))); + + assert!(!rule.evaluate(&build("CH", Method::POST, "bar"))); + assert!(!rule.evaluate(&build("CH", Method::GET, "nope"))); + + // Extension present but doesn't match the regex, even though + // methods/headers would otherwise pass + assert!(!rule.evaluate(&build("US", Method::GET, "bar"))); + + // No CountryCode extension at all + let req = Request::builder() .method(Method::GET) - .uri("https://lala/bar") + .header("foo", "bar") .body("") .unwrap(); assert!(!rule.evaluate(&req)); + } + + #[test] + fn test_request_matcher_full_match() { + let rule = RequestMatcher { + host: Some(regex!("^lala")), + path: Some(regex!("^/foo")), + methods: Some(vec![Method::GET, Method::OPTIONS]), + headers: Some(vec![ + HeaderMatcher { + name: hname!("foo"), + regex: regex!("^bar.*$"), + }, + HeaderMatcher { + name: hname!("dead"), + regex: regex!("^beef.*$"), + }, + ]), + country_code: Some(regex!("^(CH|DE)$")), + }; + + let build = |method: Method, uri: &str| { + Request::builder() + .version(Version::HTTP_2) + .method(method) + .uri(uri) + .header("foo", "barfuss") + .header("dead", "beefbeef") + .extension(CountryCode("CH".try_into().unwrap())) + .body("") + .unwrap() + }; + + assert!(rule.evaluate(&build(Method::GET, "https://lala/foo"))); + assert!(rule.evaluate(&build(Method::OPTIONS, "https://lala/foo"))); + + // Each field, perturbed individually, should fail the match + assert!(!rule.evaluate(&build(Method::POST, "https://lala/foo"))); + assert!(!rule.evaluate(&build(Method::GET, "https://other/foo"))); + assert!(!rule.evaluate(&build(Method::GET, "https://lala/bar"))); let req = Request::builder() - .header("fox", "barfuss") - .header("dead", "beefbeef") + .version(Version::HTTP_2) .method(Method::GET) .uri("https://lala/foo") + .header("fox", "barfuss") + .header("dead", "beefbeef") + .extension(CountryCode("CH".try_into().unwrap())) .body("") .unwrap(); assert!(!rule.evaluate(&req)); + + let mut req = build(Method::GET, "https://lala/foo"); + req.extensions_mut() + .insert(CountryCode("US".try_into().unwrap())); + assert!(!rule.evaluate(&req)); + } + + #[test] + fn test_header_matcher() { + let m = HeaderMatcher { + name: hname!("foo"), + regex: regex!("^bar"), + }; + + // Matching name and value + assert!(m.evaluate(&hname!("foo"), &hval!("barfuss"))); + + // Name mismatch -- the value alone would satisfy the regex + assert!(!m.evaluate(&hname!("baz"), &hval!("bar"))); + + // Value fails to_str() (invalid UTF-8), even with a permissive regex + let permissive = HeaderMatcher { + name: hname!("foo"), + regex: regex!(".*"), + }; + let bad_value = HeaderValue::from_bytes(&[0xff, 0xfe]).unwrap(); + assert!(!permissive.evaluate(&hname!("foo"), &bad_value)); + + // evaluate_headermap finds a match anywhere in the map + let mut map = HeaderMap::new(); + map.insert(hname!("other"), hval!("irrelevant")); + map.insert(hname!("foo"), hval!("barfuss")); + assert!(m.evaluate_headermap(&map)); + + map.remove(hname!("foo")); + assert!(!m.evaluate_headermap(&map)); + } + + #[test] + fn test_header_matcher_ord() { + let mut v = [ + HeaderMatcher { + name: hname!("zzz"), + regex: regex!(".*"), + }, + HeaderMatcher { + name: hname!("aaa"), + regex: regex!(".*"), + }, + HeaderMatcher { + name: hname!("mmm"), + regex: regex!(".*"), + }, + ]; + v.sort(); + let names: Vec<_> = v.iter().map(|h| h.name.as_str()).collect(); + assert_eq!(names, vec!["aaa", "mmm", "zzz"]); + + // RequestMatcher equality sorts headers before comparing, so + // differing input order shouldn't affect equality. + let a = RequestMatcher { + host: None, + path: None, + methods: None, + headers: Some(vec![ + HeaderMatcher { + name: hname!("foo"), + regex: regex!("^bar.*$"), + }, + HeaderMatcher { + name: hname!("dead"), + regex: regex!("^beef.*$"), + }, + ]), + country_code: None, + }; + let b = RequestMatcher { + host: None, + path: None, + methods: None, + headers: Some(vec![ + HeaderMatcher { + name: hname!("dead"), + regex: regex!("^beef.*$"), + }, + HeaderMatcher { + name: hname!("foo"), + regex: regex!("^bar.*$"), + }, + ]), + country_code: None, + }; + assert_eq!(a, b); } #[test] @@ -996,12 +1319,12 @@ mod test { ]), headers: Some(vec![ HeaderMatcher { - name: HeaderName::from_static("foo"), - regex: Regex::from_str("^bar.*$").unwrap(), + name: hname!("foo"), + regex: regex!("^bar.*$"), }, HeaderMatcher { - name: HeaderName::from_static("dead"), - regex: Regex::from_str("^beef.*$").unwrap(), + name: hname!("dead"), + regex: regex!("^beef.*$"), } ]), } @@ -1058,6 +1381,53 @@ mod test { assert!(!rule.evaluate(&resp)); } + #[test] + fn test_response_matcher_empty() { + // A matcher with every field unset matches any response + let rule = ResponseMatcher { + status: None, + headers: None, + }; + + let resp = Response::builder().status(200).body("").unwrap(); + assert!(rule.evaluate(&resp)); + + let resp = Response::builder() + .status(500) + .header("x", "y") + .body("") + .unwrap(); + assert!(rule.evaluate(&resp)); + } + + #[test] + fn test_response_matcher_headers_only() { + // status: None means the status check is skipped entirely + let rule = ResponseMatcher { + status: None, + headers: Some(vec![HeaderMatcher { + name: hname!("foo"), + regex: regex!("^bar.*$"), + }]), + }; + + for status in [StatusCode::OK, StatusCode::INTERNAL_SERVER_ERROR] { + let resp = Response::builder() + .status(status) + .header("foo", "barfuss") + .body("") + .unwrap(); + assert!(rule.evaluate(&resp)); + + let resp = Response::builder() + .status(status) + .header("foo", "nope") + .body("") + .unwrap(); + assert!(!rule.evaluate(&resp)); + } + } + #[test] fn test_request_action() { assert_eq!( @@ -1104,16 +1474,9 @@ mod test { assert!(RequestAction::from_str("limit").is_err()); assert!(RequestAction::from_str("limit:").is_err()); + // Propagates errors from RateLimitType::from_str -- see + // test_rate_limit_type_from_str for its detailed error branches. assert!(RequestAction::from_str("limit:foo").is_err()); - assert!(RequestAction::from_str("limit:0/1s").is_err()); - assert!(RequestAction::from_str("limit:1/0s").is_err()); - assert!(RequestAction::from_str("limit:1/foo").is_err()); - - // Rate high enough that `dur / rate` truncates to zero should be a parse - // error, not a panic (dur/rate used to be passed unchecked into - // `Quota::with_period(..).unwrap()`, which panics on a zero duration). - assert!(RequestAction::from_str("limit:global:2000000000/1s").is_err()); - assert!(RequestAction::from_str("limit:per_ip:2000000000/1s").is_err()); } #[test] @@ -1139,49 +1502,184 @@ mod test { } #[test] - fn test_ruleset() { - let ruleset = json!({ - "requests": [ - { - "match": { - "methods": ["GET", "POST"], - "host": "^foo", - "path": "^/bar" - }, - "action": "limit:global:10/1h", - }] - }) - .to_string(); - let ruleset: Ruleset = serde_json::from_str(&ruleset).unwrap(); + fn test_rate_limit_type_from_str() { + assert!(RateLimitType::from_str("global:10/1m").is_ok()); + assert!(RateLimitType::from_str("per_ip:10/1m").is_ok()); + + // Missing ':' for the 'type:rate' format + assert!(RateLimitType::from_str("10/1m").is_err()); + // Missing '/' for the 'rate/duration' format + assert!(RateLimitType::from_str("global:10").is_err()); + // Rate fails to parse as u32 + assert!(RateLimitType::from_str("global:foo/1m").is_err()); + assert!(RateLimitType::from_str("global:99999999999/1m").is_err()); + // Duration fails to parse + assert!(RateLimitType::from_str("global:10/foo").is_err()); + // Rate must be > 0 + assert!(RateLimitType::from_str("global:0/1s").is_err()); + assert!(RateLimitType::from_str("per_ip:0/1s").is_err()); + // Duration cannot be zero + assert!(RateLimitType::from_str("global:10/0s").is_err()); + assert!(RateLimitType::from_str("per_ip:10/0s").is_err()); + // Unknown limiter type + assert!(RateLimitType::from_str("bogus:10/1m").is_err()); + + // Rate high enough that `dur / rate` truncates to zero should be a + // parse error, not a panic (dur/rate used to be passed unchecked + // into `Quota::with_period(..).unwrap()`, which panics on a zero + // duration). + assert!(RateLimitType::from_str("global:2000000000/1s").is_err()); + assert!(RateLimitType::from_str("per_ip:2000000000/1s").is_err()); + } + + #[test] + fn test_rate_limit_type_eq() { + let a = RateLimitType::from_str("global:10/1m").unwrap(); + let b = RateLimitType::from_str("global:10/1m").unwrap(); + let c = RateLimitType::from_str("global:20/1m").unwrap(); + assert_eq!(a, b); + assert_ne!(a, c); + + let d = RateLimitType::from_str("per_ip:10/1m").unwrap(); + let e = RateLimitType::from_str("per_ip:10/1m").unwrap(); + let f = RateLimitType::from_str("per_ip:20/1m").unwrap(); + assert_eq!(d, e); + assert_ne!(d, f); + + // Different variants are never equal, regardless of quota + assert_ne!(a, d); + } + + #[test] + fn test_rate_limit_type_allowed_global() { + let rl = RateLimitType::from_str("global:2/1h").unwrap(); + let req = Request::builder().body(()).unwrap(); + + assert_eq!(rl.allowed(&req), RateLimitDecision::Pass); + assert_eq!(rl.allowed(&req), RateLimitDecision::Pass); + assert!(matches!(rl.allowed(&req), RateLimitDecision::Throttle(_))); + } + + #[test] + fn test_rate_limit_type_allowed_per_ip() { + let rl = RateLimitType::from_str("per_ip:1/1h").unwrap(); + + // No RemoteAddr extension at all -> defensive Pass, regardless of + // how many requests come through. + let req_no_addr = Request::builder().body(()).unwrap(); + for _ in 0..10 { + assert_eq!(rl.allowed(&req_no_addr), RateLimitDecision::Pass); + } + + // Per-IP quota: first request for an IP passes, the next throttles + let mut req_a = Request::builder().body(()).unwrap(); + req_a + .extensions_mut() + .insert(RemoteAddr(IpAddr::from_str("1.2.3.4").unwrap())); + assert_eq!(rl.allowed(&req_a), RateLimitDecision::Pass); + assert!(matches!(rl.allowed(&req_a), RateLimitDecision::Throttle(_))); + + // A different IP has an independent quota + let mut req_b = Request::builder().body(()).unwrap(); + req_b + .extensions_mut() + .insert(RemoteAddr(IpAddr::from_str("5.6.7.8").unwrap())); + assert_eq!(rl.allowed(&req_b), RateLimitDecision::Pass); + } + + #[test] + fn test_ruleset_is_empty() { + let ruleset: Ruleset = serde_json::from_str( + &json!({ + "requests": [ + { "match": { "methods": ["GET"] }, "action": "block:403" } + ] + }) + .to_string(), + ) + .unwrap(); assert!(!ruleset.is_empty()); - let ruleset = json!({ - "responses": [ - { - "match_req": { - "methods": ["OPTIONS"], - }, - "match_resp": { - "status": ["100-200", "400-499", "599"], - }, - "action": "block:499", - }] - }) - .to_string(); - let ruleset: Ruleset = serde_json::from_str(&ruleset).unwrap(); + let ruleset: Ruleset = serde_json::from_str( + &json!({ + "responses": [ + { "match_resp": { "status": ["500"] }, "action": "block:403" } + ] + }) + .to_string(), + ) + .unwrap(); assert!(!ruleset.is_empty()); - let ruleset = json!({ - "requests": [], - "responses": [], - }) - .to_string(); - let ruleset: Ruleset = serde_json::from_str(&ruleset).unwrap(); + // Both fields present but empty + let ruleset: Ruleset = + serde_json::from_str(&json!({"requests": [], "responses": []}).to_string()).unwrap(); assert!(ruleset.is_empty()); - let ruleset = json!({}).to_string(); - let ruleset: Ruleset = serde_json::from_str(&ruleset).unwrap(); + // Both fields entirely absent + let ruleset: Ruleset = serde_json::from_str(&json!({}).to_string()).unwrap(); + assert!(ruleset.is_empty()); + + // One field present-but-empty, the other absent entirely: a + // combination independent of the two cases above -- a + // requests/responses field-swap bug in is_empty() would still pass + // those but fail these. + let ruleset: Ruleset = serde_json::from_str(&json!({"requests": []}).to_string()).unwrap(); + assert!(ruleset.is_empty()); + + let ruleset: Ruleset = serde_json::from_str(&json!({"responses": []}).to_string()).unwrap(); assert!(ruleset.is_empty()); + } + + #[test] + fn test_ruleset_evaluate_request() { + let probe = Request::builder().method(Method::GET).body("").unwrap(); + + // Neither field set, or a present-but-empty requests field: both + // fall through to Decision::Pass from within evaluate_request + // itself, not merely via the Waf service's separate is_empty() + // fast path. + let empty = Ruleset { + requests: None, + responses: None, + }; + assert_eq!(empty.evaluate_request(&probe), Decision::Pass); + + let empty_vec = Ruleset { + requests: Some(vec![]), + responses: None, + }; + assert_eq!(empty_vec.evaluate_request(&probe), Decision::Pass); + + // An earlier rule with an explicit `pass` action stops the scan + // before a later, also-matching, blocking rule is considered. + let ruleset: Ruleset = serde_json::from_str( + &json!({ + "requests": [ + { "match": { "methods": ["GET"] }, "action": "pass" }, + { "match": { "methods": ["GET"] }, "action": "block:403" } + ] + }) + .to_string(), + ) + .unwrap(); + assert_eq!(ruleset.evaluate_request(&probe), Decision::Pass); + + // First-match-wins between two overlapping blocking rules + let ruleset: Ruleset = serde_json::from_str( + &json!({ + "requests": [ + { "match": { "methods": ["GET"] }, "action": "block:451" }, + { "match": { "methods": ["GET"] }, "action": "block:403" } + ] + }) + .to_string(), + ) + .unwrap(); + assert_eq!( + ruleset.evaluate_request(&probe), + Decision::Block(StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS) + ); let ruleset = json!({ "requests": [ @@ -1198,43 +1696,12 @@ mod test { "methods": ["DELETE"], }, "action": "block:403", - }], - - "responses": [ - { - "match_req": { - "methods": ["OPTIONS"], - }, - "match_resp": { - "status": ["100-200", "400-499", "599"], - }, - "action": "block:499", - }, - { - "match_resp": { - "status": ["100-200", "400-499", "599"], - }, - "action": "block:451", - }, - { - "match_resp": { - "status": ["500"], - "headers": [{ - "name": "foo", - "value": "bar.*", - }] - }, - "action": "block:401", }] }) .to_string(); - let ruleset: Ruleset = serde_json::from_str(&ruleset).unwrap(); - assert!(!ruleset.is_empty()); - // Test requests - - // Should always pass + // Should always pass (matches no rule) for _ in 0..1000 { let req = Request::builder().method(Method::OPTIONS).body("").unwrap(); assert_eq!(ruleset.evaluate_request(&req), Decision::Pass); @@ -1287,8 +1754,82 @@ mod test { Decision::Throttle(_) )); } + } + + #[test] + fn test_ruleset_evaluate_response() { + let probe_req = Request::builder().method(Method::POST).body(()).unwrap(); + let probe_resp = Response::builder().status(200).body("").unwrap(); + + // Neither field set, or a present-but-empty responses field: both + // fall through to Decision::Pass from within evaluate_response + // itself. + let empty = Ruleset { + requests: None, + responses: None, + }; + assert_eq!( + empty.evaluate_response(&probe_req, &probe_resp), + Decision::Pass + ); + + let empty_vec = Ruleset { + requests: None, + responses: Some(vec![]), + }; + assert_eq!( + empty_vec.evaluate_response(&probe_req, &probe_resp), + Decision::Pass + ); + + // An earlier rule with an explicit `pass` action stops the scan + // before a later, also-matching, blocking rule is considered. + let ruleset: Ruleset = serde_json::from_str( + &json!({ + "responses": [ + { "match_resp": { "status": ["200"] }, "action": "pass" }, + { "match_resp": { "status": ["200"] }, "action": "block:403" } + ] + }) + .to_string(), + ) + .unwrap(); + assert_eq!( + ruleset.evaluate_response(&probe_req, &probe_resp), + Decision::Pass + ); + + let ruleset = json!({ + "responses": [ + { + "match_req": { + "methods": ["OPTIONS"], + }, + "match_resp": { + "status": ["100-200", "400-499", "599"], + }, + "action": "block:499", + }, + { + "match_resp": { + "status": ["100-200", "400-499", "599"], + }, + "action": "block:451", + }, + { + "match_resp": { + "status": ["500"], + "headers": [{ + "name": "foo", + "value": "bar.*", + }] + }, + "action": "block:401", + }] + }) + .to_string(); + let ruleset: Ruleset = serde_json::from_str(&ruleset).unwrap(); - // Test responses let req = Request::builder().method(Method::POST).body(()).unwrap(); let resp = Response::builder() @@ -1310,7 +1851,8 @@ mod test { ); } - // Should always block with 499 + // Should always block with 499 -- first-match-wins: this request + // also matches rule 2 (451), but rule 1 (OPTIONS-only) comes first. let req = Request::builder().method(Method::OPTIONS).body(()).unwrap(); for _ in 0..1000 { @@ -1391,4 +1933,279 @@ mod test { let resp = router.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); } + + #[tokio::test] + async fn test_waf_fast_path_empty_ruleset() { + use axum::routing::get; + + // With an empty ruleset, the Waf service should take its fast path + // and call straight through to the inner handler. + let layer = WafLayer::new( + Arc::new(ArcSwap::new(Arc::new(Ruleset::default()))), + None, + Duration::ZERO, + ); + let mut router = Router::new() + .route("/", get(|| async { (StatusCode::IM_A_TEAPOT, "hi") })) + .layer(layer); + + let req = Request::builder() + .method(Method::GET) + .uri("/") + .body(Body::empty()) + .unwrap(); + let resp = router.call(req).await.unwrap(); + assert_eq!(resp.status(), StatusCode::IM_A_TEAPOT); + } + + #[tokio::test] + async fn test_waf_blocks_via_response_rule() { + use axum::routing::get; + + // A response-side rule must call through to the inner handler + // first, then evaluate (and can override) its actual response. + let ruleset = parse_ruleset( + b"responses:\n- match_resp:\n status: [\"404\"]\n action: block:451\n", + ) + .unwrap(); + let layer = WafLayer::new( + Arc::new(ArcSwap::new(Arc::new(ruleset))), + None, + Duration::ZERO, + ); + + let mut router = Router::new() + .route("/", get(|| async { StatusCode::NOT_FOUND })) + .layer(layer); + + let req = Request::builder() + .method(Method::GET) + .uri("/") + .body(Body::empty()) + .unwrap(); + let resp = router.call(req).await.unwrap(); + assert_eq!(resp.status(), StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS); + } + + #[test] + fn test_waflayer_new_from_cli() { + // Neither url nor file -> no fetcher + let cli = WafCli { + waf_enable: false, + waf_api: false, + waf_url: None, + waf_file: None, + waf_interval: Duration::from_secs(1), + }; + let layer = WafLayer::new_from_cli(&cli, None).unwrap(); + assert!(layer.fetcher.is_none()); + + // File source -> file fetcher, no HTTP client needed + let cli = WafCli { + waf_enable: false, + waf_api: false, + waf_url: None, + waf_file: Some(PathBuf::from("/tmp/does-not-need-to-exist.yaml")), + waf_interval: Duration::from_secs(1), + }; + let layer = WafLayer::new_from_cli(&cli, None).unwrap(); + assert!(layer.fetcher.is_some()); + + // URL source without an HTTP client -> error + let cli = WafCli { + waf_enable: false, + waf_api: false, + waf_url: Some(Url::parse("http://example.com/waf").unwrap()), + waf_file: None, + waf_interval: Duration::from_secs(1), + }; + assert!(WafLayer::new_from_cli(&cli, None).is_err()); + + // URL source with an HTTP client -> URL fetcher + let client: Arc = Arc::new(MockClient::new()); + let layer = WafLayer::new_from_cli(&cli, Some(client)).unwrap(); + assert!(layer.fetcher.is_some()); + } + + struct OkFetcher(&'static [u8]); + + #[async_trait] + impl FetchesRuleset for OkFetcher { + async fn fetch_rules(&self) -> Result { + parse_ruleset(self.0) + } + } + + struct ErrFetcher; + + #[async_trait] + impl FetchesRuleset for ErrFetcher { + async fn fetch_rules(&self) -> Result { + Err(anyhow!("boom").into()) + } + } + + struct CountingFetcher(Arc); + + #[async_trait] + impl FetchesRuleset for CountingFetcher { + async fn fetch_rules(&self) -> Result { + self.0.fetch_add(1, Ordering::SeqCst); + Err(anyhow!("no-op").into()) + } + } + + #[tokio::test] + async fn test_waflayer_update_ruleset() { + // No fetcher configured -> no-op + let layer = WafLayer::new( + Arc::new(ArcSwap::new(Arc::new(Ruleset::default()))), + None, + Duration::ZERO, + ); + layer.update_ruleset().await; + assert_eq!(layer.ruleset.load_full(), Arc::new(Ruleset::default())); + + // Fetch error -> no-op, ruleset unchanged + let layer = WafLayer::new( + Arc::new(ArcSwap::new(Arc::new(Ruleset::default()))), + Some(Arc::new(ErrFetcher) as Arc), + Duration::ZERO, + ); + layer.update_ruleset().await; + assert_eq!(layer.ruleset.load_full(), Arc::new(Ruleset::default())); + + // Fetch success -> ruleset applied + let yaml: &'static [u8] = b"requests:\n- action: block:403\n match: {}\n"; + let layer = WafLayer::new( + Arc::new(ArcSwap::new(Arc::new(Ruleset::default()))), + Some(Arc::new(OkFetcher(yaml)) as Arc), + Duration::ZERO, + ); + layer.update_ruleset().await; + assert_ne!(layer.ruleset.load_full(), Arc::new(Ruleset::default())); + } + + #[tokio::test] + async fn test_waflayer_run_ticks_and_cancels() { + let counter = Arc::new(AtomicUsize::new(0)); + let layer = WafLayer::new( + Arc::new(ArcSwap::new(Arc::new(Ruleset::default()))), + Some(Arc::new(CountingFetcher(counter.clone())) as Arc), + Duration::from_millis(5), + ); + + let token = CancellationToken::new(); + let task = tokio::spawn({ + let token = token.clone(); + async move { layer.run(token).await } + }); + + // Wait until the background loop has ticked at least once + let mut ticked = false; + for _ in 0..200 { + if counter.load(Ordering::SeqCst) > 0 { + ticked = true; + break; + } + tokio::time::sleep(Duration::from_millis(5)).await; + } + assert!(ticked, "run() never invoked the fetcher"); + + token.cancel(); + let res = tokio::time::timeout(Duration::from_secs(1), task).await; + assert!( + res.is_ok(), + "run() did not terminate promptly after cancellation" + ); + } + + #[tokio::test] + async fn test_ruleset_fetcher_file() { + use std::io::Write; + + let mut file = tempfile::NamedTempFile::new().unwrap(); + file.write_all(b"requests:\n- action: block:403\n match: {}\n") + .unwrap(); + + let fetcher = RulesetFetcherFile { + path: file.path().to_path_buf(), + }; + let ruleset = fetcher.fetch_rules().await.unwrap(); + assert!(!ruleset.is_empty()); + + let fetcher = RulesetFetcherFile { + path: PathBuf::from("/nonexistent/path/does-not-exist.yaml"), + }; + assert!(fetcher.fetch_rules().await.is_err()); + } + + #[tokio::test] + async fn test_ruleset_fetcher_url() { + let mut mock = MockClient::new(); + mock.expect_execute().returning(|_req| { + let resp: reqwest::Response = + Response::new("requests:\n- action: block:403\n match: {}\n".to_string()).into(); + Ok(resp) + }); + + let fetcher = RulesetFetcherUrl { + http_client: Arc::new(mock), + url: Url::parse("http://example.com/waf.yaml").unwrap(), + }; + let ruleset = fetcher.fetch_rules().await.unwrap(); + assert!(!ruleset.is_empty()); + } + + #[tokio::test] + async fn test_api_handler() { + let layer = WafLayer::new( + Arc::new(ArcSwap::new(Arc::new(Ruleset::default()))), + None, + Duration::ZERO, + ); + + // Invalid body -> 400 + let resp = api_handler( + State(layer.clone()), + Bytes::from_static(b"{not valid at all"), + ) + .await; + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + + // Valid body -> ruleset updated + let yaml = b"requests:\n- action: block:403\n match: {}\n"; + let resp = api_handler(State(layer.clone()), Bytes::from_static(yaml)).await; + assert_eq!(resp.status(), StatusCode::OK); + let body = axum::body::to_bytes(resp.into_body(), usize::MAX) + .await + .unwrap(); + assert_eq!(&body[..], b"Ruleset updated\n"); + + // Same body again -> reports no change + let resp = api_handler(State(layer.clone()), Bytes::from_static(yaml)).await; + let body = axum::body::to_bytes(resp.into_body(), usize::MAX) + .await + .unwrap(); + assert_eq!(&body[..], b"Ruleset is the same, not updated\n"); + } + + #[tokio::test] + async fn test_create_router() { + let layer = WafLayer::new( + Arc::new(ArcSwap::new(Arc::new(Ruleset::default()))), + None, + Duration::ZERO, + ); + let mut router: Router = create_router(layer); + + let yaml = b"requests:\n- action: block:403\n match: {}\n"; + let req = Request::builder() + .method(Method::POST) + .uri("/update") + .body(Body::from(yaml.to_vec())) + .unwrap(); + let resp = router.call(req).await.unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + } } diff --git a/ic-bn-lib/src/http/server/mod.rs b/ic-bn-lib/src/http/server/mod.rs index 5450f94..c55b6f2 100644 --- a/ic-bn-lib/src/http/server/mod.rs +++ b/ic-bn-lib/src/http/server/mod.rs @@ -28,6 +28,7 @@ use tokio_util::{sync::CancellationToken, task::TaskTracker}; use tracing::{debug, info, warn}; use crate::{ + DurationDisplay, http::{Error, server::metrics::Metrics}, network::{Addr, AsyncReadWrite, ListenerOpts, listener::Listener}, tasks::Run, @@ -323,7 +324,7 @@ impl Server { // Stop accepting new connections drop(listener); - warn!("{self}: shutting down, waiting for the active connections to close for {}s", self.options.grace_period.as_secs()); + warn!("{self}: shutting down, waiting for the active connections to close for {}", self.options.grace_period.display()); self.tracker.close(); select! { diff --git a/ic-bn-lib/src/lb/backend_router.rs b/ic-bn-lib/src/lb/backend_router.rs index 263a1e9..c29c590 100644 --- a/ic-bn-lib/src/lb/backend_router.rs +++ b/ic-bn-lib/src/lb/backend_router.rs @@ -217,7 +217,7 @@ mod test { } } - #[tokio::test] + #[tokio::test(start_paused = true)] async fn test_request_router_somewhat_healthy() { let executor = Arc::new(TestExecutor(Duration::ZERO, Mutex::new(HashMap::new()))); @@ -252,7 +252,7 @@ mod test { drop(h) } - #[tokio::test] + #[tokio::test(start_paused = true)] async fn test_request_router_unhealthy() { let executor = Arc::new(TestExecutor(Duration::ZERO, Mutex::new(HashMap::new()))); diff --git a/ic-bn-lib/src/lb/distributor.rs b/ic-bn-lib/src/lb/distributor.rs index 97d6711..d840062 100644 --- a/ic-bn-lib/src/lb/distributor.rs +++ b/ic-bn-lib/src/lb/distributor.rs @@ -254,7 +254,7 @@ pub(crate) mod test { drop(h) } - #[tokio::test] + #[tokio::test(start_paused = true)] async fn test_distributor_lor() { let backends = vec![ (2, "foo".to_string()), diff --git a/ic-bn-lib/src/lb/health_check.rs b/ic-bn-lib/src/lb/health_check.rs index 3c16574..bae961a 100644 --- a/ic-bn-lib/src/lb/health_check.rs +++ b/ic-bn-lib/src/lb/health_check.rs @@ -314,7 +314,7 @@ mod test { } } - #[tokio::test] + #[tokio::test(start_paused = true)] async fn test_health_checker() { // Some are healthy let target_checker = Arc::new(TestChecker); diff --git a/ic-bn-lib/src/lib.rs b/ic-bn-lib/src/lib.rs index 067f481..7294fd2 100644 --- a/ic-bn-lib/src/lib.rs +++ b/ic-bn-lib/src/lib.rs @@ -26,7 +26,8 @@ pub mod tests; pub mod tls; #[cfg(feature = "vector")] pub mod vector; -use std::{fs::File, net::IpAddr, path::Path}; + +use std::{fmt::Display, fs::File, net::IpAddr, path::Path, time::Duration}; use anyhow::{Context, anyhow}; use bytes::Bytes; @@ -41,6 +42,8 @@ pub use hickory_resolver; pub use hyper; pub use hyper_util; pub use ic_agent; +pub use ic_transport_types; +pub use ipnet; #[cfg(feature = "smtp")] pub use mail_auth; pub use prometheus; @@ -50,6 +53,7 @@ pub use reqwest; pub use rustls; #[cfg(feature = "acme-alpn")] pub use rustls_acme; +pub use show_option; pub use uuid; /// Converts a string representation to an `EmailAddress`. Panics when an error occurs. @@ -64,6 +68,16 @@ macro_rules! principal { ($id:expr) => {{ candid::Principal::from_text($id).unwrap() }}; } +#[doc(hidden)] +pub use regex as __regex; + +/// Converts a string representation to a `regex::Regex`. Panics when an error occurs. +/// Regex crate also has a regex! macro, but it returns a reference to a static value. +#[macro_export] +macro_rules! regex { + ($id:expr) => {{ $crate::__regex::Regex::new($id).unwrap() }}; +} + /// tdb26-jop6k-aogll-7ltgs-eruif-6kk7m-qpktf-gdiqx-mxtrf-vb5e6-eqe pub const MAINNET_ROOT_SUBNET_ID: Principal = Principal::from_slice(&[ 207, 242, 128, 227, 45, 127, 92, 205, 34, 70, 136, 47, 148, 175, 178, 15, 84, 202, 97, 162, 23, @@ -373,6 +387,93 @@ pub fn truncate(s: &str, n: usize) -> &str { &s[..m] } +/// Best-effort (w/o assembly) constant-time comparison for byte slices +pub fn constant_time_eq(a: &[u8], b: &[u8]) -> bool { + if a.len() != b.len() { + return false; + } + + a.iter() + .zip(b.iter()) + .fold(0u8, |acc, (x, y)| acc | (x ^ y)) + == 0 +} + +/// Implements display for Duration +pub trait DurationDisplay<'a> { + /// Implements Display for Duration in a human-friendly format + fn display(&self) -> DisplayDuration; +} + +/// Displays Duration in a human-friendly format +pub struct DisplayDuration(Duration); + +impl DurationDisplay<'_> for Duration { + fn display(&self) -> DisplayDuration { + DisplayDuration(*self) + } +} + +impl Display for DisplayDuration { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut secs = self.0.as_secs(); + // If the duration is shorter than a second - use a distinct approach + if secs == 0 { + let nanos = self.0.as_nanos(); + if nanos < 1000 { + return write!(f, "{nanos}ns"); + } + + let micros = self.0.as_micros(); + if micros < 1000 { + return write!(f, "{micros}us"); + } + + return write!(f, "{}ms", self.0.as_millis()); + } + + // If the duration is shorter than a minute, + // use seconds with a fractional part. + // Truncate (rather than round) the fraction so that e.g. 59.999s + // doesn't get displayed as "60.00s". + if secs < 60 { + let hundredths = self.0.subsec_millis() / 10; + return write!(f, "{secs}.{hundredths:02}s"); + } + + // Average year is 365.24 days... + let years = secs / 365 / 86400; + if years > 0 { + secs -= years * 86400 * 365; + write!(f, "{years}y")?; + } + + let days = secs / 86400; + if days > 0 { + secs -= days * 86400; + write!(f, "{days}d")?; + } + + let hours = secs / 3600; + if hours > 0 { + secs -= hours * 3600; + write!(f, "{hours}h")?; + } + + let mins = secs / 60; + if mins > 0 { + secs -= mins * 60; + write!(f, "{mins}m")?; + } + + if secs > 0 { + write!(f, "{secs}s")?; + } + + Ok(()) + } +} + #[cfg(test)] mod test { use super::*; @@ -418,4 +519,54 @@ mod test { "retry_async! ran for {elapsed:?}, expected to stay close to the {timeout:?} budget" ); } + + #[test] + fn test_constant_time_eq() { + assert!(constant_time_eq(b"foo", b"foo")); + assert!(!constant_time_eq(b"foo", b"bar")); + assert!(!constant_time_eq(b"foobar", b"bar")); + } + + #[test] + fn test_duration_display() { + assert_eq!( + Duration::from_secs(31626061).display().to_string(), + "1y1d1h1m1s" + ); + assert_eq!(Duration::from_hours(72).display().to_string(), "3d"); + assert_eq!(Duration::from_hours(80).display().to_string(), "3d8h"); + assert_eq!(Duration::from_mins(4812).display().to_string(), "3d8h12m"); + assert_eq!( + Duration::from_secs(288735).display().to_string(), + "3d8h12m15s" + ); + assert_eq!( + Duration::from_millis(288735123).display().to_string(), + "3d8h12m15s" + ); + + // Sub-second formatting + assert_eq!(Duration::ZERO.display().to_string(), "0ns"); + assert_eq!(Duration::from_nanos(999).display().to_string(), "999ns"); + assert_eq!(Duration::from_micros(1).display().to_string(), "1us"); + assert_eq!(Duration::from_micros(999).display().to_string(), "999us"); + // Exactly 1000us should roll over to the ms branch, not stay "1000us" + assert_eq!(Duration::from_micros(1000).display().to_string(), "1ms"); + assert_eq!(Duration::from_millis(1).display().to_string(), "1ms"); + assert_eq!(Duration::from_millis(999).display().to_string(), "999ms"); + + // Sub-minute fractional seconds + assert_eq!(Duration::from_millis(1001).display().to_string(), "1.00s"); + assert_eq!(Duration::from_millis(59494).display().to_string(), "59.49s"); + assert_eq!(Duration::from_millis(59999).display().to_string(), "59.99s"); + + // Minute/hour/day/year boundaries, exact and with skipped middle units + assert_eq!(Duration::from_millis(60001).display().to_string(), "1m"); + assert_eq!(Duration::from_secs(60).display().to_string(), "1m"); + assert_eq!(Duration::from_secs(3600).display().to_string(), "1h"); + assert_eq!(Duration::from_secs(3605).display().to_string(), "1h5s"); + assert_eq!(Duration::from_secs(86400).display().to_string(), "1d"); + assert_eq!(Duration::from_secs(86400 + 5).display().to_string(), "1d5s"); + assert_eq!(Duration::from_secs(365 * 86400).display().to_string(), "1y"); + } } diff --git a/ic-bn-lib/src/tasks/mod.rs b/ic-bn-lib/src/tasks/mod.rs index da20088..cfd4cef 100644 --- a/ic-bn-lib/src/tasks/mod.rs +++ b/ic-bn-lib/src/tasks/mod.rs @@ -5,6 +5,8 @@ use derive_new::new; use tokio_util::{sync::CancellationToken, task::TaskTracker}; use tracing::{error, warn}; +use crate::DurationDisplay; + // A runnable task that can be cancelled by a token #[async_trait] pub trait Run: Send + Sync { @@ -27,9 +29,9 @@ struct IntervalRunner(Duration, Task); impl Run for IntervalRunner { async fn run(&self, token: CancellationToken) -> Result<(), anyhow::Error> { warn!( - "Task '{}': running with interval {}s", + "Task '{}': running with interval {}", self.1, - self.0.as_secs() + self.0.display() ); let mut interval = tokio::time::interval(self.0); @@ -135,7 +137,7 @@ mod test { } } - #[tokio::test] + #[tokio::test(start_paused = true)] async fn test_add_runs_task_once() { let count = Arc::new(AtomicUsize::new(0)); @@ -150,7 +152,7 @@ mod test { assert_eq!(count.load(Ordering::SeqCst), 1); } - #[tokio::test] + #[tokio::test(start_paused = true)] async fn test_add_interval_runs_task_repeatedly() { let count = Arc::new(AtomicUsize::new(0)); @@ -172,7 +174,7 @@ mod test { ); } - #[tokio::test] + #[tokio::test(start_paused = true)] async fn test_stop_cancels_running_tasks() { let count = Arc::new(AtomicUsize::new(0)); diff --git a/ic-bn-lib/src/tls/acme/dns/ic_dns_lb.rs b/ic-bn-lib/src/tls/acme/dns/ic_dns_lb.rs index b24fe65..156e77b 100644 --- a/ic-bn-lib/src/tls/acme/dns/ic_dns_lb.rs +++ b/ic-bn-lib/src/tls/acme/dns/ic_dns_lb.rs @@ -113,6 +113,7 @@ impl DnsManager for IcDnsLb { let mut url = url.clone(); // Strip trailing slash if exists & add path + // SAFETY: cannot-be-a-base is checked in new() url.path_segments_mut() .unwrap() .pop_if_empty() @@ -139,6 +140,7 @@ impl DnsManager for IcDnsLb { let mut url = url.clone(); // Strip trailing slash if exists & add path + // SAFETY: cannot-be-a-base is checked in new() url.path_segments_mut().unwrap().pop_if_empty().extend([ "acme-challenge", "unset", diff --git a/ic-bn-lib/src/tls/acme/dns/mod.rs b/ic-bn-lib/src/tls/acme/dns/mod.rs index 0cdf73f..2825f28 100644 --- a/ic-bn-lib/src/tls/acme/dns/mod.rs +++ b/ic-bn-lib/src/tls/acme/dns/mod.rs @@ -420,6 +420,10 @@ mod test { pub fn insecure_http_client() -> reqwest::Client { reqwest::Client::builder() .danger_accept_invalid_certs(true) + // Sometimes the connection is torn down during a request execution + // and that causes test flakiness. Setting 0 here disables idle connections + // and forces it to open a new one for each request. + .pool_max_idle_per_host(0) .build() .unwrap() } diff --git a/ic-bn-lib/src/vector/client.rs b/ic-bn-lib/src/vector/client.rs index 07992cb..cde38b7 100644 --- a/ic-bn-lib/src/vector/client.rs +++ b/ic-bn-lib/src/vector/client.rs @@ -23,6 +23,7 @@ use tracing::{debug, warn}; use url::Url; use crate::{ + DurationDisplay, http::{Client as HttpClient, client::basic_auth, headers::CONTENT_TYPE_OCTET_STREAM}, hval, vector::{self, VectorOptions}, @@ -540,9 +541,9 @@ impl Flusher { .inc(); warn!( - "{self}: unable to send (try {}, retry interval {}s): {e:#}", + "{self}: unable to send (try {}, retry interval {}): {e:#}", retries, - interval.as_secs_f64() + interval.display() ); } else { self.metrics @@ -558,7 +559,7 @@ impl Flusher { .with_label_values(&[self.namespace.as_str(), "yes"]) .inc(); - debug!("{self}: batch sent in {}s", start.elapsed().as_secs_f64()); + debug!("{self}: batch sent in {}", start.elapsed().display()); return Ok(()); } @@ -858,7 +859,7 @@ mod test { } /// Make sure we can drain when the endpoint is down - #[tokio::test] + #[tokio::test(start_paused = true)] async fn test_vector_drain_dead() { let client = Arc::new(TestClientDead); let vector = Vector::new(make_opts(), client, "", &Registry::new()); diff --git a/ic-bn-lib/test-data/geoip-test-db.mmdb b/ic-bn-lib/test-data/geoip-test-db.mmdb new file mode 100644 index 0000000..840f893 Binary files /dev/null and b/ic-bn-lib/test-data/geoip-test-db.mmdb differ