1#![deny(warnings)]
2#![warn(unused_extern_crates)]
3#![deny(clippy::todo)]
4#![deny(clippy::unimplemented)]
5#![deny(clippy::unwrap_used)]
6#![deny(clippy::panic)]
7#![deny(clippy::await_holding_lock)]
8#![deny(clippy::needless_pass_by_value)]
9#![deny(clippy::trivially_copy_pass_by_ref)]
10#![allow(clippy::expect_used)]
12
13use base64::{Engine as _, engine::general_purpose};
14use futures_util::sink::SinkExt;
15use futures_util::stream::StreamExt;
16use ldap3_proto::LdapCodec;
17use ldap3_proto::proto::*;
18use rustls_platform_verifier::ConfigVerifierExt;
19use serde::{Deserialize, Serialize};
20use std::borrow::Borrow;
21use std::collections::{BTreeMap, BTreeSet};
22use std::fmt;
23use std::sync::Arc;
24use tokio::io::{ReadHalf, WriteHalf};
25use tokio::net::TcpStream;
26use tokio::time;
27
28use tokio_rustls::{
29 TlsConnector,
30 client::TlsStream,
31 rustls::Error as RustlsError,
32 rustls::client::{ClientConfig, danger::*},
33 rustls::pki_types::{CertificateDer, ServerName, UnixTime},
34 rustls::{DigitallySignedStruct, SignatureScheme},
35};
36
37use tokio_util::codec::{FramedRead, FramedWrite};
38use tracing::{error, info, trace, warn};
39use url::{Host, Url};
40use uuid::Uuid;
41
42pub use ldap3_proto::filter;
43pub use ldap3_proto::proto;
44pub use search::LdapSearchResult;
45pub use syncrepl::{LdapSyncRepl, LdapSyncReplEntry, LdapSyncStateValue};
46pub use tokio::time::Duration;
47
48mod addirsync;
49mod search;
50mod syncrepl;
51
52#[non_exhaustive]
53#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
54#[repr(i32)]
55pub enum LdapError {
56 InvalidUrl = -1,
57 LdapiNotSupported = -2,
58 UseCldapTool = -3,
59 ResolverError = -4,
60 ConnectError = -5,
61 TlsError = -6,
62 PasswordNotFound = -7,
63 AnonymousInvalidState = -8,
64 TransportWriteError = -9,
65 TransportReadError = -10,
66 InvalidProtocolState = -11,
67 FileIOError = -12,
68
69 UnavailableCriticalExtension = 12,
70 InvalidCredentials = 49,
71 InsufficentAccessRights = 50,
72 UnwillingToPerform = 53,
73 EsyncRefreshRequired = 4096,
74 NotImplemented = 9999,
75}
76
77impl From<LdapResultCode> for LdapError {
78 fn from(code: LdapResultCode) -> Self {
79 match code {
80 LdapResultCode::InvalidCredentials => LdapError::InvalidCredentials,
81 LdapResultCode::InsufficentAccessRights => LdapError::InsufficentAccessRights,
82 LdapResultCode::EsyncRefreshRequired => LdapError::EsyncRefreshRequired,
83 LdapResultCode::UnavailableCriticalExtension => LdapError::UnavailableCriticalExtension,
84 LdapResultCode::UnwillingToPerform => LdapError::UnwillingToPerform,
85 err => {
86 error!("{:?} not implemented yet!!", err);
87 LdapError::NotImplemented
88 }
89 }
90 }
91}
92
93impl fmt::Display for LdapError {
94 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95 match self {
96 LdapError::InvalidUrl => write!(f, "Invalid URL"),
97 LdapError::LdapiNotSupported => write!(f, "Ldapi Not Supported"),
98 LdapError::UseCldapTool => write!(f, "Use cldap tool for cldap:// urls"),
99 LdapError::ResolverError => write!(f, "Failed to resolve hostname or invalid ip"),
100 LdapError::ConnectError => write!(f, "Failed to connect to host"),
101 LdapError::TlsError => write!(f, "Failed to establish TLS"),
102 LdapError::PasswordNotFound => write!(f, "No password available for bind"),
103 LdapError::AnonymousInvalidState => write!(f, "Invalid Anonymous bind state"),
104 LdapError::InvalidProtocolState => {
105 write!(f, "The LDAP server sent a response we did not expect")
106 }
107 LdapError::FileIOError => {
108 write!(f, "An error occurred while accessing a file")
109 }
110 LdapError::TransportReadError => {
111 write!(f, "An error occurred reading from the transport")
112 }
113 LdapError::TransportWriteError => {
114 write!(f, "An error occurred writing to the transport")
115 }
116 LdapError::UnavailableCriticalExtension => {
117 write!(f, "An extension marked as critical was not available")
118 }
119 LdapError::InvalidCredentials => write!(f, "Invalid DN or Password"),
120 LdapError::InsufficentAccessRights => write!(f, "Insufficient Access"),
121 LdapError::UnwillingToPerform => write!(
122 f,
123 "Too many failures, server is unwilling to perform the operation."
124 ),
125 LdapError::EsyncRefreshRequired => write!(
126 f,
127 "An initial content sync is required. The current cookie should be considered invalid."
128 ),
129 LdapError::NotImplemented => write!(
130 f,
131 "An error occurred, but we haven't implemented code to handle this error yet."
132 ),
133 }
134 }
135}
136
137pub type LdapResult<T> = Result<T, LdapError>;
138
139enum LdapReadTransport {
140 Plain(FramedRead<ReadHalf<TcpStream>, LdapCodec>),
141 Tls(FramedRead<ReadHalf<TlsStream<TcpStream>>, LdapCodec>),
142}
143
144impl fmt::Debug for LdapReadTransport {
145 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146 match self {
147 LdapReadTransport::Plain(_) => f
148 .debug_struct("LdapReadTransport")
149 .field("type", &"plain")
150 .finish(),
151 LdapReadTransport::Tls(_) => f
152 .debug_struct("LdapReadTransport")
153 .field("type", &"tls")
154 .finish(),
155 }
156 }
157}
158
159enum LdapWriteTransport {
160 Plain(FramedWrite<WriteHalf<TcpStream>, LdapCodec>),
161 Tls(FramedWrite<WriteHalf<TlsStream<TcpStream>>, LdapCodec>),
162}
163
164impl fmt::Debug for LdapWriteTransport {
165 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166 match self {
167 LdapWriteTransport::Plain(_) => f
168 .debug_struct("LdapWriteTransport")
169 .field("type", &"plain")
170 .finish(),
171 LdapWriteTransport::Tls(_) => f
172 .debug_struct("LdapWriteTransport")
173 .field("type", &"tls")
174 .finish(),
175 }
176 }
177}
178
179impl LdapWriteTransport {
180 async fn send(&mut self, msg: LdapMsg) -> LdapResult<()> {
181 match self {
182 LdapWriteTransport::Plain(f) => f.send(msg).await.map_err(|e| {
183 info!(?e, "transport error");
184 LdapError::TransportWriteError
185 }),
186 LdapWriteTransport::Tls(f) => f.send(msg).await.map_err(|e| {
187 info!(?e, "transport error");
188 LdapError::TransportWriteError
189 }),
190 }
191 }
192}
193
194impl LdapReadTransport {
195 async fn next(&mut self) -> LdapResult<LdapMsg> {
196 match self {
197 LdapReadTransport::Plain(f) => f.next().await.transpose().map_err(|e| {
198 info!(?e, "transport error");
199 LdapError::TransportReadError
200 })?,
201 LdapReadTransport::Tls(f) => f.next().await.transpose().map_err(|e| {
202 info!(?e, "transport error");
203 LdapError::TransportReadError
204 })?,
205 }
206 .ok_or_else(|| {
207 info!("connection closed");
208 LdapError::TransportReadError
209 })
210 }
211}
212
213#[derive(Debug, Deserialize, Serialize)]
214pub struct LdapEntry {
215 pub dn: String,
216 pub attrs: BTreeMap<String, BTreeSet<String>>,
217}
218
219impl LdapEntry {
220 pub fn get_ava_single(&self, attr: &str) -> Option<&str> {
221 if let Some(ava) = self.attrs.get(attr) {
222 if ava.len() == 1 {
223 ava.iter().next().map(String::as_ref)
224 } else {
225 None
226 }
227 } else {
228 None
229 }
230 }
231
232 pub fn remove_ava_single(&mut self, attr: &str) -> Option<String> {
233 if let Some(ava) = self.attrs.remove(attr) {
234 if ava.len() == 1 {
235 ava.into_iter().next()
236 } else {
237 None
238 }
239 } else {
240 None
241 }
242 }
243
244 pub fn remove_ava(&mut self, attr: &str) -> Option<BTreeSet<String>> {
245 self.attrs.remove(attr)
246 }
247}
248
249impl From<LdapSearchResultEntry> for LdapEntry {
250 fn from(ent: LdapSearchResultEntry) -> Self {
251 let LdapSearchResultEntry { dn, attributes } = ent;
252
253 let attrs = attributes
254 .into_iter()
255 .map(|LdapPartialAttribute { atype, vals }| {
256 let atype = atype.to_lowercase();
257
258 let lower = atype == "objectclass";
259
260 let va = vals
261 .into_iter()
262 .map(|bin| {
263 std::str::from_utf8(&bin)
264 .map(|s| {
265 if lower {
266 s.to_lowercase()
267 } else {
268 s.to_string()
269 }
270 })
271 .unwrap_or_else(|_| general_purpose::URL_SAFE.encode(&bin))
272 })
273 .collect();
274 (atype, va)
275 })
276 .collect();
277
278 LdapEntry { dn, attrs }
279 }
280}
281
282#[derive(Debug, Clone)]
283pub struct LdapClientBuilder<U> {
284 url: U,
285 timeout: Duration,
286 max_ber_size: Option<usize>,
288 rustls_client: Option<Arc<ClientConfig>>,
289}
290
291impl<U: Borrow<Url>> LdapClientBuilder<U> {
292 pub fn new(url: U) -> Self {
293 Self {
294 url,
295 timeout: Duration::from_secs(30),
296 max_ber_size: None,
297 rustls_client: None,
298 }
299 }
300
301 pub fn set_tls_config(&mut self, config: Option<Arc<ClientConfig>>) {
302 self.rustls_client = config
303 }
304
305 pub fn with_tls_config(mut self, config: ClientConfig) -> Self {
309 self.set_tls_config(Some(Arc::new(config)));
310 self
311 }
312
313 pub fn danger_accept_invalid_certs(self) -> Self {
315 warn!("⚠️ CERTIFICATE VERIFICATION IS DISABLED. THIS IS DANGEROUS!!!!");
316 let yolo_cert_validator = Arc::new(YoloCertValidator);
317
318 let client_config = ClientConfig::builder()
319 .dangerous()
320 .with_custom_certificate_verifier(yolo_cert_validator)
321 .with_no_client_auth();
322 self.with_tls_config(client_config)
323 }
324
325 pub fn set_timeout(self, timeout: Duration) -> Self {
326 Self { timeout, ..self }
327 }
328
329 pub fn max_ber_size(self, max_ber_size: Option<usize>) -> Self {
331 Self {
332 max_ber_size,
333 ..self
334 }
335 }
336
337 #[tracing::instrument(level = "debug", skip_all)]
338 pub async fn build(self) -> LdapResult<LdapClient> {
339 let LdapClientBuilder {
340 url,
341 timeout,
342 max_ber_size,
343 rustls_client,
344 } = self;
345
346 let url = url.borrow();
347
348 info!(%url);
349 info!(?timeout);
350
351 let need_tls = match url.scheme() {
354 "ldapi" => return Err(LdapError::LdapiNotSupported),
355 "cldap" => return Err(LdapError::UseCldapTool),
356 "ldap" => false,
357 "ldaps" => true,
358 _ => return Err(LdapError::InvalidUrl),
359 };
360
361 info!(%need_tls);
362 let addrs = url
369 .socket_addrs(|| Some(if need_tls { 636 } else { 389 }))
370 .map_err(|e| {
371 info!(?e, "resolver error");
372 LdapError::ResolverError
373 })?;
374
375 if addrs.is_empty() {
376 return Err(LdapError::ResolverError);
377 }
378
379 addrs.iter().for_each(|address| info!(?address));
380
381 let mut aiter = addrs.into_iter();
382
383 let tcpstream = loop {
385 if let Some(addr) = aiter.next() {
386 let sleep = time::sleep(timeout);
387 tokio::pin!(sleep);
388 tokio::select! {
389 maybe_stream = TcpStream::connect(addr) => {
390 match maybe_stream {
391 Ok(t) => {
392 info!(?addr, "connection established");
393 break t;
394 }
395 Err(e) => {
396 info!(?addr, ?e, "error");
397 continue;
398 }
399 }
400 }
401 _ = &mut sleep => {
402 info!(?addr, "timeout");
403 continue;
404 }
405 }
406 } else {
407 return Err(LdapError::ConnectError);
408 }
409 };
410
411 let max_ber_size = max_ber_size.unwrap_or(ldap3_proto::DEFAULT_MAX_BER_SIZE);
413
414 let (write_transport, read_transport) = if need_tls {
416 let tls_client_config = if let Some(client_config) = rustls_client {
417 client_config
418 } else {
419 Arc::new(ClientConfig::with_platform_verifier().map_err(|e| {
420 error!(?e, "rustls");
421 LdapError::TlsError
422 })?)
423 };
424
425 let tls_connector = TlsConnector::from(tls_client_config);
426
427 let server_name = match url.host() {
428 Some(Host::Domain(name)) => {
429 ServerName::try_from(name.to_owned()).map_err(|err| {
430 error!(?err, "server name invalid");
431 LdapError::TlsError
432 })?
433 }
434 Some(Host::Ipv4(addr)) => ServerName::from(addr),
435 Some(Host::Ipv6(addr)) => ServerName::from(addr),
436 None => {
437 error!("url invalid");
438 return Err(LdapError::TlsError);
439 }
440 };
441
442 let tlsstream = tls_connector
443 .connect(
444 server_name,
445 tcpstream,
447 )
448 .await
449 .map_err(|e| {
450 error!(?e, "rustls");
451 LdapError::TlsError
452 })?;
453
454 info!("tls configured");
455
456 let (r, w) = tokio::io::split(tlsstream);
457 (
458 LdapWriteTransport::Tls(FramedWrite::new(w, LdapCodec::default())),
459 LdapReadTransport::Tls(FramedRead::new(
460 r,
461 LdapCodec::new(Some(max_ber_size), None),
462 )),
463 )
464 } else {
465 let (r, w) = tokio::io::split(tcpstream);
466 (
467 LdapWriteTransport::Plain(FramedWrite::new(w, LdapCodec::default())),
468 LdapReadTransport::Plain(FramedRead::new(
469 r,
470 LdapCodec::new(Some(max_ber_size), None),
471 )),
472 )
473 };
474
475 let msg_counter = 1;
476
477 Ok(LdapClient {
479 read_transport,
480 write_transport,
481 msg_counter,
482 })
483 }
484}
485
486#[derive(Debug)]
487pub struct LdapClient {
488 read_transport: LdapReadTransport,
489 write_transport: LdapWriteTransport,
490 msg_counter: i32,
491}
492
493impl LdapClient {
494 fn get_next_msgid(&mut self) -> i32 {
495 let msgid = self.msg_counter;
496 self.msg_counter += 1;
497 msgid
498 }
499
500 #[tracing::instrument(level = "debug", skip_all)]
501 pub async fn bind<S: Into<String>>(&mut self, dn: S, pw: S) -> LdapResult<()> {
502 let dn = dn.into();
503 info!(%dn);
504 let msgid = self.get_next_msgid();
505
506 let msg = LdapMsg {
507 msgid,
508 op: LdapOp::BindRequest(LdapBindRequest {
509 dn,
510 cred: LdapBindCred::Simple(pw.into()),
511 }),
512 ctrl: vec![],
513 };
514
515 self.write_transport.send(msg).await?;
516
517 self.read_transport
519 .next()
520 .await
521 .and_then(|msg| match msg.op {
522 LdapOp::BindResponse(res) => {
523 if res.res.code == LdapResultCode::Success {
524 info!("bind success");
525 Ok(())
526 } else {
527 info!(?res.res.code);
528 Err(LdapError::from(res.res.code))
529 }
530 }
531 op => {
532 trace!(?op);
533 Err(LdapError::InvalidProtocolState)
534 }
535 })
536 }
537
538 #[tracing::instrument(level = "debug", skip_all)]
539 pub async fn whoami(&mut self) -> LdapResult<Option<String>> {
540 let msgid = self.get_next_msgid();
541
542 let msg = LdapMsg {
543 msgid,
544 op: LdapOp::ExtendedRequest(Into::into(LdapWhoamiRequest {})),
545 ctrl: vec![],
546 };
547
548 self.write_transport.send(msg).await?;
549
550 self.read_transport
551 .next()
552 .await
553 .and_then(|msg| match msg.op {
554 LdapOp::ExtendedResponse(ler) => LdapWhoamiResponse::try_from(&ler)
555 .map_err(|_| LdapError::InvalidProtocolState)
556 .map(|res| res.dn),
557 op => {
558 trace!(?op);
559 Err(LdapError::InvalidProtocolState)
560 }
561 })
562 }
563}
564
565#[derive(Debug)]
566struct YoloCertValidator;
568
569impl ServerCertVerifier for YoloCertValidator {
570 fn verify_server_cert(
571 &self,
572 _end_entity: &CertificateDer<'_>,
573 _intermediates: &[CertificateDer<'_>],
574 _server_name: &ServerName<'_>,
575 _ocsp_response: &[u8],
576 _now: UnixTime,
577 ) -> Result<ServerCertVerified, RustlsError> {
578 Ok(ServerCertVerified::assertion())
580 }
581
582 fn verify_tls12_signature(
583 &self,
584 _message: &[u8],
585 _cert: &CertificateDer<'_>,
586 _dss: &DigitallySignedStruct,
587 ) -> Result<HandshakeSignatureValid, RustlsError> {
588 Ok(HandshakeSignatureValid::assertion())
589 }
590
591 fn verify_tls13_signature(
592 &self,
593 _message: &[u8],
594 _cert: &CertificateDer<'_>,
595 _dss: &DigitallySignedStruct,
596 ) -> Result<HandshakeSignatureValid, RustlsError> {
597 Ok(HandshakeSignatureValid::assertion())
598 }
599
600 fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
601 vec![
602 SignatureScheme::RSA_PKCS1_SHA384,
603 SignatureScheme::ECDSA_NISTP256_SHA256,
604 SignatureScheme::RSA_PKCS1_SHA256,
605 SignatureScheme::ECDSA_NISTP384_SHA384,
606 SignatureScheme::RSA_PKCS1_SHA512,
607 SignatureScheme::ECDSA_NISTP521_SHA512,
608 ]
609 }
610}
611
612#[test]
614fn test_ldapclient_builder() {
615 let url = Url::parse("ldap://ldap.example.com:389").expect("failed to parse URL");
616 let client = LdapClientBuilder::new(&url).max_ber_size(Some(1234567));
617 assert_eq!(client.timeout, Duration::from_secs(30));
618 let client = client.set_timeout(Duration::from_secs(60));
619 assert_eq!(client.timeout, Duration::from_secs(60));
620 assert_eq!(client.max_ber_size, Some(1234567));
621 assert!(client.rustls_client.is_none());
622}