From 1626fb062428afc20e1846022f5bdd14a474ddb4 Mon Sep 17 00:00:00 2001 From: selfhoster1312 Date: Thu, 3 Sep 2026 20:31:28 +0200 Subject: [PATCH 01/20] WHY IS IT NOT WORKING --- src/db/interface.rs | 2 +- src/ldap/handler.rs | 21 +++++++++++++++++++++ src/ldap/mod.rs | 2 +- src/ldap/op/search.rs | 2 +- src/main.rs | 10 +++------- 5 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/db/interface.rs b/src/db/interface.rs index 74ce9ee..7a6d8ad 100644 --- a/src/db/interface.rs +++ b/src/db/interface.rs @@ -14,7 +14,7 @@ impl DatabaseInterface for Database { } } -pub trait DatabaseInterface { +pub trait DatabaseInterface: Clone + Send + Sync + 'static { async fn get_user(&self, user: &UserRef) -> Result, BoxedError>; async fn create_user( &mut self, diff --git a/src/ldap/handler.rs b/src/ldap/handler.rs index 32d2ac0..c8af21e 100644 --- a/src/ldap/handler.rs +++ b/src/ldap/handler.rs @@ -2,9 +2,30 @@ use ldap3_proto::LdapMsg; use ldap3_proto::proto::LdapOp; use crate::db::{Database, DatabaseInterface}; +use crate::error::GlobalError; use crate::ldap::{ LdapClientState, LdapStream, LdapStreamError, op_bind, op_ext, search_by_mail_filter, }; +use crate::listener::Listener; + +pub async fn ldap_listen(listener: Listener, db: Database) { + // If the connection is None, it's because the client aborted early + // so there's nothing to do about it. + loop { + match listener.accept_ldap().await { + Ok(Some(stream)) => { + let db = db.clone(); + tokio::spawn(ldap_handler(stream, db)); + } + Ok(None) => { + panic!("LDAP listener closed"); + } + Err(e) => { + panic!("Failed to listen on LDAP listener"); + } + } + } +} #[tracing::instrument(name = "ldap", skip(stream, db), fields(session = %stream.session))] pub async fn ldap_handler(mut stream: LdapStream, mut db: Database) { diff --git a/src/ldap/mod.rs b/src/ldap/mod.rs index d72d094..dc33d6c 100644 --- a/src/ldap/mod.rs +++ b/src/ldap/mod.rs @@ -5,7 +5,7 @@ pub use dn::{Dn, MalformedDn}; mod filter; mod handler; mod op; -pub use handler::ldap_handler; +pub use handler::ldap_listen; pub use op::bind::{BindDn, op_bind}; pub use op::ext::op_ext; pub use op::search::search_by_mail_filter; diff --git a/src/ldap/op/search.rs b/src/ldap/op/search.rs index 3255298..b65b160 100644 --- a/src/ldap/op/search.rs +++ b/src/ldap/op/search.rs @@ -153,7 +153,7 @@ fn search_entry_from_user(user: &User, req_attrs: &[String]) -> LdapSearchResult for attr in req_attrs { if let Some(attr_value) = match attr.as_str() { "uid" => Some(user.username.clone()), - "cn"|"mail" => Some(user.mail.clone()), + "cn" | "mail" => Some(user.mail.clone()), _ => { tracing::warn!("Ignoring unknown attr in search query: {attr}"); None diff --git a/src/main.rs b/src/main.rs index 1bcde2c..3cd41f9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,7 @@ mod stream; use cli::CliArgs; use db::{Database, DatabaseInterface, MemoryDatabase, User}; use error::GlobalError; -use ldap::ldap_handler; +use ldap::ldap_listen; use listener::ListenerPath; async fn create_dummy_users(db: &mut Database) { @@ -44,12 +44,8 @@ async fn main() -> Result<(), GlobalError> { let mut db = MemoryDatabase::new(); create_dummy_users(&mut db).await; - // If the connection is None, it's because the client aborted early - // so there's nothing to do about it. - while let Some(stream) = listener.accept_ldap().await? { - let db = db.clone(); - tokio::spawn(ldap_handler(stream, db)); - } + let ldap_db = db.clone(); + tokio::spawn(ldap_listen(listener, ldap_db)); Ok(()) } From 452da7341386b4f011190723ef9219a9408fc5af Mon Sep 17 00:00:00 2001 From: selfhoster1312 Date: Thu, 3 Sep 2026 21:50:32 +0200 Subject: [PATCH 02/20] feat: Basic HTTP listener --- Cargo.lock | 265 ++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 6 + src/cli.rs | 10 +- src/db/interface.rs | 2 +- src/http/mod.rs | 57 +++++++++ src/ldap/op/search.rs | 2 +- src/main.rs | 15 ++- 7 files changed, 350 insertions(+), 7 deletions(-) create mode 100644 src/http/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 696a646..929b28a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,6 +33,64 @@ dependencies = [ "serde", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "base64" version = "0.22.1" @@ -67,6 +125,24 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" name = "dn_escape" version = "0.1.0" +[[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-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + [[package]] name = "futures-core" version = "0.3.34" @@ -121,6 +197,92 @@ dependencies = [ "r-efi", ] +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[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.104" @@ -176,6 +338,7 @@ name = "llldap" version = "0.1.0" dependencies = [ "argh", + "axum", "camino", "dn_escape", "futures-util", @@ -183,6 +346,7 @@ dependencies = [ "serde", "tokio", "tokio-util", + "tower", "tracing", "tracing-subscriber", "uuid", @@ -194,12 +358,24 @@ version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "memchr" version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -269,6 +445,12 @@ 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" @@ -305,6 +487,12 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "serde" version = "1.0.229" @@ -335,6 +523,42 @@ dependencies = [ "syn 3.0.4", ] +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -388,6 +612,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + [[package]] name = "thiserror" version = "2.0.20" @@ -456,12 +686,41 @@ dependencies = [ "tokio", ] +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -601,3 +860,9 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index 0ea53c5..4b0be25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] argh = "0.1.19" +axum = { version = "0.8.9", optional = true } camino = "1.2.5" dn_escape = { path = "vendor/dn_escape" } futures-util = { version = "0.3.34", features = ["sink"] } @@ -12,6 +13,7 @@ ldap3_proto = "0.8.1" serde = { version = "1.0.229", features = ["derive"] } tokio = { version = "1.53.1", features = ["macros", "net", "rt", "time", "sync"] } tokio-util = { version = "0.7.19", features = ["codec"] } +tower = "0.5.3" tracing = "0.1.44" tracing-subscriber = "0.3.23" uuid = { version = "1.26.0", features = ["v4"] } @@ -22,3 +24,7 @@ complexity = "deny" pedantic = "deny" perf = "deny" style = "deny" + +[features] +default = [ "http" ] +http = ["dep:axum"] diff --git a/src/cli.rs b/src/cli.rs index f93abe3..6867704 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -3,7 +3,11 @@ use argh::FromArgs; /// Run the llldap server #[derive(FromArgs)] pub struct CliArgs { - /// address or socket to listen on - #[argh(positional, default = "String::from(\"127.0.0.1:3389\")")] - pub listen: String, + /// address or socket to listen on for LDAP connections + #[argh(option, default = "String::from(\"127.0.0.1:3389\")")] + pub listen_ldap: String, + + /// address or socket to listen on for HTTP connections + #[argh(option, default = "String::from(\"127.0.0.1:3390\")")] + pub listen_http: String, } diff --git a/src/db/interface.rs b/src/db/interface.rs index 74ce9ee..7a6d8ad 100644 --- a/src/db/interface.rs +++ b/src/db/interface.rs @@ -14,7 +14,7 @@ impl DatabaseInterface for Database { } } -pub trait DatabaseInterface { +pub trait DatabaseInterface: Clone + Send + Sync + 'static { async fn get_user(&self, user: &UserRef) -> Result, BoxedError>; async fn create_user( &mut self, diff --git a/src/http/mod.rs b/src/http/mod.rs new file mode 100644 index 0000000..cba0164 --- /dev/null +++ b/src/http/mod.rs @@ -0,0 +1,57 @@ +use axum::Router; +use axum::routing::get; +use axum::serve::Listener as AxumListener; + +use crate::db::{Database, DatabaseInterface}; +use crate::listener::{Listener, ListenerKind}; +use crate::stream::{AbstractSocketAddr, AbstractStreamKind}; + +impl AxumListener for Listener { + type Io = AbstractStreamKind; + type Addr = AbstractSocketAddr; + + async fn accept(&mut self) -> (Self::Io, Self::Addr) { + loop { + let res = match &self.kind { + ListenerKind::Tcp(l) => l + .accept() + .await + .map(|(stream, remote_addr)| (stream.into(), remote_addr.into())), + ListenerKind::Uds(l) => l + .accept() + .await + .map(|(stream, remote_addr)| (stream.into(), remote_addr.into())), + }; + + match res { + Ok((stream, remote_addr)) => return (stream, remote_addr), + Err(e) => { + // Here the error could be fatal, or could simply be that a client aborted the connected, + // in which case we don't want to crash the server, simply skip this client connection. + // https://doc.rust-lang.org/stable/std/net/struct.TcpListener.html#errors + match e.kind() { + std::io::ErrorKind::ConnectionAborted => {} + _ => panic!("Unrecoverable HTTP client connection error: {e}"), + } + } + } + } + } + + fn local_addr(&self) -> std::io::Result { + match &self.kind { + ListenerKind::Tcp(l) => l.local_addr().map(Into::into), + ListenerKind::Uds(l) => l.local_addr().map(Into::into), + } + } +} + +pub async fn http_listen(listener: Listener, db: Database) { + let app = Router::new().route("/", get(handler)).with_state(db); + + axum::serve(listener, app).await.unwrap(); +} + +pub async fn handler() -> &'static str { + "hello world" +} diff --git a/src/ldap/op/search.rs b/src/ldap/op/search.rs index 3255298..b65b160 100644 --- a/src/ldap/op/search.rs +++ b/src/ldap/op/search.rs @@ -153,7 +153,7 @@ fn search_entry_from_user(user: &User, req_attrs: &[String]) -> LdapSearchResult for attr in req_attrs { if let Some(attr_value) = match attr.as_str() { "uid" => Some(user.username.clone()), - "cn"|"mail" => Some(user.mail.clone()), + "cn" | "mail" => Some(user.mail.clone()), _ => { tracing::warn!("Ignoring unknown attr in search query: {attr}"); None diff --git a/src/main.rs b/src/main.rs index 1bcde2c..0edea35 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,10 +3,14 @@ mod cli; mod db; mod error; +#[cfg(feature = "http")] +mod http; mod ldap; mod listener; mod stream; +#[cfg(feature = "http")] +use crate::http::http_listen; use cli::CliArgs; use db::{Database, DatabaseInterface, MemoryDatabase, User}; use error::GlobalError; @@ -39,14 +43,21 @@ async fn main() -> Result<(), GlobalError> { // .init(); // tracing::subscriber::set_global_default(subscriber)?; - let listener = ListenerPath::new(&cli.listen)?.listener().await?; + let ldap_listener = ListenerPath::new(&cli.listen_ldap)?.listener().await?; let mut db = MemoryDatabase::new(); create_dummy_users(&mut db).await; + #[cfg(feature = "http")] + { + let http_listener = ListenerPath::new(&cli.listen_http)?.listener().await?; + let http_db = db.clone(); + tokio::spawn(http_listen(http_listener, http_db)); + } + // If the connection is None, it's because the client aborted early // so there's nothing to do about it. - while let Some(stream) = listener.accept_ldap().await? { + while let Some(stream) = ldap_listener.accept_ldap().await? { let db = db.clone(); tokio::spawn(ldap_handler(stream, db)); } From b37c0665d3b3620644a6a81fdfaa5f0b175c2891 Mon Sep 17 00:00:00 2001 From: selfhoster1312 Date: Thu, 3 Sep 2026 21:53:31 +0200 Subject: [PATCH 03/20] minor: Remove unused warnings --- src/db/group.rs | 2 ++ src/db/memory.rs | 1 + src/ldap/dn.rs | 1 + src/ldap/op/search.rs | 2 +- 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/db/group.rs b/src/db/group.rs index 428d8f9..c07a891 100644 --- a/src/db/group.rs +++ b/src/db/group.rs @@ -1,5 +1,7 @@ #[derive(Clone, Debug)] pub struct Group { + #[expect(unused)] name: String, + #[expect(unused)] domain: String, } diff --git a/src/db/memory.rs b/src/db/memory.rs index 186486c..a3cfad5 100644 --- a/src/db/memory.rs +++ b/src/db/memory.rs @@ -6,6 +6,7 @@ use crate::db::{Database, DatabaseInterface, Group, User, UserRef}; #[derive(Clone, Debug, Default)] pub struct MemoryDatabase { pub users: Vec, + #[expect(unused)] pub groups: Vec, } diff --git a/src/ldap/dn.rs b/src/ldap/dn.rs index 87922aa..0f492f8 100644 --- a/src/ldap/dn.rs +++ b/src/ldap/dn.rs @@ -137,6 +137,7 @@ impl Dn { /// Overrides the DN hostname (`dc` fields) with the provided host. /// /// When no `dc` fields are present, they are only added when `force` is true. + #[expect(unused)] pub fn set_hostname(&mut self, host: &str, force: bool) { let domain_components: Vec = host.split('.').map(|x| dn_escape(x).to_string()).collect(); diff --git a/src/ldap/op/search.rs b/src/ldap/op/search.rs index b65b160..9182fdb 100644 --- a/src/ldap/op/search.rs +++ b/src/ldap/op/search.rs @@ -39,7 +39,7 @@ impl LdapReturnError for InvalidSearchDn { } #[derive(Clone, Debug)] -pub struct SearchDn(Dn); +pub struct SearchDn(#[expect(unused)] Dn); impl SearchDn { pub fn from_dn_str(input: &str) -> Result { From 5683174746cccf22ee19456bbbf83748c2bf212e Mon Sep 17 00:00:00 2001 From: selfhoster1312 Date: Thu, 3 Sep 2026 23:21:08 +0200 Subject: [PATCH 04/20] Workaround --- src/db/interface.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/db/interface.rs b/src/db/interface.rs index 7a6d8ad..f8ca052 100644 --- a/src/db/interface.rs +++ b/src/db/interface.rs @@ -15,7 +15,7 @@ impl DatabaseInterface for Database { } pub trait DatabaseInterface: Clone + Send + Sync + 'static { - async fn get_user(&self, user: &UserRef) -> Result, BoxedError>; + fn get_user(&self, user: &UserRef) -> impl std::future::Future, BoxedError>> + Send; async fn create_user( &mut self, user: User, From 8c29f1d31fd0718dc8a5895750d34f76e8a67538 Mon Sep 17 00:00:00 2001 From: selfhoster1312 Date: Fri, 4 Sep 2026 17:33:37 +0200 Subject: [PATCH 05/20] feat: Implement basic (dummy) admin auth --- Cargo.lock | 363 ++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 6 +- assets/css/main.css | 81 +++++++++ assets/img/logo.png | Bin 0 -> 41039 bytes assets/img/password.svg | 1 + assets/img/user.svg | 1 + src/http/login.rs | 52 ++++++ src/http/logout.rs | 23 +++ src/http/mod.rs | 58 ++++++- src/http/session.rs | 75 +++++++++ templates/base.html | 11 ++ templates/home.html | 11 ++ templates/login.html | 29 ++++ 13 files changed, 706 insertions(+), 5 deletions(-) create mode 100644 assets/css/main.css create mode 100644 assets/img/logo.png create mode 100644 assets/img/password.svg create mode 100644 assets/img/user.svg create mode 100644 src/http/login.rs create mode 100644 src/http/logout.rs create mode 100644 src/http/session.rs create mode 100644 templates/base.html create mode 100644 templates/home.html create mode 100644 templates/login.html diff --git a/Cargo.lock b/Cargo.lock index 929b28a..45bb96d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "argh" version = "0.1.19" @@ -91,12 +97,43 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-extra" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be44683b41ccb9ab2d23a5230015c9c3c55be97a25e4428366de8873103f7970" +dependencies = [ + "axum", + "axum-core", + "bytes", + "cookie", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "bumpalo" version = "3.20.3" @@ -115,16 +152,112 @@ version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" +[[package]] +name = "cc" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "cookie" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", +] + +[[package]] +name = "display_full_error" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7c47e9a2fd28a8edd1446f10dabe8ac5d26bb77ed2b1077bfcd8308904e8c6" + [[package]] name = "dn_escape" version = "0.1.0" +[[package]] +name = "find-msvc-tools" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide", + "zlib-rs", +] + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -197,6 +330,12 @@ dependencies = [ "r-efi", ] +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + [[package]] name = "http" version = "1.5.0" @@ -242,6 +381,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.11.1" @@ -283,6 +431,16 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom", + "libc", +] + [[package]] name = "js-sys" version = "0.3.104" @@ -339,11 +497,15 @@ version = "0.1.0" dependencies = [ "argh", "axum", + "axum-extra", "camino", "dn_escape", "futures-util", + "http", "ldap3_proto", + "minijinja", "serde", + "static-serve", "tokio", "tokio-util", "tower", @@ -370,18 +532,54 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "memo-map" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5449c8c750f1a07ea702bbd212bd999fceece9b3d1508b17023b3e174583124b" + [[package]] name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minijinja" +version = "2.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86886cf6dbf4e614b19c9a1eec9775f021869d7eadde0fc73921a81b90c9b4c9" +dependencies = [ + "memo-map", + "serde", +] + [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.2.2" @@ -412,6 +610,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + [[package]] name = "once_cell" version = "1.21.4" @@ -457,6 +661,18 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro2" version = "1.0.107" @@ -481,6 +697,18 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "range-requests" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e30cea6780132baa8257e81ab3f6e11526a9271e9032963eab8d830972e68b6" +dependencies = [ + "axum-core", + "bytes", + "http", + "thiserror", +] + [[package]] name = "rustversion" version = "1.0.23" @@ -559,6 +787,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -568,6 +807,18 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + [[package]] name = "slab" version = "0.4.12" @@ -590,6 +841,36 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "static-serve" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49c7a4b3dbe44375f7e30895c04d3ca4fde020eb7336c0f6a71ed495c5302e1a" +dependencies = [ + "axum", + "bytes", + "range-requests", + "static-serve-macro", +] + +[[package]] +name = "static-serve-macro" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cb887d817cf39e80fd7fdd21c4f138c8277d7dc2623fee4d07e141f4ce32a0a" +dependencies = [ + "display_full_error", + "flate2", + "glob", + "mime_guess", + "proc-macro2", + "quote", + "sha2", + "syn 3.0.4", + "thiserror", + "zstd", +] + [[package]] name = "syn" version = "2.0.119" @@ -647,6 +928,36 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tokio" version = "1.53.1" @@ -772,6 +1083,18 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -795,6 +1118,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -861,8 +1190,42 @@ dependencies = [ "windows-link", ] +[[package]] +name = "zlib-rs" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" + [[package]] name = "zmij" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index 4b0be25..9ce4c55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,11 +6,15 @@ edition = "2024" [dependencies] argh = "0.1.19" axum = { version = "0.8.9", optional = true } +axum-extra = { version = "0.12.6", features = ["cookie"], optional = true } camino = "1.2.5" dn_escape = { path = "vendor/dn_escape" } futures-util = { version = "0.3.34", features = ["sink"] } +http = { version = "1.5.0", optional = true } ldap3_proto = "0.8.1" +minijinja = { version = "2.24.0", optional = true } serde = { version = "1.0.229", features = ["derive"] } +static-serve = { version = "0.6.3", optional = true } tokio = { version = "1.53.1", features = ["macros", "net", "rt", "time", "sync"] } tokio-util = { version = "0.7.19", features = ["codec"] } tower = "0.5.3" @@ -27,4 +31,4 @@ style = "deny" [features] default = [ "http" ] -http = ["dep:axum"] +http = ["dep:axum", "dep:axum-extra", "dep:http", "dep:minijinja", "dep:static-serve"] diff --git a/assets/css/main.css b/assets/css/main.css new file mode 100644 index 0000000..5660244 --- /dev/null +++ b/assets/css/main.css @@ -0,0 +1,81 @@ +body { + margin: 0; +} + +#logo { + display: block; + max-height: 5rem; + max-width: 5rem; + height: auto; + width: auto; + margin: 0 auto; +} + +#center { + width: 100vw; + height: 100vh; + display: flex; + justify-content: center; + align-items: space-around; + flex-direction: column; + max-width: 250px; + margin: auto; +} + +#login-form { + max-width: 200px; + margin: 0 auto; +} + +#login-line { + margin-top: 1rem; + display: flex; + justify-content: center; + font-size: 2rem; + text-align: center; + height: 2.5rem; + gap: 0.5rem; +} + +#login-line > img { + width: 1.75rem; + height: auto; +} + +#login-line > input { + border-color: rgba(31, 41, 55, 0.2); + border-radius: 8px; + padding: 0.5rem 1rem; +} + +#submit-login { + width: 100%; + background-color: rgb(87, 13, 248); + cursor: pointer; + box-sizing: border-box; + border: none; + color: rgb(240, 240, 240); + font-weight: bolder; + text-transform: uppercase; + margin: auto; +} + +.banner { + margin: 1rem auto; + width: 100%; + border-radius: 5px; +} + +.banner > p { + text-align: center; +} + +.is-success { + background-color: #28cb28; + color: #efefef; +} + +.is-error { + background-color: #d42929; + color: #efefef; +} diff --git a/assets/img/logo.png b/assets/img/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..9278cb6baefbac616a6536bb2b4a812f5d737fa4 GIT binary patch literal 41039 zcmXtfbzIY5)IT+(y95;J2I&^1WONA9-E4G<#1K(Z7%3?*L3-3^7)T0|k|PENIY43{ z`P=t-p5GtaUa#%5bI-l^oO{lFpZ7^HGI&l&#!7~Vherw2)-b`tBe3}IMM8}GrpGOJ z1@}Sfr)?RChes6m-wXdMCVdz8<^3Q{iy%{9x1dnR09U-w&`>c?AMZeCM?Y6F-vIag z17%h`yhnIIjb|^y3ib;_+xB1lLma;QgAbFVrDf2h>JB9$N)LGY^l4@l*K2~rMf5!~ z3lAZEBc#}(2Z0Ifd@UkZz*#(gykbI{Ljj8j z)Dvl??QWzGvInWRnkoO^g9#d?_rfUUS>+e8NqEq5;;4Hdx$kg7epjWJ1h>Rpy<@R_ zxMy%OPsyb4BJfc<%PUw)1i#KD!Q(kU^7+u1cMw$q=iPLf{}Z+qMz}bHU4PU@Ucrh8 zw2M7_Eco*ObQ-)*crAEB;1to1`(qQbAQ+haKMHV7|5OGfx7-~2k(-;;{qJZz-=sK> zz`4pvkN;OL*Yy2*0*>6O-;Vz$ETMFq1eD8A^S{$g@LJ*#wD`ZD<|JhY9eDp&&Wo`? zV4u!^)8tD3!o&{{{y#OHOeoC%XQ2`_2K8ny(`2!SxDtIFJ#qUyrk=p}5Bmwe8vlVZ6@eJou=ð6^&d2J@d+cozV_mjepr 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 z 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