Compare commits
7 changed files with 49 additions and 345 deletions
20
README.md
20
README.md
|
|
@ -7,12 +7,6 @@ Proxy LDAP requests to different LDAP servers based on base DN. Based on code fr
|
|||
- [x] No TLS setup ; use only in trusted networks
|
||||
- [x] LDAP bind requests
|
||||
- [x] LDAP search requests
|
||||
- [x] extract requested backend from mail attribute filter
|
||||
- [x] bind to requested backend with mapping `user` and `password` fields
|
||||
- [ ] **not (yet?) planned:** extract requested backend from more filters
|
||||
- [x] rewrite the search dn with backend `to` dn (eg. `ou=people,dc=a,dc=localhost` -> `ou=people,dc=example,dc=com`)
|
||||
- [x] rewrite returned entries with backend dn (eg. `uid=a,ou=people,dc=example,dc=com` -> `uid=a,ou=people,dc=a,dc=localhost`)
|
||||
- [ ] rewrite search dn and result entries for authenticated searches on a backend
|
||||
- [x] Configurable listening port
|
||||
- [ ] Default fallback to `/etc/ldap-rp/config.toml`
|
||||
- [x] Unix Domain Socket support (incoming requests)
|
||||
|
|
@ -41,16 +35,10 @@ to = "example.com"
|
|||
# - start with anything else for a TCP connection
|
||||
# backend = "/run/lldap/example.com.sock"
|
||||
backend = "127.0.0.1:4389"
|
||||
# Credentials for performing search query to the backend
|
||||
# set `lldap_strict_readonly` perms on the account in lldap.
|
||||
user = "stalwart"
|
||||
password = "adminadmin"
|
||||
[[mapping]]
|
||||
from = "b.localhost"
|
||||
to = "example.com"
|
||||
backend = "127.0.0.1:5389"
|
||||
user = "stalwart"
|
||||
password = "adminadmin"
|
||||
```
|
||||
|
||||
~~By default, `ldap-rp` will look for a config file in `/etc/ldap-rp/config.toml` but you can change that
|
||||
|
|
@ -73,11 +61,3 @@ ldapwhoami -H ldap://localhost:3389 -D "cn=b,ou=people,dc=a,dc=localhost" -W
|
|||
# with `%2F` and the protocol is changed to `ldapi`
|
||||
ldapwhoami -H ldapi://%2Frun%2Fldap-rp%2Fldap-rp.sock -D "cn=b,ou=people,dc=a,dc=localhost" -W
|
||||
```
|
||||
|
||||
You can also perform a search by email without binding with specific credentials, the base dn provided will have its hostname set to the backend mapping's `from` value:
|
||||
|
||||
```
|
||||
ldapsearch -x -b "ou=people" -H "ldap://localhost:3389" -s sub "(mail=a@a.localhost)" uid mail
|
||||
```
|
||||
|
||||
This is strictly equivalent to using `-b "ou=people,dc=a,dc=localhost"` because the search dn is always overwritten.
|
||||
|
|
|
|||
|
|
@ -33,12 +33,14 @@ impl BasicLdapClient {
|
|||
t
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
// trace!(?addr, ?err, "error");
|
||||
error!("error to {addr}: {err}");
|
||||
return Err(LdapError::ConnectError);
|
||||
panic!();
|
||||
}
|
||||
Err(_) => {
|
||||
warn!("timeout to {addr}");
|
||||
return Err(LdapError::Transport);
|
||||
panic!();
|
||||
// continue;
|
||||
}
|
||||
};
|
||||
unixstream.into()
|
||||
|
|
@ -51,11 +53,12 @@ impl BasicLdapClient {
|
|||
Ok(Err(err)) => {
|
||||
// trace!(?addr, ?err, "error");
|
||||
error!("error to {addr}: {err}");
|
||||
return Err(LdapError::ConnectError);
|
||||
panic!();
|
||||
}
|
||||
Err(_) => {
|
||||
warn!("timeout to {addr}");
|
||||
return Err(LdapError::Transport);
|
||||
panic!();
|
||||
// continue;
|
||||
}
|
||||
};
|
||||
tcpstream.into()
|
||||
|
|
|
|||
|
|
@ -64,6 +64,4 @@ pub struct Mapping {
|
|||
pub from: String,
|
||||
pub to: String,
|
||||
pub backend: String,
|
||||
pub user: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
|
|
|||
38
src/dn.rs
38
src/dn.rs
|
|
@ -24,10 +24,6 @@ impl Dn {
|
|||
pub fn from_dn_str(input: &str) -> Result<Self, LdapError> {
|
||||
let mut keys: IndexMap<String, Vec<String>> = IndexMap::new();
|
||||
|
||||
if input == "" {
|
||||
return Ok(Self { keys });
|
||||
}
|
||||
|
||||
for query in input.split(',') {
|
||||
let mut query_parts = query.split('=');
|
||||
// Here we have key=val pairs
|
||||
|
|
@ -102,37 +98,3 @@ impl Dn {
|
|||
self.keys.insert("dc".to_string(), domain_components);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_basic_dn_str() {
|
||||
let s = "cn=admin,ou=people,dc=example,dc=com";
|
||||
let mut dn = Dn::from_dn_str(s).unwrap();
|
||||
|
||||
assert_eq!(&dn.to_dn_string(), s);
|
||||
assert_eq!(dn.get_hostname().as_deref(), Some("example.com"));
|
||||
dn.set_hostname("a.localhost", false);
|
||||
assert_eq!(dn.get_hostname().as_deref(), Some("a.localhost"));
|
||||
assert_eq!(&dn.to_dn_string(), "cn=admin,ou=people,dc=a,dc=localhost");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_dn_str() {
|
||||
let s = "";
|
||||
let mut dn = Dn::from_dn_str(s).unwrap();
|
||||
|
||||
assert_eq!(&dn.to_dn_string(), s);
|
||||
assert!(dn.get_hostname().is_none());
|
||||
|
||||
dn.set_hostname("a.localhost", false);
|
||||
assert_eq!(dn.get_hostname().as_deref(), None);
|
||||
assert_eq!(&dn.to_dn_string(), s);
|
||||
|
||||
dn.set_hostname("a.localhost", true);
|
||||
assert_eq!(dn.get_hostname().as_deref(), Some("a.localhost"));
|
||||
assert_eq!(&dn.to_dn_string(), "dc=a,dc=localhost");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
51
src/main.rs
51
src/main.rs
|
|
@ -63,11 +63,8 @@ pub async fn client_process<W: AsyncWrite + Unpin, R: AsyncRead + Unpin>(
|
|||
|
||||
// Start to wait for incoming packets
|
||||
while let Ok(Some(Ok(protomsg))) = timeout(LDAP_CLIENT_IO_TIMEOUT, r.next()).await {
|
||||
trace!("{:#?}", protomsg);
|
||||
let next_state = match (&mut state, protomsg) {
|
||||
// Doesn't matter what state we are in, any bind will trigger this process.
|
||||
// TODO: Support anonymous binds for search if config allows it, this will
|
||||
// allow manual testing with ldapsearch.
|
||||
(
|
||||
_,
|
||||
LdapMsg {
|
||||
|
|
@ -99,20 +96,42 @@ pub async fn client_process<W: AsyncWrite + Unpin, R: AsyncRead + Unpin>(
|
|||
ctrl,
|
||||
},
|
||||
) => {
|
||||
// It's very tempting here to just say every email exists, and let stalwart perform
|
||||
// the bind to check. However, what about *receiving emails*? Do you really want
|
||||
// to pretend every mailbox exists here instead of letting stalwart know there
|
||||
// is no such recipient? Do you????
|
||||
if let Err(e) =
|
||||
op::search::search_by_email_attribute(&mut w, sr, msgid, ctrl, config.clone())
|
||||
.await
|
||||
{
|
||||
debug!("Search unsuccessful: {e:?}");
|
||||
break;
|
||||
// We have to trigger a bind first in case we have a mapping.
|
||||
let lbr = LdapBindRequest {
|
||||
dn: "".to_string(),
|
||||
cred: LdapBindCred::Simple("".to_string()),
|
||||
};
|
||||
|
||||
let mut next_state =
|
||||
match op::bind::bind(&mut w, lbr, config.clone(), 0, Vec::default()).await {
|
||||
Ok(ns) => ns,
|
||||
Err(_) => break,
|
||||
};
|
||||
|
||||
match &mut next_state {
|
||||
Some(ClientState::Unbound) | None => {
|
||||
error!("Invalid state, bind did not return an authenticated state!");
|
||||
break;
|
||||
}
|
||||
Some(ClientState::Authenticated {
|
||||
client,
|
||||
request_dn: _,
|
||||
backend_dn: _,
|
||||
}) => {
|
||||
let search_req = op::search::SearchRequest {
|
||||
sr,
|
||||
msgid,
|
||||
ctrl,
|
||||
client,
|
||||
};
|
||||
match op::search::search(&mut w, search_req).await {
|
||||
Ok(()) => {}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We are still not logged in, but we don't abort the session
|
||||
Some(ClientState::Unbound)
|
||||
next_state
|
||||
}
|
||||
|
||||
// Authenticated message handler.
|
||||
|
|
@ -136,8 +155,6 @@ pub async fn client_process<W: AsyncWrite + Unpin, R: AsyncRead + Unpin>(
|
|||
client,
|
||||
};
|
||||
|
||||
// TODO: search that's authenticated with a backend should still rewrite
|
||||
// the search dn and result entries
|
||||
match op::search::search(&mut w, search_req).await {
|
||||
Ok(()) => None,
|
||||
Err(_) => break,
|
||||
|
|
|
|||
|
|
@ -9,9 +9,7 @@ use std::sync::Arc;
|
|||
|
||||
use crate::{BasicLdapClient, ClientState, Config, Dn, LdapError};
|
||||
|
||||
// TODO: replace with a more generic approach for different op response types
|
||||
// (bind, search) and custom error codes
|
||||
pub fn bind_operror(msgid: i32, msg: &dyn ToString) -> LdapMsg {
|
||||
pub fn bind_operror(msgid: i32, msg: &str) -> LdapMsg {
|
||||
LdapMsg {
|
||||
msgid,
|
||||
op: LdapOp::BindResponse(LdapBindResponse {
|
||||
|
|
@ -36,33 +34,6 @@ pub async fn bind<W: AsyncWrite + Unpin>(
|
|||
) -> Result<Option<ClientState>, LdapError> {
|
||||
trace!("{:?}", lbr);
|
||||
|
||||
if lbr.dn == "" {
|
||||
// Here we pretend to have successfully bound so that
|
||||
// a client performing an anonymous bind can proceed with
|
||||
// more requests (such as a search request).
|
||||
// This supports ldap search which always performs a bind.
|
||||
let resp_msg = LdapMsg {
|
||||
msgid,
|
||||
op: LdapOp::BindResponse(LdapBindResponse {
|
||||
res: LdapResult {
|
||||
code: LdapResultCode::Success,
|
||||
matcheddn: "".to_string(),
|
||||
message: "".to_string(),
|
||||
referral: vec![],
|
||||
},
|
||||
saslcreds: None,
|
||||
}),
|
||||
ctrl: vec![],
|
||||
};
|
||||
w.send(resp_msg).await.map_err(|err| {
|
||||
error!("Unable to send response: {err}");
|
||||
LdapError::Transport
|
||||
})?;
|
||||
// We still treat the client as unbounded because it doesn't
|
||||
// have a session to a backend.
|
||||
return Ok(Some(ClientState::Unbound));
|
||||
}
|
||||
|
||||
let request_dn = lbr.dn.clone();
|
||||
|
||||
debug!("Received bind request on DN: {}", lbr.dn);
|
||||
|
|
@ -81,7 +52,6 @@ pub async fn bind<W: AsyncWrite + Unpin>(
|
|||
.iter()
|
||||
.find(|x| x.from.to_lowercase() == requested_domain)
|
||||
else {
|
||||
// TODO: we should probably return an error to the client here
|
||||
debug!("No mapping found for domain {requested_domain}");
|
||||
return Err(LdapError::InvalidQuery);
|
||||
};
|
||||
|
|
@ -92,15 +62,14 @@ pub async fn bind<W: AsyncWrite + Unpin>(
|
|||
);
|
||||
dn.set_hostname(&mapping.to, false);
|
||||
lbr.dn = dn.to_dn_string();
|
||||
let backend_dn = lbr.dn.clone();
|
||||
let dn = lbr.dn.clone();
|
||||
|
||||
// We need the client to connect *and* bind to proceed here!
|
||||
let mut client = match BasicLdapClient::build(&mapping.backend).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("A client build error has occurred: {e:?}");
|
||||
// TODO: send more detailed error to the client (connection refused / timeout)
|
||||
let resp_msg = bind_operror(msgid, &"unable to bind");
|
||||
let resp_msg = bind_operror(msgid, "unable to bind");
|
||||
w.send(resp_msg).await.map_err(|err| {
|
||||
error!("Unable to send response: {err}");
|
||||
LdapError::Transport
|
||||
|
|
@ -128,7 +97,7 @@ pub async fn bind<W: AsyncWrite + Unpin>(
|
|||
}
|
||||
Err(e) => {
|
||||
error!("A client bind error has occurred: {e:?}");
|
||||
let resp_msg = bind_operror(msgid, &"unable to bind");
|
||||
let resp_msg = bind_operror(msgid, "unable to bind");
|
||||
w.send(resp_msg).await.map_err(|err| {
|
||||
error!("Unable to send response: {err}");
|
||||
LdapError::Transport
|
||||
|
|
@ -139,10 +108,10 @@ pub async fn bind<W: AsyncWrite + Unpin>(
|
|||
};
|
||||
|
||||
if valid {
|
||||
info!("Successful bind for `{request_dn}` -> `{backend_dn}`");
|
||||
info!("Successful bind for {}", dn);
|
||||
Ok(Some(ClientState::Authenticated {
|
||||
request_dn,
|
||||
backend_dn,
|
||||
backend_dn: dn,
|
||||
client,
|
||||
}))
|
||||
} else {
|
||||
|
|
|
|||
229
src/op/search.rs
229
src/op/search.rs
|
|
@ -5,10 +5,8 @@ use ldap3_proto::proto::*;
|
|||
use tokio::io::AsyncWrite;
|
||||
use tokio_util::codec::FramedWrite;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::op::bind::bind_operror;
|
||||
use crate::{BasicLdapClient, Config, Dn, LdapError};
|
||||
use crate::{BasicLdapClient, LdapError};
|
||||
|
||||
pub struct SearchRequest<'a> {
|
||||
pub sr: LdapSearchRequest,
|
||||
|
|
@ -17,231 +15,10 @@ pub struct SearchRequest<'a> {
|
|||
pub client: &'a mut BasicLdapClient,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MailDomainError {
|
||||
/// mail value not user@domain format
|
||||
InvalidMail,
|
||||
/// No mail filter found in the search query
|
||||
NoMailFilter,
|
||||
/// Filter is not And/Or/Equality for which we can find a mail filter
|
||||
InvalidFilter,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for MailDomainError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let msg = match self {
|
||||
Self::InvalidMail => {
|
||||
"No valid email address requested in mail filter in search request"
|
||||
}
|
||||
Self::NoMailFilter => "No mail filter found in search request",
|
||||
Self::InvalidFilter => "No AND/OR/EQUALITY filter found in search request",
|
||||
};
|
||||
write!(f, "{}", msg)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for MailDomainError {}
|
||||
|
||||
pub fn mail_username_domain_from_equality_filter(
|
||||
attr: &str,
|
||||
value: &str,
|
||||
) -> Result<Option<(String, String)>, MailDomainError> {
|
||||
if attr != "mail" {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut parts = value.split('@');
|
||||
let username = parts.next().unwrap();
|
||||
let Some(domain) = parts.next() else {
|
||||
debug!("Not a valid username@domain email: {value}");
|
||||
return Err(MailDomainError::InvalidMail);
|
||||
};
|
||||
|
||||
if parts.next().is_some() {
|
||||
debug!("Too many parts in mail address");
|
||||
return Err(MailDomainError::InvalidMail);
|
||||
}
|
||||
|
||||
return Ok(Some((username.to_string(), domain.to_string())));
|
||||
}
|
||||
|
||||
pub fn mail_username_domain_from_filters(
|
||||
filters: &[LdapFilter],
|
||||
) -> Result<(String, String), MailDomainError> {
|
||||
for filter in filters {
|
||||
match filter {
|
||||
LdapFilter::Equality(attr, value) => {
|
||||
if let Some(found) = mail_username_domain_from_equality_filter(attr, value)? {
|
||||
return Ok(found);
|
||||
}
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
|
||||
debug!("No mail filter found");
|
||||
return Err(MailDomainError::NoMailFilter);
|
||||
}
|
||||
|
||||
pub fn mail_username_domain_from_filter(
|
||||
filter: &LdapFilter,
|
||||
) -> Result<(String, String), MailDomainError> {
|
||||
match filter {
|
||||
LdapFilter::And(filters) => mail_username_domain_from_filters(&filters),
|
||||
LdapFilter::Or(filters) => mail_username_domain_from_filters(&filters),
|
||||
LdapFilter::Equality(attr, value) => {
|
||||
if let Some(domain) = mail_username_domain_from_equality_filter(attr, value)? {
|
||||
Ok(domain)
|
||||
} else {
|
||||
debug!("No mail filter found");
|
||||
Err(MailDomainError::NoMailFilter)
|
||||
}
|
||||
}
|
||||
_ => Err(MailDomainError::InvalidFilter),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn search_by_email_attribute<W: AsyncWrite + Unpin>(
|
||||
w: &mut FramedWrite<W, LdapCodec>,
|
||||
mut sr: LdapSearchRequest,
|
||||
msgid: i32,
|
||||
ctrl: Vec<LdapControl>,
|
||||
config: Arc<Config>,
|
||||
) -> Result<(), LdapError> {
|
||||
debug!("unbound search by email attribute {:?}", sr);
|
||||
// This is hardcoded to mail attr with AND filter for stalwart behavior
|
||||
// We also support direct EQUALITY and OR filters just in case
|
||||
let (_requested_username, requested_domain) = match mail_username_domain_from_filter(&sr.filter)
|
||||
{
|
||||
Ok(found) => {
|
||||
info!(
|
||||
"Found domain in search query {} with username {}",
|
||||
found.1, found.0
|
||||
);
|
||||
found
|
||||
}
|
||||
Err(e) => {
|
||||
let resp_msg = bind_operror(msgid, &e);
|
||||
w.send(resp_msg).await.map_err(|err| {
|
||||
error!("Unable to send response: {err}");
|
||||
LdapError::Transport
|
||||
})?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let Some(mapping) = config
|
||||
.mapping
|
||||
.iter()
|
||||
.find(|x| x.from.to_lowercase() == requested_domain)
|
||||
else {
|
||||
debug!("No mapping found for domain {requested_domain}");
|
||||
// TODO: we should probably send an answer to the client here
|
||||
return Err(LdapError::InvalidQuery);
|
||||
};
|
||||
|
||||
let mut client = match BasicLdapClient::build(&mapping.backend).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
// TODO: error code backend unavailable and more detailed message (timeout/connection refused)
|
||||
error!("A client build error has occurred: {e:?}");
|
||||
let resp_msg = bind_operror(msgid, &"unable to bind");
|
||||
w.send(resp_msg).await.map_err(|err| {
|
||||
error!("Unable to send response: {err}");
|
||||
LdapError::Transport
|
||||
})?;
|
||||
// Always bail.
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let lbr = LdapBindRequest {
|
||||
dn: mapping.user.to_string(),
|
||||
cred: LdapBindCred::Simple(mapping.password.to_string()),
|
||||
};
|
||||
|
||||
match client.bind(lbr, ctrl.clone()).await {
|
||||
Ok((bind_resp, _ctrl)) => {
|
||||
// Almost there, lets check the bind result.
|
||||
let valid = bind_resp.res.code == LdapResultCode::Success;
|
||||
|
||||
if !valid {
|
||||
// TODO: client error
|
||||
error!("Invalid backend credentials!");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
// TODO: client error
|
||||
warn!("Failed to bind to the backend: {e:?}");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// Now edit the search base DN to match what the backend server expects
|
||||
let mut dn = Dn::from_dn_str(&sr.base)?;
|
||||
// Maybe there was no hostname to begin with, force to add it
|
||||
dn.set_hostname(&mapping.to, true);
|
||||
let dn = dn.to_dn_string();
|
||||
debug!("Rewrote anonymous search DN from {} to {}", sr.base, dn);
|
||||
sr.base = dn;
|
||||
|
||||
// Query the backend and rewrote matching dns with the mapping vhost
|
||||
let (entries, result, ctrl) = match client.search(sr, ctrl).await {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
error!("A client search error has occurred: {e:?}");
|
||||
let resp_msg = bind_operror(msgid, &"unable to search");
|
||||
w.send(resp_msg).await.map_err(|err| {
|
||||
error!("Unable to send response: {err}");
|
||||
LdapError::Transport
|
||||
})?;
|
||||
|
||||
// Error sent, return with no state change.
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
for (mut entry, ctrl) in entries {
|
||||
trace!("Search result from backend: {:?}", entry);
|
||||
// TODO: maybe don't fail the whole request here???
|
||||
let mut dn = Dn::from_dn_str(&entry.dn)?;
|
||||
dn.set_hostname(&mapping.from, false);
|
||||
entry.dn = dn.to_dn_string();
|
||||
debug!("Search result: {:?}", entry);
|
||||
|
||||
w.send(LdapMsg {
|
||||
msgid,
|
||||
op: LdapOp::SearchResultEntry(entry),
|
||||
ctrl,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Unable to send response: {err}");
|
||||
LdapError::Transport
|
||||
})?;
|
||||
}
|
||||
|
||||
debug!("Search result done: {:?}", result);
|
||||
w.send(LdapMsg {
|
||||
msgid,
|
||||
op: LdapOp::SearchResultDone(result),
|
||||
ctrl,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Unable to send response: {err}");
|
||||
LdapError::Transport
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn search<W: AsyncWrite + Unpin>(
|
||||
w: &mut FramedWrite<W, LdapCodec>,
|
||||
search_request: SearchRequest<'_>,
|
||||
) -> Result<(), LdapError> {
|
||||
debug!("{:?}", search_request.sr);
|
||||
let SearchRequest {
|
||||
sr,
|
||||
msgid,
|
||||
|
|
@ -253,7 +30,7 @@ pub async fn search<W: AsyncWrite + Unpin>(
|
|||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
error!("A client search error has occurred: {e:?}");
|
||||
let resp_msg = bind_operror(msgid, &"unable to search");
|
||||
let resp_msg = bind_operror(msgid, "unable to search");
|
||||
w.send(resp_msg).await.map_err(|err| {
|
||||
error!("Unable to send response: {err}");
|
||||
LdapError::Transport
|
||||
|
|
@ -265,7 +42,6 @@ pub async fn search<W: AsyncWrite + Unpin>(
|
|||
};
|
||||
|
||||
for (entry, ctrl) in entries {
|
||||
debug!("Search result: {:?}", entry);
|
||||
w.send(LdapMsg {
|
||||
msgid,
|
||||
op: LdapOp::SearchResultEntry(entry),
|
||||
|
|
@ -278,7 +54,6 @@ pub async fn search<W: AsyncWrite + Unpin>(
|
|||
})?;
|
||||
}
|
||||
|
||||
debug!("Search result done: {:?}", result);
|
||||
w.send(LdapMsg {
|
||||
msgid,
|
||||
op: LdapOp::SearchResultDone(result),
|
||||
|
|
|
|||
Loading…
Reference in a new issue