[Exim] Strangeness in LDAP API

Startseite
Nachricht löschen
Nachricht beantworten
Autor: Brian Candler
Datum:  
To: exim-users
Betreff: [Exim] Strangeness in LDAP API
--
I have been investigating a problem with LDAP: specifically, that if the
LDAP server returns an error such as LDAP_BUSY or LDAP_UNAVAILABLE, that the
router does not _defer_ the message, but treats it as an empty search
result. That's rather important - it means that rather than deferring and
trying again later, exim permanently bounces the mail as 'no such user' :-(

In the following examples, I am using OpenLDAP as my LDAP server (I have
tried both 2.0.25 and 1.2.12 with the same results)

The problem is very easy to replicate. For example, try a search query where
the baseDN does not exist; the LDAP server should return LDAP_NO_SUCH_OBJECT

# exim -d -be '${lookup ldap {ldap://hostname/o=JUNK}}'

The debug output from Exim says:

  Start search
  search ended by ldap_result yielding 101
  LDAP search: no results              ^^^
  lookup failed


Looking at debugging at the slapd end, I see:

  send_ldap_result: conn=5 op=1 p=2
  send_ldap_response: msgid=2 tag=101 err=32
                                  ^^^
Now, tag=101 is LDAP_RES_BIND (which is strange), and err=32 is
LDAP_NO_SUCH_OBJECT which is the error we expected.


As far as exim's code is concerned (src/lookups/ldap.c), an error has only
occured if ldap_result returns -1. A result of 101 (LDAP_RES_BIND) is
treated as the end of the attributes. In fact, exim treats
LDAP_RES_SEARCH_ENTRY as a returned entry, 0 as timeout, and any other
value as simply the end of the attributes.

So, is this a bug in the OpenLDAP client library (should it return -1 in
this case?) or in exim (should it be calling a different function to
determine if the search encountered an error?) It certainly seems strange to
be returning LDAP_RES_BIND!

Notice that failures to _bind_ are handled correctly, it's only _searches_
that have this problem.

$ exim -d -be '${lookup ldap {user=JUNK pass=JUNK ldap://hostname/o=JUNK}}'

Binding with user=JUNK password=JUNK
failed to bind the LDAP connection to server hostname:389 - LDAP error 49: Invalid credentials
lookup deferred: failed to bind the LDAP connection to server hostname:389 - LDAP error 49: Invalid credentials
Failed: lookup of "user=JUNK pass=JUNK ldap://hostname/o=JUNK" gave DEFER: failed to bind the LDAP connection to server hostname:389 - LDAP error 49: Invalid credentials

A 'defer' is the right thing, and I think that's what we should be seeing
when we we do a _search_ which suffers a failure (where 'matching no
entries' is not a failure)

Now, the 'ldapsearch' client provided as part of OpenLDAP seems to detect
this error condition properly:

$ ldapsearch -h x.x.x.x -b "o=JUNK"
version: 2

#
# filter: (objectclass=*)
# requesting: ALL
#

# search result
search: 2
result: 32 No such object

# numResponses: 1

$ echo $?
32

So it's possible that the problem is with exim's use of the API, rather than
the library itself. Simplified, what ldapsearch does is:

        while ((rc = ldap_result( ... ) > 0 ) {
                ... read an entry
                switch( ldap_msgtype( msg ) ) {
                case LDAP_RES_SEARCH_ENTRY:
                        ... print the entry
                case LDAP_RES_SEARCH_RESULT:
                        rc = print_result( ld, msg, 1 );
                        goto done;
        }


        if ( rc == -1 ) {
                ldap_perror( ld, "ldap_result" );
                return( rc );
        }



print_result() {

        rc = ldap_parse_result( ld, result,
                &err, &matcheddn, &text, &refs, &ctrls, 0 );


        if( rc != LDAP_SUCCESS ) {
                ldap_perror(ld, "ldap_parse_result");
                exit( EXIT_FAILURE );
        }


        printf( "result: %d %s\n", err, ldap_err2string(err) );
}


This seems to imply that rc=-1 means "ldap_result failed internally or
couldn't provide you with a message" rather than "the server has reported a
problem with your search", which is very different.

Of course, this could all be sorted if there was an accurate definition of
the LDAP API. Having looked at RFC1823 and a later draft from the OpenLDAP
source, I eventually found that what I needed was really in the spec for the
LDAP protocol itself:

There may be zero or more
responses containing SearchResultEntry, one for each entry found
during the search. There may also be zero or more responses
containing SearchResultReference, one for each area not explored by
this server during the search. The SearchResultEntry and
SearchResultReference PDUs may come in any order. Following all the
SearchResultReference responses and all SearchResultEntry responses
to be returned by the server, the server will return a response
containing the SearchResultDone, which contains an indication of
success, or detailing any errors that have occurred.

Mapping this to the API, it means that in the simple case the messages
returned by a search request are 0 or more instances of
LDAP_RES_SEARCH_ENTRY, followed by one instance of LDAP_RES_SEARCH_RESULT.
(That's not LDAP_RES_BIND! I'll raise that with the OpenLDAP team, but even
if fixed it actually wouldn't make any difference here)

So I have produced a proposed patch to exim-4.10 (attached) which I think
does The Right Thing[TM]. It does appear to work in practice, giving a 451
error in the case of LDAP search failures, although I have not done
extensive testing. The patch also includes an extra ldap_msgfree for the
final message, which I think was missing before, unless I have overlooked
something.

Any suggestions or enlightenments from those who have a deeper understanding
of LDAP or the API than me are of course gratefully received...

Regards,

Brian.
--
--- exim-4.10/src/lookups/ldap.c.orig    Thu Aug 22 16:37:43 2002
+++ exim-4.10/src/lookups/ldap.c    Thu Aug 22 17:08:56 2002
@@ -124,7 +124,7 @@
   int sizelimit, int timelimit)
 {
 LDAPURLDesc  *ludp = NULL;
-LDAPMessage  *result;
+LDAPMessage  *result = NULL;
 BerElement   *ber;
 LDAP_CONNECTION *lcp;


@@ -490,6 +490,7 @@
/* Free the result */

   ldap_msgfree(result);
+  result=NULL;
   }            /* End "while" loop for multiple results */


/* Terminate the dynamic string that we have built. */
@@ -534,6 +535,28 @@
goto RETURN_ERROR;
}

+if (rc != LDAP_RES_SEARCH_RESULT && rc != LDAP_RES_BIND)
+  {  /* LDAP_RES_BIND is returned by broken version of OpenLDAP */
+
+  *errmsg = string_sprintf("ldap_result returned unexpected result %d", rc);
+  goto RETURN_ERROR;
+  }
+
+/* Extract the status from the LDAP_RES_SEARCH_RESULT message */
+
+if (ldap_parse_result(lcp->ld, result, &rc, NULL, &attr, NULL, NULL, 0) < 0)
+  {
+  *errmsg = string_sprintf("ldap_parse_result failed");
+  goto RETURN_ERROR;
+  }
+
+if (rc != LDAP_SUCCESS)
+  {
+  *errmsg = string_sprintf("LDAP search failed - LDAP error %d: %s",
+    rc, ldap_err2string(rc));
+  goto RETURN_ERROR;
+  }
+
 /* Check if we have too many results */


if (search_type != SEARCH_LDAP_MULTIPLE && rescount > 1)
@@ -568,6 +591,7 @@
*res = data;

RETURN_OK:
+if (result != NULL) ldap_msgfree(result);
ldap_free_urldesc(ludp);
return OK;

@@ -580,6 +604,7 @@
DEBUG(D_lookup) debug_printf("%s\n", *errmsg);

RETURN_ERROR_NOMSG:
+if (result != NULL) ldap_msgfree(result);
if (ludp != NULL) ldap_free_urldesc(ludp);
return error_yield;
}
--