feat: Implement LdapStream, begin event loop

This commit is contained in:
selfhoster selfhoster 2026-09-01 17:57:42 +02:00
commit 1ef02c58dd
8 changed files with 152 additions and 56 deletions

37
src/ldap/handler.rs Normal file
View file

@ -0,0 +1,37 @@
use ldap3_proto::LdapMsg;
use crate::ldap::{LdapStream, LdapStreamError};
#[tracing::instrument(name = "ldap", skip(s), fields(session = %s.session))]
pub async fn ldap_handler(mut s: LdapStream) {
tracing::info! {
remote_addr = ?s.remote_addr,
"New client connection"
};
loop {
match s.next().await {
Ok(msg) => {
if let Err(e) = ldap_handler_inner(msg).await {
tracing::debug!(
reason = ?e,
"Failed to respond"
);
return;
}
}
Err(e) => {
tracing::debug!(
reason = ?e,
"Closing connection"
);
return;
}
}
}
}
pub async fn ldap_handler_inner(msg: LdapMsg) -> Result<(), LdapStreamError> {
tracing::debug!(msg = ?msg, "Received LDAP message");
Ok(())
}

4
src/ldap/mod.rs Normal file
View file

@ -0,0 +1,4 @@
mod handler;
pub use handler::ldap_handler;
mod stream;
pub use stream::{LdapStream, LdapStreamError};

69
src/ldap/stream.rs Normal file
View file

@ -0,0 +1,69 @@
use futures_util::StreamExt;
use ldap3_proto::{LdapCodec, LdapMsg};
use tokio::time::{Duration, timeout};
use tokio_util::codec::Framed;
use uuid::Uuid;
use std::fmt;
use crate::stream::{AbstractSocketAddr, AbstractStreamKind};
#[derive(Debug)]
pub enum LdapStreamError {
ClientClosed,
Timeout(Duration),
LdapError(std::io::Error),
}
impl fmt::Display for LdapStreamError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ClientClosed => write!(f, "Client closed the connection"),
Self::Timeout(d) => write!(f, "Client timeout after {}ms", d.as_millis()),
Self::LdapError(e) => write!(f, "Failed to parse LDAP message: {e:?}"),
}
}
}
impl std::error::Error for LdapStreamError {}
pub struct LdapStream {
pub remote_addr: AbstractSocketAddr,
pub inner: Framed<AbstractStreamKind, LdapCodec>,
/// An arbitrary ID to correlate logs with a specific TCP session
pub session: Uuid,
pub timeout: Duration,
}
impl LdapStream {
pub fn new(
remote_addr: AbstractSocketAddr,
kind: AbstractStreamKind,
timeout: tokio::time::Duration,
) -> Self {
Self {
remote_addr,
inner: Framed::new(kind, LdapCodec::new(None, None)),
session: Uuid::new_v4(),
timeout,
}
}
pub async fn next(&mut self) -> Result<LdapMsg, LdapStreamError> {
// Check for timeout
let Ok(msg) = timeout(self.timeout, self.inner.next()).await else {
return Err(LdapStreamError::Timeout(self.timeout));
};
// Check for closed client connection
let Some(msg) = msg else {
return Err(LdapStreamError::ClientClosed);
};
// So far the message may or may not be a valid LDAP message,
// but using the codec some other conditions (eg. OOM) may trigger
// an error. We abort whether the message failed because it was malformed,
// because of transient IO error, or any other error condition.
msg.map_err(LdapStreamError::LdapError)
}
}