pub mod connection;
pub mod session;
use crate::config::Config;
use crate::directory::{AuthHandler, Directory};
use crate::yaml::{self, YamlWatcher};
use std::sync::{Arc, RwLock};
use tokio::net::TcpListener;
use tracing::{error, info};
pub struct Server {
config: Config,
directory: Arc<RwLock<Directory>>,
auth_handler: Arc<AuthHandler>,
}
impl Server {
pub async fn new(config: Config) -> crate::Result<Self> {
let (yaml_dir, schema) = yaml::parse_directory_file(&config.yaml_file).await?;
let directory = Directory::from_yaml(yaml_dir, schema);
info!("Loaded directory with base DN: {}", directory.base_dn);
let auth_handler = AuthHandler::new(config.allow_anonymous);
Ok(Self {
config,
directory: Arc::new(RwLock::new(directory)),
auth_handler: Arc::new(auth_handler),
})
}
pub async fn run(self) -> crate::Result<()> {
let reload_rx = if self.config.hot_reload {
info!("Hot-reload enabled, watching YAML file for changes");
let (_watcher, rx) = YamlWatcher::new(&self.config.yaml_file)?;
Some(rx)
} else {
None
};
if let Some(mut rx) = reload_rx {
let yaml_path = self.config.yaml_file.clone();
let directory = Arc::clone(&self.directory);
tokio::spawn(async move {
while rx.changed().await.is_ok() {
info!("Reloading YAML directory file...");
match yaml::parse_directory_file(&yaml_path).await {
Ok((yaml_dir, schema)) => {
let new_directory = Directory::from_yaml(yaml_dir, schema);
match directory.write() {
Ok(mut dir) => {
*dir = new_directory;
info!("Successfully reloaded directory");
}
Err(e) => {
error!(
"Failed to acquire write lock for directory reload: {}",
e
);
}
}
}
Err(e) => {
error!("Failed to reload YAML file: {}", e);
}
}
}
});
}
let listener = TcpListener::bind(&self.config.bind_address).await?;
info!("LDAP server listening on {}", self.config.bind_address);
loop {
match listener.accept().await {
Ok((socket, addr)) => {
info!("New connection from {}", addr);
let directory = Arc::clone(&self.directory);
let auth_handler = Arc::clone(&self.auth_handler);
tokio::spawn(async move {
let dir_snapshot = match directory.read() {
Ok(dir) => Arc::new(dir.clone()),
Err(e) => {
error!("Failed to read directory: {}", e);
return;
}
};
if let Err(e) =
connection::handle_connection(socket, dir_snapshot, auth_handler).await
{
error!("Connection error: {}", e);
}
});
}
Err(e) => {
error!("Failed to accept connection: {}", e);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use std::path::PathBuf;
use tempfile::NamedTempFile;
fn create_test_yaml_file() -> NamedTempFile {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "directory:").unwrap();
writeln!(file, " base_dn: dc=test,dc=com").unwrap();
writeln!(file, "schema:").unwrap();
writeln!(file, " attributes:").unwrap();
writeln!(file, " cn:").unwrap();
writeln!(file, " syntax: string").unwrap();
writeln!(file, " multi_valued: false").unwrap();
writeln!(file, "entries:").unwrap();
writeln!(file, " - dn: cn=test,dc=test,dc=com").unwrap();
writeln!(file, " objectClass: [top, person]").unwrap();
writeln!(file, " cn: test").unwrap();
file.flush().unwrap();
file
}
#[tokio::test]
async fn test_server_new() {
let yaml_file = create_test_yaml_file();
let config = Config {
yaml_file: yaml_file.path().to_path_buf(),
bind_address: "127.0.0.1:389".parse().unwrap(),
base_dn: None,
allow_anonymous: false,
hot_reload: false,
log_level: tracing::Level::INFO,
};
let server = Server::new(config).await.unwrap();
assert!(server.directory.read().unwrap().base_dn == "dc=test,dc=com");
assert!(!server.auth_handler.is_anonymous_allowed());
}
#[tokio::test]
async fn test_server_new_with_anonymous() {
let yaml_file = create_test_yaml_file();
let config = Config {
yaml_file: yaml_file.path().to_path_buf(),
bind_address: "127.0.0.1:389".parse().unwrap(),
base_dn: None,
allow_anonymous: true,
hot_reload: false,
log_level: tracing::Level::INFO,
};
let server = Server::new(config).await.unwrap();
assert!(server.auth_handler.is_anonymous_allowed());
}
#[tokio::test]
async fn test_server_new_invalid_yaml() {
let config = Config {
yaml_file: PathBuf::from("/nonexistent/file.yaml"),
bind_address: "127.0.0.1:389".parse().unwrap(),
base_dn: None,
allow_anonymous: false,
hot_reload: false,
log_level: tracing::Level::INFO,
};
let result = Server::new(config).await;
assert!(result.is_err());
}
#[allow(dead_code)]
async fn test_server_run_with_hot_reload() {
let yaml_file = create_test_yaml_file();
let config = Config {
yaml_file: yaml_file.path().to_path_buf(),
bind_address: "127.0.0.1:0".parse().unwrap(), base_dn: None,
allow_anonymous: false,
hot_reload: true,
log_level: tracing::Level::INFO,
};
let server = Server::new(config).await.unwrap();
let handle = tokio::spawn(async move {
let _ = tokio::time::timeout(std::time::Duration::from_millis(100), server.run()).await;
});
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
handle.abort();
}
}