1use crate::LdapServerConn;
2use rand::Rng;
3use random_port::{PortPicker, Protocol};
4use rcgen::{CertificateParams, KeyPair, SanType};
5use std::net::{IpAddr, ToSocketAddrs};
6use std::path::{Path, PathBuf};
7use std::process::Stdio;
8use std::str::FromStr;
9use std::time::Duration;
10use tempfile::tempdir;
11use tokio::fs;
12use tokio::io::AsyncBufReadExt;
13use tokio::net::TcpStream;
14use tokio::process::Command;
15use tokio::time::{sleep, timeout};
16use tracing::debug;
17use url::Url;
18
19const INIT_LDIF: &str = include_str!("init.ldif");
20const POSSIBLE_SCHEMA_DIR: &[&str] = &[
21 "/etc/ldap/schema",
22 "/usr/local/etc/openldap/schema",
23 "/etc/openldap/schema/",
24];
25
26#[derive(Debug)]
27enum LdapFile {
28 SystemSchema(PathBuf),
29 File { template: bool, file: PathBuf },
30 Text { template: bool, content: String },
31}
32
33#[derive(Debug)]
35pub struct LdapServerBuilder {
36 base_dn: String,
37 root_dn: String,
38 root_pw: String,
39 bind_addr: Option<String>,
40 port: Option<u16>,
41 ssl_port: Option<u16>,
42 includes: Vec<(u8, LdapFile)>,
43 ssl_cert_key: Option<(String, String)>,
44}
45
46impl LdapServerBuilder {
47 pub fn empty(
49 base_dn: impl Into<String>,
50 root_dn: impl Into<String>,
51 root_pw: impl Into<String>,
52 ) -> Self {
53 let base_dn = base_dn.into();
54 let root_dn = root_dn.into();
55 let root_pw = root_pw.into();
56
57 Self {
58 base_dn,
59 root_dn,
60 root_pw,
61 bind_addr: None,
62 port: None,
63 ssl_port: None,
64 includes: vec![],
65 ssl_cert_key: None,
66 }
67 }
68
69 pub fn new(base_dn: &str) -> Self {
71 let root_dn = format!("cn=admin,{base_dn}");
72 let root_pw = "secret".to_string();
73 LdapServerBuilder::empty(base_dn, root_dn, root_pw).add_template(0, INIT_LDIF)
74 }
75
76 pub fn ssl_certificates(mut self, certificate: String, key: String) -> Self {
78 self.ssl_cert_key = Some((certificate, key));
79 self
80 }
81
82 pub fn bind_addr(mut self, bind_addr: &str) -> Self {
84 self.bind_addr = Some(bind_addr.to_string());
85 self
86 }
87
88 pub fn port(mut self, port: u16) -> Self {
90 self.port = Some(port);
91 self
92 }
93
94 pub fn ssl_port(mut self, port: u16) -> Self {
96 self.ssl_port = Some(port);
97 self
98 }
99
100 pub fn add_system_file<P: AsRef<Path>>(mut self, dbnum: u8, file: P) -> Self {
115 self.includes
116 .push((dbnum, LdapFile::SystemSchema(file.as_ref().to_path_buf())));
117 self
118 }
119
120 pub fn add(mut self, dbnum: u8, content: &str) -> Self {
148 self.includes.push((
149 dbnum,
150 LdapFile::Text {
151 template: false,
152 content: content.to_string(),
153 },
154 ));
155 self
156 }
157
158 pub fn add_file<P: AsRef<Path>>(mut self, dbnum: u8, file: P) -> Self {
160 self.includes.push((
161 dbnum,
162 LdapFile::File {
163 template: true,
164 file: file.as_ref().to_path_buf(),
165 },
166 ));
167 self
168 }
169
170 pub fn add_template(mut self, dbnum: u8, content: &str) -> Self {
185 self.includes.push((
186 dbnum,
187 LdapFile::Text {
188 template: true,
189 content: content.to_string(),
190 },
191 ));
192 self
193 }
194
195 pub fn add_template_file<P: AsRef<Path>>(mut self, dbnum: u8, file: P) -> Self {
197 self.includes.push((
198 dbnum,
199 LdapFile::File {
200 template: true,
201 file: file.as_ref().to_path_buf(),
202 },
203 ));
204 self
205 }
206
207 async fn build_config(
208 includes: Vec<(u8, LdapFile)>,
209 work_dir: &Path,
210 config_dir: &Path,
211 system_schema_dir: &Path,
212 ) {
213 fs::create_dir(&config_dir)
214 .await
215 .expect("cannot create config dir");
216
217 for (idx, (dbnum, include)) in includes.into_iter().enumerate() {
218 let file = match include {
219 LdapFile::SystemSchema(file) => system_schema_dir.join(file),
220 LdapFile::File {
221 template: false,
222 file,
223 } => file,
224 LdapFile::Text {
225 template: false,
226 content,
227 } => {
228 let tmp_ldif = work_dir.join(format!("tmp_{idx}.ldif"));
229 tokio::fs::write(&tmp_ldif, content).await.unwrap();
230 tmp_ldif
231 }
232 LdapFile::File { template: true, .. } | LdapFile::Text { template: true, .. } => {
233 panic!("Templates should be already built");
234 }
235 };
236
237 LdapServerBuilder::load_ldif(config_dir, dbnum, file).await;
238 }
239 }
240
241 async fn load_ldif(config_dir: &Path, dbnum: u8, file: PathBuf) {
242 debug!("slapadd dbnum: {dbnum} file: {}", file.display());
243
244 let db_number = dbnum.to_string();
245 let output = Command::new("slapadd")
247 .arg("-F")
248 .arg(config_dir)
249 .arg("-n")
250 .arg(db_number)
251 .arg("-l")
252 .arg(&file)
253 .output()
254 .await
255 .expect("failed to execute slapadd");
256
257 if !output.status.success() {
258 panic!(
259 "slapadd command exited with error {}, stdout: {}, stderr: {} on file {}",
260 output.status,
261 String::from_utf8_lossy(&output.stdout),
262 String::from_utf8_lossy(&output.stderr),
263 file.display()
264 );
265 }
266 }
267
268 async fn build_templates(&mut self, system_schema_dir: &Path, work_dir: &Path) {
269 let schema_dir_url = Url::from_file_path(system_schema_dir).unwrap();
270 let work_dir_path = work_dir.display().to_string();
271
272 for (_, include) in &mut self.includes {
273 let content = match include {
274 LdapFile::File {
275 template: true,
276 file,
277 } => fs::read_to_string(file).await.unwrap(),
278 LdapFile::Text {
279 template: true,
280 content,
281 } => std::mem::take(content),
282 _ => continue,
283 };
284
285 let new_content = content
286 .replace("@SCHEMADIR@", schema_dir_url.as_ref())
287 .replace("@WORKDIR@", &work_dir_path)
288 .replace("@BASEDN@", &self.base_dn)
289 .replace("@ROOTDN@", &self.root_dn)
290 .replace("@ROOTPW@", &self.root_pw);
291
292 *include = LdapFile::Text {
293 template: false,
294 content: new_content,
295 };
296 }
297 }
298
299 pub async fn run(mut self) -> LdapServerConn {
313 let schema_dir = find_slapd_schema_dir()
314 .await
315 .expect("no slapd schema directory found. Is openldap server installed?");
316 let host = self
317 .bind_addr
318 .clone()
319 .unwrap_or_else(|| "127.0.0.1".to_string());
320 let port_picker = PortPicker::new()
321 .host(host.clone())
322 .protocol(Protocol::Tcp)
323 .random(true);
324 let port = self.port.unwrap_or_else(|| {
325 port_picker.pick().unwrap_or_else(|_| {
326 let mut rng = rand::thread_rng();
327 rng.gen_range(15000..55000)
328 })
329 });
330
331 let ssl_port = self.ssl_port.unwrap_or_else(|| {
332 port_picker.pick().unwrap_or_else(|_| {
333 let mut rng = rand::thread_rng();
334 rng.gen_range(15000..55000)
335 })
336 });
337
338 let url = format!("ldap://{host}:{port}");
339 let ssl_url = format!("ldaps://{host}:{ssl_port}");
340 let dir = tempdir().unwrap();
341
342 let (ssl_cert_pem, ssl_key_pem) = if let Some(keys) = self.ssl_cert_key.clone() {
343 keys
344 } else {
345 let params = if let Ok(addr) = IpAddr::from_str(&host) {
346 let mut params = CertificateParams::new(vec![]).unwrap();
347 params.subject_alt_names.push(SanType::IpAddress(addr));
348 params
349 } else {
350 CertificateParams::new(vec![host.clone()]).unwrap()
351 };
352
353 let key_pair = KeyPair::generate().unwrap();
354 let cert = params.self_signed(&key_pair).unwrap();
355 let ssl_cert_pem = cert.pem();
356 let ssl_key_pem = key_pair.serialize_pem();
357 (ssl_cert_pem, ssl_key_pem)
358 };
359
360 let cert_pem = dir.path().join("cert.pem");
361 fs::write(&cert_pem, &ssl_cert_pem).await.unwrap();
362
363 let key_pem = dir.path().join("key.pem");
364 fs::write(&key_pem, &ssl_key_pem).await.unwrap();
365
366 self.build_templates(schema_dir, dir.path()).await;
367 let config_dir = dir.path().join("config");
368 LdapServerBuilder::build_config(self.includes, dir.path(), &config_dir, schema_dir).await;
369
370 let urls = format!("{url} {ssl_url}");
371 let mut server = Command::new("slapd")
373 .arg("-F")
374 .arg(&config_dir)
375 .arg("-d")
376 .arg("2048")
377 .arg("-h")
378 .arg(&urls)
379 .stderr(Stdio::piped())
380 .spawn()
381 .unwrap();
382
383 let stderr = server.stderr.take().unwrap();
385 let mut lines = tokio::io::BufReader::new(stderr).lines();
386 let timeouted = timeout(Duration::from_secs(60), async {
387 while let Some(line) = lines.next_line().await.unwrap() {
388 debug!("slapd: {line}");
389 if line.ends_with("slapd starting") {
390 return true;
391 }
392 }
393 false
394 })
395 .await;
396
397 if timeouted.is_err() || timeouted == Ok(false) {
398 let _ = server.kill().await;
399 panic!("Failed to start slapd server: timeout");
400 }
401
402 let timeouted = timeout(Duration::from_secs(60), async {
403 while !is_tcp_port_open(&host, port).await {
404 debug!("tcp port {port} is not open yet, waiting...");
405 sleep(Duration::from_micros(100)).await;
406 }
407 })
408 .await;
409
410 if timeouted.is_err() {
411 let _ = server.kill().await;
412 panic!("Failed to start slapd server, port {port} not open");
413 }
414
415 debug!("Started ldap server on {urls}");
416
417 LdapServerConn {
418 url,
419 host,
420 port,
421 ssl_url,
422 ssl_port,
423 ssl_cert_pem,
424 dir,
425 base_dn: self.base_dn,
426 root_dn: self.root_dn,
427 root_pw: self.root_pw,
428 server,
429 }
430 }
431}
432
433async fn find_slapd_schema_dir() -> Option<&'static Path> {
434 for dir in POSSIBLE_SCHEMA_DIR {
435 let dir: &Path = dir.as_ref();
436 if tokio::fs::metadata(dir)
437 .await
438 .map(|m| m.is_dir())
439 .unwrap_or(false)
440 {
441 return Some(dir);
442 }
443 }
444 None
445}
446
447async fn is_tcp_port_open(host: &str, port: u16) -> bool {
448 let addr = (host, port).to_socket_addrs().unwrap().next().unwrap();
449 let Ok(sock) = timeout(Duration::from_secs(1), TcpStream::connect(&addr)).await else {
450 return false;
451 };
452 sock.is_ok()
453}