commit d42508b15eb9d1b9f4f96af6b5dbee8f82df9bda Author: selfhoster1312 Date: Thu Aug 20 21:05:09 2026 +0200 feat: Initial implementation (bind/search/whoami) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..de82c5b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1164 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[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.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "humantime" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15" + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92a7ed671a6aad807a8651a2e1782a6598fda9ce5185dd8158549e95a91c6428" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[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", + "nom", + "percent-encoding", + "thiserror", + "tokio", + "tokio-stream", + "tokio-util", + "url", +] + +[[package]] +name = "ldap3_lber" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf85b08fb467260c0f8f305b30e819cf3e9584f0f50f2879fbec9bc58f639504" +dependencies = [ + "bytes", + "nom", +] + +[[package]] +name = "ldap3_proto" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "105256b138a7ed84ac1ae375870eae23be5f8fed16ca172ae5f1c1aa37bd4242" +dependencies = [ + "base64", + "bytes", + "ldap3_lber", + "nom", + "peg", + "serde", + "thiserror", + "tokio-util", + "tracing", + "uuid", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "multildap" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "futures-util", + "indexmap", + "ldap3", + "ldap3_proto", + "log", + "pretty_env_logger", + "serde", + "tokio", + "tokio-util", + "toml", +] + +[[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 = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "peg" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aad070be5b63aa72103f2fcdd70a83adbd5e90112ce5b574171ff1c65501773" +dependencies = [ + "peg-macros", + "peg-runtime", +] + +[[package]] +name = "peg-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd8ef6825cae95355031ae26a99b616a2a21f22ba2de0197c43dfb05acbe7ee" +dependencies = [ + "peg-runtime", + "proc-macro2", + "quote", +] + +[[package]] +name = "peg-runtime" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7011d97b484a5ebdc4b1fdb3b12d5e4bbbea56e9d22b688f2e79e04b65a7d8a6" + +[[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 = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "pretty_env_logger" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c" +dependencies = [ + "env_logger", + "log", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "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 2.0.119", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "1.1.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cefc03fd367c0c6d4305de1b312cf00248c4114f4a0418ce6a6af769e3b0bd9" +dependencies = [ + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f212a141d820099d57ffafb9569be9617a6f27d3dc881fbee8fb56642f917a9" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..60e5c1b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "multildap" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = "1.0.104" +clap = { version = "4.6.6", features = ["derive"] } +futures-util = { version = "^0.3.32", features = [ "sink" ] } +indexmap = "2.14.0" +ldap3 = { version = "0.12.1", default-features = false } +ldap3_proto = { version = "0.8.0", features = ["serde"] } +log = "0.4.33" +pretty_env_logger = "0.5.0" +serde = { version = "1.0.229", features = ["derive"] } +tokio = { version = "1.53.1", features = ["net", "rt", "macros", "time", "io-util", "fs"] } +tokio-util = "0.7.19" +toml = "1.1.4" diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..74dee48 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,327 @@ +# Mozilla Public License Version 2.0 + +1. Definitions + +--- + +1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation +of, or owns Covered Software. + +1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a +Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" means Covered Software of a particular Contributor. + +1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the +notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source +Code Form, in each case including portions thereof. + +1.5. "Incompatible With Secondary Licenses" means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" means any form of the work other than Source Code Form. + +1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate +file or files, that is not Covered Software. + +1.8. "License" means this document. + +1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the +time of the initial grant or subsequently, any and all of the rights conveyed by this License. + +1.10. "Modifications" means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, +method, process, and apparatus claims, in any patent Licensable by such Contributor that would be +infringed, but for the grant of the License, by the making, using, selling, offering for sale, +having made, import, or transfer of either its Contributions or its Contributor Version. + +1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser +General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any +later versions of those licenses. + +1.13. "Source Code Form" means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. +For legal entities, "You" includes any entity that controls, is controlled by, or is under common +control with You. For purposes of this definition, "control" means (a) the power, direct or +indirect, to cause the direction or management of such entity, whether by contract or otherwise, or +(b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of +such entity. + +2. License Grants and Conditions + +--- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) Licensable by such +Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise +exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger +Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, +and otherwise transfer either its Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution become effective for each +Contribution on the date the Contributor first distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under this License. No additional +rights or licenses will be implied from the distribution or licensing of Covered Software under this +License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: + +(a) for any code that a Contributor has removed from Covered Software; or + +(b) for infringements caused by: (i) Your and any other third party's modifications of Covered +Software, or (ii) the combination of its Contributions with other software (except as part of its +Contributor Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of its Contributions. + +This License does not grant any rights in the trademarks, service marks, or logos of any Contributor +(except as may be necessary to comply with the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to distribute the Covered Software +under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary +License (if permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its Contributions are its original +creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this +License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under applicable copyright doctrines of +fair use, fair dealing, or other equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. + +3. Responsibilities + +--- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any Modifications that You +create or to which You contribute, must be under the terms of this License. You must inform +recipients that the Source Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not attempt to alter or restrict +the recipients' rights in the Source Code Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code Form, as described in Section +3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source +Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution +to the recipient; and + +(b) You may distribute such Executable Form under the terms of this License, or sublicense it under +different terms, provided that the license for the Executable Form does not attempt to limit or +alter the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, provided that You also +comply with the requirements of this License for the Covered Software. If the Larger Work is a +combination of Covered Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this License permits You to +additionally distribute such Covered Software under the terms of such Secondary License(s), so that +the recipient of the Larger Work may, at their option, further distribute the Covered Software under +the terms of either this License or such Secondary License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices (including copyright notices, +patent notices, disclaimers of warranty, or limitations of liability) contained within the Source +Code Form of the Covered Software, except that You may alter any license notices to the extent +required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability +obligations to one or more recipients of Covered Software. However, You may do so only on Your own +behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such +warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree +to indemnify every Contributor for any liability incurred by such Contributor as a result of +warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of +warranty and limitations of liability specific to any jurisdiction. + +4. Inability to Comply Due to Statute or Regulation + +--- + +If it is impossible for You to comply with any of the terms of this License with respect to some or +all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply +with the terms of this License to the maximum extent possible; and (b) describe the limitations and +the code they affect. Such description must be placed in a text file included with all distributions +of the Covered Software under this License. Except to the extent prohibited by statute or +regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be +able to understand it. + +5. Termination + +--- + +5.1. The rights granted under this License will terminate automatically if You fail to comply with +any of its terms. However, if You become compliant, then the rights granted under this License from +a particular Contributor are reinstated (a) provisionally, unless and until such Contributor +explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor +fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an +ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this +is the first time You have received notice of non-compliance with this License from such +Contributor, and You become compliant prior to 30 days after Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent infringement claim +(excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a +Contributor Version directly or indirectly infringes any patent, then the rights granted to You by +any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements +(excluding distributors and resellers) which have been validly granted by You or Your distributors +under this License prior to termination shall survive termination. + +--- + +- + - +- + 6. Disclaimer of Warranty * +- ------------------------- * +- + - +- Covered Software is provided under this License on an "as is" * +- basis, without warranty of any kind, either expressed, implied, or * +- statutory, including, without limitation, warranties that the * +- Covered Software is free of defects, merchantable, fit for a * +- particular purpose or non-infringing. The entire risk as to the * +- quality and performance of the Covered Software is with You. * +- Should any Covered Software prove defective in any respect, You * +- (not any Contributor) assume the cost of any necessary servicing, * +- repair, or correction. This disclaimer of warranty constitutes an * +- essential part of this License. No use of any Covered Software is * +- authorized under this License except under this disclaimer. * +- + - + +--- + +--- + +- + - +- + 7. Limitation of Liability * +- -------------------------- * +- + - +- Under no circumstances and under no legal theory, whether tort * +- (including negligence), contract, or otherwise, shall any * +- Contributor, or anyone who distributes Covered Software as * +- permitted above, be liable to You for any direct, indirect, * +- special, incidental, or consequential damages of any character * +- including, without limitation, damages for lost profits, loss of * +- goodwill, work stoppage, computer failure or malfunction, or any * +- and all other commercial damages or losses, even if such party * +- shall have been informed of the possibility of such damages. This * +- limitation of liability shall not apply to liability for death or * +- personal injury resulting from such party's negligence to the * +- extent applicable law prohibits such limitation. Some * +- jurisdictions do not allow the exclusion or limitation of * +- incidental or consequential damages, so this exclusion and * +- limitation may not apply to You. * +- + - + +--- + +8. Litigation + +--- + +Any litigation relating to this License may be brought only in the courts of a jurisdiction where +the defendant maintains its principal place of business and such litigation shall be governed by +laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this +Section shall prevent a party's ability to bring cross-claims or counter-claims. + +9. Miscellaneous + +--- + +This License represents the complete agreement concerning the subject matter hereof. If any +provision of this License is held to be unenforceable, such provision shall be reformed only to the +extent necessary to make it enforceable. Any law or regulation which provides that the language of a +contract shall be construed against the drafter shall not be used to construe this License against a +Contributor. + +10. Versions of the License + +--- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the +license steward has the right to modify or publish new versions of this License. Each version will +be given a distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version of the License under which +You originally received the Covered Software, or under the terms of any subsequent version published +by the license steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to create a new license for such +software, you may create and use a modified version of this License if you rename the license and +remove any references to the name of the license steward (except to note that such modified license +differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses + +If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the +terms of this version of the License, the notice described in Exhibit B of this License must be +attached. + +## Exhibit A - Source Code Form License Notice + +This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of +the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular file, then You may include the +notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be +likely to look for such a notice. + +You may add additional accurate notices of copyright ownership. + +## Exhibit B - "Incompatible With Secondary Licenses" Notice + +This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public +License, v. 2.0. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8c7504f --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# multildap + +Proxy LDAP requests to different LDAP servers based on base DN. Based on code from [kanidm/ldap-proxy](https://github.com/kanidm/ldap-proxy/) under the [MPL license](LICENSE.md). + +## Features + +- [x] No TLS setup ; use only in trusted networks +- [x] LDAP bind requests +- [x] LDAP search requests + +## Configuration + +Create a configuration file `config.toml` with the following: + +```toml +[[mapping]] +from = "a.localhost" +to = "example.com" +backend = "127.0.0.1:4389" +[[mapping]] +from = "b.localhost" +to = "example.com" +backend = "127.0.0.1:5389" +``` + +## Running + +``` +cargo run -- --config config.toml +``` diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..6bda01d --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,11 @@ +use clap::Parser; + +use std::path::PathBuf; + +#[derive(Parser)] +#[command(version, about, long_about = None)] +pub struct Cli { + /// Sets a custom config file + #[arg(short, long, value_name = "FILE")] + pub config: PathBuf, +} diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..a61f014 --- /dev/null +++ b/src/client.rs @@ -0,0 +1,176 @@ +use futures_util::sink::SinkExt; +use futures_util::stream::StreamExt; +use ldap3_proto::LdapCodec; +use ldap3_proto::control::LdapControl; +use ldap3_proto::proto::*; +use tokio::net::TcpStream; +use tokio::time::timeout; +use tokio_util::codec::{FramedRead, FramedWrite}; + +use std::time::Duration; + +use crate::{CR, CW, LdapError}; + +pub struct BasicLdapClient { + r: FramedRead, + w: FramedWrite, + msg_counter: i32, +} + +impl BasicLdapClient { + fn next_msgid(&mut self) -> i32 { + self.msg_counter += 1; + self.msg_counter + } + + pub async fn build(addr: &str) -> Result { + let tcpstream = match timeout(Duration::from_secs(1), TcpStream::connect(addr)).await { + Ok(Ok(t)) => { + trace!("connection established to {addr}"); + t + } + Ok(Err(err)) => { + // trace!(?addr, ?err, "error"); + error!("error to {addr}: {err}"); + panic!(); + } + Err(_) => { + warn!("timeout to {addr}"); + panic!(); + // continue; + } + }; + + let (r, w) = tokio::io::split(tcpstream); + + let w = FramedWrite::new(w, LdapCodec::new(None, None)); + let r = FramedRead::new(r, LdapCodec::new(None, None)); + + Ok(Self { + r, + w, + msg_counter: 0, + }) + } + + pub async fn bind( + &mut self, + lbr: LdapBindRequest, + ctrl: Vec, + ) -> Result<(LdapBindResponse, Vec), LdapError> { + let ck_msgid = self.next_msgid(); + + let msg = LdapMsg { + msgid: ck_msgid, + op: LdapOp::BindRequest(lbr), + ctrl, + }; + + match self.w.send(msg).await { + Ok(_) => {} + Err(err) => { + error!("unable to transmit to ldap server: {err}"); + return Err(LdapError::Transport); + } + }; + + match self.r.next().await { + Some(Ok(LdapMsg { + msgid, + op: LdapOp::BindResponse(bind_resp), + ctrl, + })) => { + if msgid == ck_msgid { + Ok((bind_resp, ctrl)) + } else { + error!("invalid msgid, sequence error."); + Err(LdapError::InvalidProtocolState) + } + } + Some(Ok(msg)) => { + trace!("{:?}", msg); + Err(LdapError::InvalidProtocolState) + } + Some(Err(e)) => { + error!("unable to receive from ldap server: {e}"); + Err(LdapError::Transport) + } + None => { + error!("connection closed"); + Err(LdapError::Transport) + } + } + } + + pub async fn search( + &mut self, + sr: LdapSearchRequest, + ctrl: Vec, + ) -> Result< + ( + Vec<(LdapSearchResultEntry, Vec)>, + LdapResult, + Vec, + ), + LdapError, + > { + let ck_msgid = self.next_msgid(); + + let msg = LdapMsg { + msgid: ck_msgid, + op: LdapOp::SearchRequest(sr), + ctrl, + }; + + match self.w.send(msg).await { + Ok(_) => {} + Err(err) => { + error!("unable to transmit to ldap server: {err}"); + return Err(LdapError::Transport); + } + }; + + let mut entries = Vec::new(); + loop { + match self.r.next().await { + // This terminates the iteration of entries. + Some(Ok(LdapMsg { + msgid, + op: LdapOp::SearchResultDone(search_res), + ctrl, + })) => { + if msgid == ck_msgid { + break Ok((entries, search_res, ctrl)); + } else { + error!("invalid msgid, sequence error."); + break Err(LdapError::InvalidProtocolState); + } + } + Some(Ok(LdapMsg { + msgid, + op: LdapOp::SearchResultEntry(search_entry), + ctrl, + })) => { + if msgid == ck_msgid { + entries.push((search_entry, ctrl)) + } else { + error!("invalid msgid, sequence error."); + break Err(LdapError::InvalidProtocolState); + } + } + Some(Ok(msg)) => { + trace!("{:?}", msg); + break Err(LdapError::InvalidProtocolState); + } + Some(Err(e)) => { + error!("unable to receive from ldap server: {e}"); + break Err(LdapError::Transport); + } + None => { + error!("connection closed"); + break Err(LdapError::Transport); + } + } + } + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..0f1ff60 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,22 @@ +use serde::Deserialize; + +use std::path::Path; + +#[derive(Clone, Debug, Deserialize)] +pub struct Config { + pub mapping: Vec, +} + +impl Config { + pub async fn from_path(path: &Path) -> anyhow::Result { + let content = tokio::fs::read(path).await?; + Ok(toml::from_slice(&content)?) + } +} + +#[derive(Clone, Debug, Deserialize)] +pub struct Mapping { + pub from: String, + pub to: String, + pub backend: String, +} diff --git a/src/dn.rs b/src/dn.rs new file mode 100644 index 0000000..d3793f3 --- /dev/null +++ b/src/dn.rs @@ -0,0 +1,100 @@ +use indexmap::IndexMap; +use ldap3::dn_escape; + +use crate::LdapError; + +/// A Dn is a key-value mapping which can contain the same key several times. +/// +/// This implementation is not fully RFC compliant and will only parse simple DNs for +/// basic attribute manipulation. +/// +/// Keys are lowercased, but scrambled keys in a broken order will be reordered. For example, +/// `dc=example,ou=people,dc=com` will become `dc=example,dc=com,ou=people`. +pub struct Dn { + pub keys: IndexMap>, +} + +impl Dn { + /// Parse a DN string. Here parsing escaped characters is not critical, if a + /// client sends us funny characters, the domain name simply won't match + /// and their request won't go anywhere. + /// + /// However, if we find a really funny request such as `dc=foo=bar`, then + /// we return an error to the client. + pub fn from_dn_str(input: &str) -> Result { + let mut keys: IndexMap> = IndexMap::new(); + + for query in input.split(',') { + let mut query_parts = query.split('='); + // Here we have key=val pairs + if query_parts.clone().count() != 2 { + // Bad request + log::debug!("Invalid query DN: {input}"); + return Err(LdapError::InvalidQuery); + } + + let key = query_parts.next().unwrap(); + let val = query_parts.next().unwrap(); + if let Some(previous) = keys.get_mut(key) { + previous.push(val.to_string()); + } else { + keys.insert(key.to_string(), vec![val.to_string()]); + } + } + + Ok(Self { keys }) + } + + pub fn to_dn_string(&self) -> String { + let mut s = String::new(); + let mut first = true; + for (key, values) in &self.keys { + for value in values { + if first { + first = false; + } else { + s.push(','); + } + s.push_str(key); + s.push('='); + s.push_str(value); + } + } + + s + } + + /// Gets the hostname defined in the `dc` fields of the DN. + /// + /// For example, `dc=example,dc=com` becomes `Some(example.com)`. + /// + /// The returned domain is not normalized and may require casing treatment + /// to compare meaningfully. + pub fn get_hostname(&self) -> Option { + let domain_components = self.keys.get("dc")?; + + // We don't populate the dc key if there was no value at all, so + // we have at least one component. + let mut domain_components = domain_components.iter(); + let mut s = String::from(domain_components.next().unwrap()); + for domain_component in domain_components { + s.push('.'); + s.push_str(domain_component); + } + + Some(s) + } + + /// Overrides the DN hostname (`dc` fields) with the provided host. + /// + /// When no `dc` fields are present, they are only added when `force` is true. + pub fn set_hostname(&mut self, host: &str, force: bool) { + let domain_components: Vec = + host.split('.').map(|x| dn_escape(x).to_string()).collect(); + if !force && !self.keys.contains_key("dc") { + return; + } + + self.keys.insert("dc".to_string(), domain_components); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..44789f2 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,232 @@ +#[macro_use] +extern crate log; + +use clap::Parser; +use futures_util::StreamExt; +use ldap3_proto::LdapCodec; +use ldap3_proto::proto::*; +use tokio::io::{AsyncRead, AsyncWrite, ReadHalf, WriteHalf}; +use tokio::net::{TcpListener, TcpStream}; +use tokio::time::timeout; +use tokio_util::codec::{FramedRead, FramedWrite}; + +use std::sync::Arc; +use std::time::Duration; + +mod cli; +use cli::Cli; +mod client; +use crate::client::BasicLdapClient; +mod config; +use config::Config; +mod dn; +mod op; +use crate::dn::Dn; + +const LDAP_CLIENT_IO_TIMEOUT: Duration = Duration::from_secs(1); + +type CR = ReadHalf; +type CW = WriteHalf; + +#[derive(Debug, Clone)] +pub enum LdapError { + TlsError, + ConnectError, + Transport, + InvalidProtocolState, + InvalidQuery, +} + +// We allow the large enum to exist as we always do a mem swap from unbound to authenticated, so +// the memory layout penalty doesn't apply. +#[allow(clippy::large_enum_variant)] +enum ClientState { + Unbound, + Authenticated { + #[allow(dead_code)] + request_dn: String, + backend_dn: String, + client: BasicLdapClient, + }, +} + +pub async fn client_process( + mut r: FramedRead, + mut w: FramedWrite, + config: Arc, +) { + info!("Received new connection"); + + // We always start unbound. + let mut state = ClientState::Unbound; + + // Start to wait for incoming packets + while let Ok(Some(Ok(protomsg))) = timeout(LDAP_CLIENT_IO_TIMEOUT, r.next()).await { + let next_state = match (&mut state, protomsg) { + // Doesn't matter what state we are in, any bind will trigger this process. + ( + _, + LdapMsg { + msgid, + op: LdapOp::BindRequest(lbr), + ctrl, + }, + ) => match op::bind::bind(&mut w, lbr, config.clone(), msgid, ctrl).await { + Ok(ns) => ns, + Err(_) => break, + }, + // Unbinds are always actioned. + ( + _, + LdapMsg { + msgid: _, + op: LdapOp::UnbindRequest, + ctrl: _, + }, + ) => { + break; + } + // Unbound handler + ( + ClientState::Unbound, + LdapMsg { + msgid, + op: LdapOp::SearchRequest(sr), + ctrl, + }, + ) => { + // We have to trigger a bind first in case we have a mapping. + let lbr = LdapBindRequest { + dn: "".to_string(), + cred: LdapBindCred::Simple("".to_string()), + }; + + let mut next_state = + match op::bind::bind(&mut w, lbr, config.clone(), 0, Vec::default()).await { + Ok(ns) => ns, + Err(_) => break, + }; + + match &mut next_state { + Some(ClientState::Unbound) | None => { + error!("Invalid state, bind did not return an authenticated state!"); + break; + } + Some(ClientState::Authenticated { + client, + request_dn: _, + backend_dn: _, + }) => { + let search_req = op::search::SearchRequest { + sr, + msgid, + ctrl, + client, + }; + match op::search::search(&mut w, search_req).await { + Ok(()) => {} + Err(_) => break, + } + } + } + + next_state + } + + // Authenticated message handler. + // - Search + ( + ClientState::Authenticated { + client, + backend_dn: _, + request_dn: _, + }, + LdapMsg { + msgid, + op: LdapOp::SearchRequest(sr), + ctrl, + }, + ) => { + let search_req = op::search::SearchRequest { + sr, + msgid, + ctrl, + client, + }; + + match op::search::search(&mut w, search_req).await { + Ok(()) => None, + Err(_) => break, + } + } + // Extended Requests - Generally whoami. + ( + ClientState::Authenticated { + request_dn: _, + backend_dn, + client: _, + }, + LdapMsg { + msgid, + op: LdapOp::ExtendedRequest(ler), + ctrl: _, + }, + ) => match op::ext::extop(&mut w, ler, msgid, backend_dn).await { + Ok(ns) => ns, + Err(_) => break, + }, + _ => { + log::debug!("unimplemented"); + None + } + }; + + if let Some(next_state) = next_state { + // Update the client state, dropping any former state. + state = next_state; + } + } +} + +#[tokio::main(flavor = "current_thread")] +async fn main() { + if let Err(_) = std::env::var("RUST_LOG") { + unsafe { std::env::set_var("RUST_LOG", "info"); } + } + + pretty_env_logger::formatted_timed_builder() + .parse_default_env() + .init(); + + let cli = Cli::parse(); + let config = match Config::from_path(&cli.config).await { + Ok(config) => config, + Err(e) => { + error!( + "Loading configuration file {} failed!", + cli.config.display() + ); + error!("{}", e); + std::process::exit(1); + } + }; + + let state = Arc::new(config); + + // Let the listening port ready. + let listener = TcpListener::bind("127.0.0.1:3389").await.unwrap(); + info!("Listening on {:?}", listener); + + loop { + match listener.accept().await { + Ok((tcpstream, client_socket_addr)) => { + log::debug!("New connection from {client_socket_addr}"); + let (r, w) = tokio::io::split(tcpstream); + let r = FramedRead::new(r, LdapCodec::new(None, None)); + let w = FramedWrite::new(w, LdapCodec::new(None, None)); + tokio::spawn(client_process(r, w, state.clone())); + } + Err(_e) => continue, + } + } +} diff --git a/src/op/bind.rs b/src/op/bind.rs new file mode 100644 index 0000000..cfa9d65 --- /dev/null +++ b/src/op/bind.rs @@ -0,0 +1,120 @@ +use futures_util::SinkExt; +use ldap3_proto::LdapCodec; +use ldap3_proto::control::*; +use ldap3_proto::proto::*; +use tokio::io::AsyncWrite; +use tokio_util::codec::FramedWrite; + +use std::sync::Arc; + +use crate::{BasicLdapClient, ClientState, Config, Dn, LdapError}; + +pub fn bind_operror(msgid: i32, msg: &str) -> LdapMsg { + LdapMsg { + msgid, + op: LdapOp::BindResponse(LdapBindResponse { + res: LdapResult { + code: LdapResultCode::OperationsError, + matcheddn: "".to_string(), + message: msg.to_string(), + referral: vec![], + }, + saslcreds: None, + }), + ctrl: vec![], + } +} + +pub async fn bind( + w: &mut FramedWrite, + mut lbr: LdapBindRequest, + config: Arc, + msgid: i32, + ctrl: Vec, +) -> Result, LdapError> { + trace!("{:?}", lbr); + + let request_dn = lbr.dn.clone(); + + debug!("Received bind request on DN: {}", lbr.dn); + let mut dn = Dn::from_dn_str(&lbr.dn)?; + + let Some(requested_domain) = dn.get_hostname() else { + debug!("No domain name CN found in DN: {}", lbr.dn); + return Err(LdapError::InvalidQuery); + }; + + // Lowercase the domain systematically to allow matches + let requested_domain = requested_domain.to_lowercase(); + + let Some(mapping) = config + .mapping + .iter() + .find(|x| x.from.to_lowercase() == requested_domain) + else { + debug!("No mapping found for domain {requested_domain}"); + return Err(LdapError::InvalidQuery); + }; + + debug!( + "Redirecting {} to {} with domain {}", + requested_domain, mapping.backend, mapping.to + ); + dn.set_hostname(&mapping.to, false); + lbr.dn = dn.to_dn_string(); + let dn = lbr.dn.clone(); + + // We need the client to connect *and* bind to proceed here! + let mut client = match BasicLdapClient::build(&mapping.backend).await { + Ok(c) => c, + Err(e) => { + error!("A client build error has occurred: {e:?}"); + let resp_msg = bind_operror(msgid, "unable to bind"); + w.send(resp_msg).await.map_err(|err| { + error!("Unable to send response: {err}"); + LdapError::Transport + })?; + // Always bail. + return Ok(None); + } + }; + + let valid = match client.bind(lbr, ctrl).await { + Ok((bind_resp, ctrl)) => { + // Almost there, lets check the bind result. + let valid = bind_resp.res.code == LdapResultCode::Success; + + let resp_msg = LdapMsg { + msgid, + op: LdapOp::BindResponse(bind_resp), + ctrl, + }; + w.send(resp_msg).await.map_err(|err| { + error!("Unable to send response: {err}"); + LdapError::Transport + })?; + valid + } + Err(e) => { + error!("A client bind error has occurred: {e:?}"); + let resp_msg = bind_operror(msgid, "unable to bind"); + w.send(resp_msg).await.map_err(|err| { + error!("Unable to send response: {err}"); + LdapError::Transport + })?; + // Always bail. + return Ok(None); + } + }; + + if valid { + info!("Successful bind for {}", dn); + Ok(Some(ClientState::Authenticated { + request_dn, + backend_dn: dn, + client, + })) + } else { + Ok(None) + } +} diff --git a/src/op/ext.rs b/src/op/ext.rs new file mode 100644 index 0000000..875a435 --- /dev/null +++ b/src/op/ext.rs @@ -0,0 +1,51 @@ +use futures_util::SinkExt; +use ldap3_proto::LdapCodec; +use ldap3_proto::proto::*; +use tokio::io::AsyncWrite; +use tokio_util::codec::FramedWrite; + +use crate::{ClientState, LdapError}; + +pub async fn extop( + w: &mut FramedWrite, + ler: LdapExtendedRequest, + msgid: i32, + + display_dn: &str, +) -> Result, LdapError> { + let op = match ler.name.as_str() { + "1.3.6.1.4.1.4203.1.11.3" => LdapOp::ExtendedResponse(LdapExtendedResponse { + res: LdapResult { + code: LdapResultCode::Success, + matcheddn: "".to_string(), + message: "".to_string(), + referral: vec![], + }, + name: None, + value: Some(Vec::from(display_dn)), + }), + _ => LdapOp::ExtendedResponse(LdapExtendedResponse { + res: LdapResult { + code: LdapResultCode::OperationsError, + matcheddn: "".to_string(), + message: "".to_string(), + referral: vec![], + }, + name: None, + value: None, + }), + }; + + w.send(LdapMsg { + msgid, + op, + ctrl: vec![], + }) + .await + .map_err(|err| { + error!("Unable to send response: {err}"); + LdapError::Transport + })?; + + Ok(None) +} diff --git a/src/op/mod.rs b/src/op/mod.rs new file mode 100644 index 0000000..8febca5 --- /dev/null +++ b/src/op/mod.rs @@ -0,0 +1,3 @@ +pub mod bind; +pub mod ext; +pub mod search; diff --git a/src/op/search.rs b/src/op/search.rs new file mode 100644 index 0000000..f5934fc --- /dev/null +++ b/src/op/search.rs @@ -0,0 +1,70 @@ +use futures_util::SinkExt; +use ldap3_proto::LdapCodec; +use ldap3_proto::control::*; +use ldap3_proto::proto::*; +use tokio::io::AsyncWrite; +use tokio_util::codec::FramedWrite; + +use crate::op::bind::bind_operror; +use crate::{BasicLdapClient, LdapError}; + +pub struct SearchRequest<'a> { + pub sr: LdapSearchRequest, + pub msgid: i32, + pub ctrl: Vec, + pub client: &'a mut BasicLdapClient, +} + +pub async fn search( + w: &mut FramedWrite, + search_request: SearchRequest<'_>, +) -> Result<(), LdapError> { + let SearchRequest { + sr, + msgid, + ctrl, + client, + } = search_request; + + let (entries, result, ctrl) = match client.search(sr, ctrl).await { + Ok(data) => data, + Err(e) => { + error!("A client search error has occurred: {e:?}"); + let resp_msg = bind_operror(msgid, "unable to search"); + w.send(resp_msg).await.map_err(|err| { + error!("Unable to send response: {err}"); + LdapError::Transport + })?; + + // Error sent, return with no state change. + return Ok(()); + } + }; + + for (entry, ctrl) in entries { + w.send(LdapMsg { + msgid, + op: LdapOp::SearchResultEntry(entry), + ctrl, + }) + .await + .map_err(|err| { + error!("Unable to send response: {err}"); + LdapError::Transport + })?; + } + + w.send(LdapMsg { + msgid, + op: LdapOp::SearchResultDone(result), + ctrl, + }) + .await + .map_err(|err| { + error!("Unable to send response: {err}"); + LdapError::Transport + })?; + + // No state change + Ok(()) +}