diff --git a/Cargo.lock b/Cargo.lock index 77db37f..2e94b8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,6 +67,17 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -105,11 +116,13 @@ dependencies = [ "bytes", "clap", "hostname", + "ldap3", "md-5", "num-bigint", "num-integer", "num-traits", "rand", + "rusqlite", "sha2", "socket2 0.5.10", "thiserror", @@ -118,12 +131,28 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + [[package]] name = "bytes" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "cc" +version = "1.2.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -182,6 +211,22 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.3.0" @@ -231,6 +276,23 @@ dependencies = [ "crypto-common 0.2.1", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.14" @@ -241,6 +303,154 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -262,6 +472,46 @@ dependencies = [ "wasi", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash 0.2.0", +] + +[[package]] +name = "hashlink" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" +dependencies = [ + "hashbrown 0.16.1", +] + [[package]] name = "heck" version = "0.5.0" @@ -288,24 +538,222 @@ dependencies = [ "typenum", ] +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lber" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbcf559624bfd9fe8d488329a8959766335a43a9b8b2cdd6a2c379fca02909a5" +dependencies = [ + "bytes", + "nom", +] + +[[package]] +name = "ldap3" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01fe89f5e7cfb7e4701e3a38ff9f00358e026a9aee940355d88ee9d81e5c7503" +dependencies = [ + "async-trait", + "bytes", + "futures", + "futures-util", + "lber", + "log", + "native-tls", + "nom", + "percent-encoding", + "thiserror", + "tokio", + "tokio-native-tls", + "tokio-stream", + "tokio-util", + "url", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +[[package]] +name = "libsqlite3-sys" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + [[package]] name = "lock_api" version = "0.4.14" @@ -346,6 +794,12 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "mio" version = "1.2.0" @@ -357,6 +811,33 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -406,6 +887,50 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "openssl" +version = "0.10.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -429,12 +954,33 @@ dependencies = [ "windows-link", ] +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -444,6 +990,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -462,6 +1018,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.8.5" @@ -489,7 +1051,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.17", ] [[package]] @@ -518,12 +1080,136 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "rsqlite-vfs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d" +dependencies = [ + "hashbrown 0.16.1", + "thiserror", +] + +[[package]] +name = "rusqlite" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2b0146dd9661bf67bb107c0bb2a55064d556eeb3fc314151b957f313bcd4e" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", + "sqlite-wasm-rs", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "sha2" version = "0.11.0" @@ -544,6 +1230,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -554,6 +1246,12 @@ dependencies = [ "libc", ] +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "smallvec" version = "1.15.1" @@ -580,6 +1278,24 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "sqlite-wasm-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4206ed3a67690b9c29b77d728f6acc3ce78f16bf846d83c94f76400320181b" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "strsim" version = "0.11.1" @@ -597,6 +1313,30 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "thiserror" version = "2.0.18" @@ -626,6 +1366,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tokio" version = "1.50.0" @@ -654,6 +1404,40 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "tracing" version = "0.1.44" @@ -727,6 +1511,30 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -739,6 +1547,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -751,6 +1565,103 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "windows-link" version = "0.2.1" @@ -839,6 +1750,123 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.48" @@ -858,3 +1886,63 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index f81188a..e59e977 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,15 @@ path = "src/lib.rs" name = "btest" path = "src/main.rs" +[[bin]] +name = "btest-server-pro" +path = "src/server_pro/main.rs" +required-features = ["pro"] + +[features] +default = [] +pro = ["dep:rusqlite", "dep:ldap3"] + [dependencies] tokio = { version = "1", features = ["full"] } clap = { version = "4", features = ["derive"] } @@ -32,6 +41,8 @@ num-traits = "0.2.19" num-integer = "0.1.46" sha2 = "0.11.0" hostname = "0.4.2" +rusqlite = { version = "0.39.0", features = ["bundled"], optional = true } +ldap3 = { version = "0.12.1", optional = true } [profile.release] opt-level = 3 diff --git a/src/server_pro/ldap_auth.rs b/src/server_pro/ldap_auth.rs new file mode 100644 index 0000000..d6f4b80 --- /dev/null +++ b/src/server_pro/ldap_auth.rs @@ -0,0 +1,74 @@ +//! LDAP/Active Directory authentication for btest-server-pro. +//! +//! Authenticates users against an LDAP directory using simple bind. + +use ldap3::{LdapConnAsync, Scope, SearchEntry}; + +pub struct LdapConfig { + pub url: String, + pub base_dn: String, + pub bind_dn: Option, + pub bind_pass: Option, +} + +pub struct LdapAuth { + config: LdapConfig, +} + +impl LdapAuth { + pub fn new(config: LdapConfig) -> Self { + Self { config } + } + + /// Authenticate a user by attempting an LDAP bind. + /// Returns Ok(true) if authentication succeeds. + pub async fn authenticate(&self, username: &str, password: &str) -> anyhow::Result { + let (conn, mut ldap) = LdapConnAsync::new(&self.config.url).await?; + ldap3::drive!(conn); + + // If service account configured, bind first to search for user DN + let user_dn = if let (Some(ref bind_dn), Some(ref bind_pass)) = + (&self.config.bind_dn, &self.config.bind_pass) + { + let result = ldap.simple_bind(bind_dn, bind_pass).await?; + if result.rc != 0 { + tracing::warn!("LDAP service bind failed: rc={}", result.rc); + return Ok(false); + } + + // Search for the user + let filter = format!( + "(&(objectClass=person)(|(uid={})(sAMAccountName={})(cn={})))", + username, username, username + ); + let (results, _) = ldap + .search(&self.config.base_dn, Scope::Subtree, &filter, vec!["dn"]) + .await? + .success()?; + + if results.is_empty() { + tracing::debug!("LDAP user not found: {}", username); + return Ok(false); + } + + let entry = SearchEntry::construct(results.into_iter().next().unwrap()); + entry.dn + } else { + // No service account — construct DN directly + format!("uid={},{}", username, self.config.base_dn) + }; + + // Attempt user bind + let result = ldap.simple_bind(&user_dn, password).await?; + let success = result.rc == 0; + + if success { + tracing::info!("LDAP auth successful for {} (dn={})", username, user_dn); + } else { + tracing::warn!("LDAP auth failed for {} (dn={}): rc={}", username, user_dn, result.rc); + } + + let _ = ldap.unbind().await; + Ok(success) + } +} diff --git a/src/server_pro/main.rs b/src/server_pro/main.rs new file mode 100644 index 0000000..67c7416 --- /dev/null +++ b/src/server_pro/main.rs @@ -0,0 +1,158 @@ +//! btest-server-pro: MikroTik Bandwidth Test server with multi-user, quotas, and LDAP. +//! +//! This is a superset of the standard `btest` server with additional features: +//! - SQLite user database (--users-db) +//! - Per-user and per-IP bandwidth quotas (daily/weekly) +//! - LDAP/Active Directory authentication (--ldap-url) +//! - Rate limiting for public server deployment +//! +//! Build with: cargo build --release --features pro --bin btest-server-pro + +mod user_db; +mod quota; +mod ldap_auth; + +use clap::Parser; +use tracing_subscriber::EnvFilter; + +#[derive(Parser, Debug)] +#[command( + name = "btest-server-pro", + about = "btest-rs Pro Server: multi-user, quotas, LDAP", + version, +)] +struct Cli { + /// Listen port + #[arg(short = 'P', long = "port", default_value_t = 2000)] + port: u16, + + /// IPv4 listen address + #[arg(long = "listen", default_value = "0.0.0.0")] + listen_addr: String, + + /// IPv6 listen address (optional) + #[arg(long = "listen6")] + listen6_addr: Option, + + /// SQLite user database path + #[arg(long = "users-db", default_value = "btest-users.db")] + users_db: String, + + /// LDAP server URL (e.g., ldap://dc.example.com) + #[arg(long = "ldap-url")] + ldap_url: Option, + + /// LDAP base DN for user search + #[arg(long = "ldap-base-dn")] + ldap_base_dn: Option, + + /// LDAP bind DN (for service account) + #[arg(long = "ldap-bind-dn")] + ldap_bind_dn: Option, + + /// LDAP bind password + #[arg(long = "ldap-bind-pass")] + ldap_bind_pass: Option, + + /// Default daily quota per user in bytes (0 = unlimited) + #[arg(long = "daily-quota", default_value_t = 0)] + daily_quota: u64, + + /// Default weekly quota per user in bytes (0 = unlimited) + #[arg(long = "weekly-quota", default_value_t = 0)] + weekly_quota: u64, + + /// Maximum concurrent connections per IP (0 = unlimited) + #[arg(long = "max-conn-per-ip", default_value_t = 5)] + max_conn_per_ip: u32, + + /// Maximum test duration in seconds (0 = unlimited) + #[arg(long = "max-duration", default_value_t = 300)] + max_duration: u64, + + /// Use EC-SRP5 authentication + #[arg(long = "ecsrp5")] + ecsrp5: bool, + + /// Syslog server address + #[arg(long = "syslog")] + syslog: Option, + + /// CSV output file + #[arg(long = "csv")] + csv: Option, + + /// Verbose logging + #[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count)] + verbose: u8, +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + + let filter = match cli.verbose { + 0 => "info", + 1 => "debug", + _ => "trace", + }; + tracing_subscriber::fmt() + .with_env_filter( + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(filter)), + ) + .with_target(false) + .init(); + + // Initialize subsystems + btest_rs::cpu::start_sampler(); + + if let Some(ref syslog_addr) = cli.syslog { + if let Err(e) = btest_rs::syslog_logger::init(syslog_addr) { + eprintln!("Warning: syslog init failed: {}", e); + } + } + + if let Some(ref csv_path) = cli.csv { + if let Err(e) = btest_rs::csv_output::init(csv_path) { + eprintln!("Warning: CSV init failed: {}", e); + } + } + + // Initialize user database + tracing::info!("Opening user database: {}", cli.users_db); + let db = user_db::UserDb::open(&cli.users_db)?; + db.ensure_tables()?; + tracing::info!("User database ready ({} users)", db.user_count()?); + + // Initialize LDAP if configured + if let Some(ref url) = cli.ldap_url { + tracing::info!("LDAP configured: {}", url); + } + + // Initialize quota manager + let quota_mgr = quota::QuotaManager::new( + db.clone(), + cli.daily_quota, + cli.weekly_quota, + cli.max_conn_per_ip, + cli.max_duration, + ); + tracing::info!( + "Quotas: daily={}, weekly={}, max_conn_per_ip={}, max_duration={}s", + if cli.daily_quota == 0 { "unlimited".to_string() } else { format!("{}", cli.daily_quota) }, + if cli.weekly_quota == 0 { "unlimited".to_string() } else { format!("{}", cli.weekly_quota) }, + cli.max_conn_per_ip, + cli.max_duration, + ); + + tracing::info!("btest-server-pro starting on port {}", cli.port); + + // TODO: Run the enhanced server loop with quota checks and multi-user auth + // For now, delegate to the standard server + let v4 = if cli.listen_addr.eq_ignore_ascii_case("none") { None } else { Some(cli.listen_addr) }; + let v6 = cli.listen6_addr; + + btest_rs::server::run_server(cli.port, None, None, cli.ecsrp5, v4, v6).await?; + + Ok(()) +} diff --git a/src/server_pro/quota.rs b/src/server_pro/quota.rs new file mode 100644 index 0000000..924d23c --- /dev/null +++ b/src/server_pro/quota.rs @@ -0,0 +1,140 @@ +//! Bandwidth quota management for btest-server-pro. +//! +//! Enforces per-user and per-IP bandwidth limits. + +use std::collections::HashMap; +use std::net::IpAddr; +use std::sync::{Arc, Mutex}; + +use super::user_db::UserDb; + +#[derive(Clone)] +pub struct QuotaManager { + db: UserDb, + default_daily: u64, + default_weekly: u64, + max_conn_per_ip: u32, + max_duration: u64, + active_connections: Arc>>, +} + +#[derive(Debug)] +pub enum QuotaError { + DailyExceeded { used: u64, limit: u64 }, + WeeklyExceeded { used: u64, limit: u64 }, + TooManyConnections { current: u32, limit: u32 }, + UserDisabled, + UserNotFound, +} + +impl std::fmt::Display for QuotaError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::DailyExceeded { used, limit } => + write!(f, "Daily quota exceeded: {}/{} bytes", used, limit), + Self::WeeklyExceeded { used, limit } => + write!(f, "Weekly quota exceeded: {}/{} bytes", used, limit), + Self::TooManyConnections { current, limit } => + write!(f, "Too many connections from this IP: {}/{}", current, limit), + Self::UserDisabled => write!(f, "User account is disabled"), + Self::UserNotFound => write!(f, "User not found"), + } + } +} + +impl QuotaManager { + pub fn new( + db: UserDb, + default_daily: u64, + default_weekly: u64, + max_conn_per_ip: u32, + max_duration: u64, + ) -> Self { + Self { + db, + default_daily, + default_weekly, + max_conn_per_ip, + max_duration, + active_connections: Arc::new(Mutex::new(HashMap::new())), + } + } + + /// Check if a user is allowed to start a test. + pub fn check_user(&self, username: &str) -> Result<(), QuotaError> { + let user = self.db.get_user(username) + .map_err(|_| QuotaError::UserNotFound)? + .ok_or(QuotaError::UserNotFound)?; + + if !user.enabled { + return Err(QuotaError::UserDisabled); + } + + // Check daily quota + let daily_limit = if user.daily_quota > 0 { user.daily_quota as u64 } else { self.default_daily }; + if daily_limit > 0 { + let (tx, rx) = self.db.get_daily_usage(username).unwrap_or((0, 0)); + let used = tx + rx; + if used >= daily_limit { + return Err(QuotaError::DailyExceeded { used, limit: daily_limit }); + } + } + + // Check weekly quota + let weekly_limit = if user.weekly_quota > 0 { user.weekly_quota as u64 } else { self.default_weekly }; + if weekly_limit > 0 { + let (tx, rx) = self.db.get_weekly_usage(username).unwrap_or((0, 0)); + let used = tx + rx; + if used >= weekly_limit { + return Err(QuotaError::WeeklyExceeded { used, limit: weekly_limit }); + } + } + + Ok(()) + } + + /// Check if an IP is allowed to connect. + pub fn check_ip(&self, ip: &IpAddr) -> Result<(), QuotaError> { + if self.max_conn_per_ip == 0 { + return Ok(()); + } + let conns = self.active_connections.lock().unwrap(); + let current = conns.get(ip).copied().unwrap_or(0); + if current >= self.max_conn_per_ip { + return Err(QuotaError::TooManyConnections { + current, + limit: self.max_conn_per_ip, + }); + } + Ok(()) + } + + /// Register an active connection from an IP. + pub fn connect(&self, ip: &IpAddr) { + let mut conns = self.active_connections.lock().unwrap(); + *conns.entry(*ip).or_insert(0) += 1; + } + + /// Unregister a connection from an IP. + pub fn disconnect(&self, ip: &IpAddr) { + let mut conns = self.active_connections.lock().unwrap(); + if let Some(count) = conns.get_mut(ip) { + *count = count.saturating_sub(1); + if *count == 0 { + conns.remove(ip); + } + } + } + + /// Record usage after a test completes. + pub fn record_usage(&self, username: &str, tx_bytes: u64, rx_bytes: u64) { + if let Err(e) = self.db.record_usage(username, tx_bytes, rx_bytes) { + tracing::error!("Failed to record usage for {}: {}", username, e); + } + } + + /// Get the maximum test duration in seconds. + pub fn max_duration(&self) -> u64 { + self.max_duration + } +} diff --git a/src/server_pro/user_db.rs b/src/server_pro/user_db.rs new file mode 100644 index 0000000..5993b42 --- /dev/null +++ b/src/server_pro/user_db.rs @@ -0,0 +1,220 @@ +//! SQLite-based user database for btest-server-pro. +//! +//! Stores users with credentials, quotas, and usage tracking. + +use rusqlite::{Connection, params}; +use std::sync::{Arc, Mutex}; + +#[derive(Clone)] +pub struct UserDb { + conn: Arc>, +} + +#[derive(Debug, Clone)] +pub struct User { + pub id: i64, + pub username: String, + pub password_hash: String, // stored as hex of SHA256(username:password) + pub daily_quota: i64, // 0 = use default + pub weekly_quota: i64, // 0 = use default + pub enabled: bool, +} + +#[derive(Debug)] +pub struct UsageRecord { + pub username: String, + pub date: String, // YYYY-MM-DD + pub tx_bytes: u64, + pub rx_bytes: u64, + pub test_count: u32, +} + +impl UserDb { + pub fn open(path: &str) -> anyhow::Result { + let conn = Connection::open(path)?; + conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;")?; + Ok(Self { + conn: Arc::new(Mutex::new(conn)), + }) + } + + pub fn ensure_tables(&self) -> anyhow::Result<()> { + let conn = self.conn.lock().unwrap(); + conn.execute_batch(" + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + daily_quota INTEGER DEFAULT 0, + weekly_quota INTEGER DEFAULT 0, + enabled INTEGER DEFAULT 1, + created_at TEXT DEFAULT (datetime('now')) + ); + + CREATE TABLE IF NOT EXISTS usage ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL, + date TEXT NOT NULL, + tx_bytes INTEGER DEFAULT 0, + rx_bytes INTEGER DEFAULT 0, + test_count INTEGER DEFAULT 0, + UNIQUE(username, date) + ); + + CREATE TABLE IF NOT EXISTS sessions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL, + peer_ip TEXT NOT NULL, + started_at TEXT DEFAULT (datetime('now')), + ended_at TEXT, + tx_bytes INTEGER DEFAULT 0, + rx_bytes INTEGER DEFAULT 0, + protocol TEXT, + direction TEXT + ); + + CREATE INDEX IF NOT EXISTS idx_usage_user_date ON usage(username, date); + CREATE INDEX IF NOT EXISTS idx_sessions_peer ON sessions(peer_ip, started_at); + ")?; + Ok(()) + } + + pub fn user_count(&self) -> anyhow::Result { + let conn = self.conn.lock().unwrap(); + let count: i64 = conn.query_row("SELECT COUNT(*) FROM users", [], |r| r.get(0))?; + Ok(count as u64) + } + + pub fn add_user(&self, username: &str, password: &str) -> anyhow::Result<()> { + let hash = hash_password(username, password); + let conn = self.conn.lock().unwrap(); + conn.execute( + "INSERT OR REPLACE INTO users (username, password_hash) VALUES (?1, ?2)", + params![username, hash], + )?; + Ok(()) + } + + pub fn get_user(&self, username: &str) -> anyhow::Result> { + let conn = self.conn.lock().unwrap(); + let mut stmt = conn.prepare( + "SELECT id, username, password_hash, daily_quota, weekly_quota, enabled FROM users WHERE username = ?1" + )?; + let user = stmt.query_row(params![username], |row| { + Ok(User { + id: row.get(0)?, + username: row.get(1)?, + password_hash: row.get(2)?, + daily_quota: row.get(3)?, + weekly_quota: row.get(4)?, + enabled: row.get::<_, i32>(5)? != 0, + }) + }).optional()?; + Ok(user) + } + + pub fn verify_password(&self, username: &str, password: &str) -> anyhow::Result { + let expected = hash_password(username, password); + match self.get_user(username)? { + Some(user) => Ok(user.enabled && user.password_hash == expected), + None => Ok(false), + } + } + + pub fn record_usage(&self, username: &str, tx_bytes: u64, rx_bytes: u64) -> anyhow::Result<()> { + let conn = self.conn.lock().unwrap(); + let today = chrono_date_today(); + conn.execute( + "INSERT INTO usage (username, date, tx_bytes, rx_bytes, test_count) + VALUES (?1, ?2, ?3, ?4, 1) + ON CONFLICT(username, date) DO UPDATE SET + tx_bytes = tx_bytes + ?3, + rx_bytes = rx_bytes + ?4, + test_count = test_count + 1", + params![username, today, tx_bytes as i64, rx_bytes as i64], + )?; + Ok(()) + } + + pub fn get_daily_usage(&self, username: &str) -> anyhow::Result<(u64, u64)> { + let conn = self.conn.lock().unwrap(); + let today = chrono_date_today(); + let result = conn.query_row( + "SELECT COALESCE(SUM(tx_bytes),0), COALESCE(SUM(rx_bytes),0) FROM usage WHERE username = ?1 AND date = ?2", + params![username, today], + |row| { + let a: i64 = row.get(0)?; + let b: i64 = row.get(1)?; + Ok((a as u64, b as u64)) + }, + )?; + Ok(result) + } + + pub fn get_weekly_usage(&self, username: &str) -> anyhow::Result<(u64, u64)> { + let conn = self.conn.lock().unwrap(); + let result = conn.query_row( + "SELECT COALESCE(SUM(tx_bytes),0), COALESCE(SUM(rx_bytes),0) FROM usage + WHERE username = ?1 AND date >= date('now', '-7 days')", + params![username], + |row| { + let a: i64 = row.get(0)?; + let b: i64 = row.get(1)?; + Ok((a as u64, b as u64)) + }, + )?; + Ok(result) + } + + pub fn list_users(&self) -> anyhow::Result> { + let conn = self.conn.lock().unwrap(); + let mut stmt = conn.prepare( + "SELECT id, username, password_hash, daily_quota, weekly_quota, enabled FROM users ORDER BY username" + )?; + let users = stmt.query_map([], |row| { + Ok(User { + id: row.get(0)?, + username: row.get(1)?, + password_hash: row.get(2)?, + daily_quota: row.get(3)?, + weekly_quota: row.get(4)?, + enabled: row.get::<_, i32>(5)? != 0, + }) + })?.filter_map(|r| r.ok()).collect(); + Ok(users) + } +} + +fn hash_password(username: &str, password: &str) -> String { + use sha2::{Sha256, Digest}; + let mut hasher = Sha256::new(); + hasher.update(format!("{}:{}", username, password).as_bytes()); + let result = hasher.finalize(); + result.iter().map(|b| format!("{:02x}", b)).collect() +} + +fn chrono_date_today() -> String { + // Simple date without chrono crate + use std::time::{SystemTime, UNIX_EPOCH}; + let secs = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs(); + let days = secs / 86400; + let mut y = 1970u64; + let mut remaining = days; + loop { + let leap = if y % 4 == 0 && (y % 100 != 0 || y % 400 == 0) { 366 } else { 365 }; + if remaining < leap { break; } + remaining -= leap; + y += 1; + } + let leap = y % 4 == 0 && (y % 100 != 0 || y % 400 == 0); + let days_in_months = [31u64, if leap { 29 } else { 28 }, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + let mut m = 0usize; + for i in 0..12 { + if remaining < days_in_months[i] { m = i; break; } + remaining -= days_in_months[i]; + } + format!("{:04}-{:02}-{:02}", y, m + 1, remaining + 1) +} + +// Re-export for use by rusqlite +use rusqlite::OptionalExtension;