/*************************************************
*     Exim - an Internet mail transport agent    *
*************************************************/

/* Copyright (c) University of Cambridge 1995 - 1999 */
/* See the file NOTICE for conditions of use and distribution. */

/* Many thanks to Stuart Lynne for contributing the code for this driver.

"The driver should link against Netscape. It's tested with the Netscape LDAP
SDK that is available in source. I don't know if it will work (but it should)
with their binary LDAP SDK libraries."

TODO
   1. We should support the bindname extension
   2. Should ldapm simply be an ldap url extension?
*/

#include "../exim.h"
#include "ldap.h"


/* Compile these functions only if LOOKUP_DBM is defined. However, some
compilers don't like compiling empty modules, so keep them happy with a dummy
when skipping the rest. Make it reference itself to stop picky compilers
complaining that it is unused, and put in a dummy argument to stop even pickier
compilers complaining about infinite loops. */

#ifndef LOOKUP_LDAP
static void dummy(int x) { dummy(x-1); }
#else


/* Include LDAP headers */

#include <lber.h>
#include <ldap.h>

/* Structure and anchor for caching connections. */

typedef struct ldap_connection {
  LDAPURLDesc  *ludp;
  LDAP         *ld;
  struct ldap_connection *next;
} LDAP_CONNECTION;

static LDAP_CONNECTION *ldap_connections = NULL;



/*************************************************
*         Internal search function               *
*************************************************/

/* This is the function that actually does the work. It is called from
both eldap_find() and eldapm_find(), with a difference in the "single"
argument.

Arguments:
  handle     the handle passed to xxx_find() - not in fact used
  ldap_url   the URL to be looked up
  single     TRUE if a maximum of 1 result value is permitted; FALSE otherwise
  res        set to point at the result
  errmsg     ser to point a message if result is not OK

Returns:     OK or FAIL or DEFER
             FAIL is given only if a lookup was performed successfully, but
             returned no data.
*/

static int
perform_ldap_search(void *handle, char *ldap_url, BOOL single, char **res,
  char **errmsg)
{
LDAP         *ld;
LDAPURLDesc  *ludp;
LDAPMessage  *result;
LDAPMessage  *e;
BerElement   *ber;
LDAP_CONNECTION *lcp;

char  *attr;
char  *data = NULL;
char  **attrp;
char  **values;
char  **firstval;
int   attr_count = 0;
int   rc;
int   ptr = 0;
int   size = 0;
BOOL  add_newline = FALSE;

DEBUG(9) debug_printf("perform_ldap_search: ldap URL =\"%s\"\n", ldap_url);

/* Check if LDAP thinks the URL is a valid LDAP URL */

if (!ldap_is_ldap_url(ldap_url))
  {
  *errmsg = string_sprintf("ldap_is_ldap_url: not an LDAP url \"%s\"\n",
    ldap_url);
  DEBUG(9) debug_printf("%s\n", *errmsg);
  return DEFER;
  }

/* Parse the URL to get host and port, too bad there isn't an
ldap_url_[init|open] */

if ((rc = ldap_url_parse(ldap_url, &ludp)) != 0)
  {
  *errmsg = string_sprintf("ldap_url_parse: (error %d) parsing \"%s\"\n", rc,
    ldap_url);
  DEBUG(9) debug_printf("%s\n", *errmsg);
  return DEFER;
  }

/* Count the attributes; we need this later to tell us how to format results */

for (attrp = ludp->lud_attrs; attrp != NULL && *attrp != NULL; attrp++)
  attr_count++;

/* See if we can find a cached connection to this host. The host name pointer
is set to NULL if no host was given, rather than to the empty string.. */

for (lcp = ldap_connections; lcp != NULL; lcp = lcp->next)
  {
  if (lcp->ludp->lud_port != ludp->lud_port) continue;
  if (lcp->ludp->lud_host == NULL)
    {
    if (ludp->lud_host == NULL) break;
    }
  else
    {
    if (ludp->lud_host != NULL &&
      strcmpic(lcp->ludp->lud_host, ludp->lud_host) == 0) break;
    }
  }

/* If no cached connection found, we must open a connection to the server. */

if (lcp == NULL)
  {
  ld = ldap_open(ludp->lud_host, (ludp->lud_port)?(ludp->lud_port):(LDAP_PORT));
  if (ld == NULL)
    {
    ldap_free_urldesc(ludp);
    *errmsg = string_sprintf("ldap_open: failed to initialise LDAP search: %s",
      strerror(errno));
    DEBUG(9) debug_printf("%s\n", *errmsg);
    return DEFER;
    }

  DEBUG(9) debug_printf("Binding directory server for %s\n", ldap_url);
  lcp = store_malloc(sizeof(LDAP_CONNECTION));
  lcp->ludp = ludp;
  lcp->ld = ld;
  lcp->next = ldap_connections;
  ldap_connections = lcp;
  }

/* Found cached connection; tidy up a bit */

else
  {
  DEBUG(9) debug_printf("Re-using cached connection to directory server "
    "for %s\n", ldap_url);
  ldap_free_urldesc(ludp);
  }

/* Do an anonymous bind */

if ((rc = ldap_bind_s(lcp->ld, NULL, NULL, LDAP_AUTH_SIMPLE)) != LDAP_SUCCESS)
  {
  *errmsg = string_sprintf("ldap_bind_s: failed to bind the LDAP connection: "
    "%s", ldap_err2string(rc));
  DEBUG(9) debug_printf("%s\n", *errmsg);
  return DEFER;
  }

/* Use the LDAP URL search function; no need to do our own parsing. */

if (ldap_url_search(lcp->ld, ldap_url, 0) == -1)
  {
  char *matched, *error;
  #if LDAP_OPT_SIZELIMIT        /* Netscape SDK */
  (void)ldap_get_lderrno(lcp->ld, &matched, &error);
  #else
  matched = ld->ld_matched;
  error = lcp->ld->ld_error;
  #endif
  *errmsg = string_sprintf("ldap_url_search failed: %s (%s)", error, matched);
  DEBUG(9) debug_printf("open: %s\n", *errmsg);
  return DEFER;
  }

/* Wait for results - this will timeout if no results available in reasonable
time. */

rc = ldap_result(lcp->ld, LDAP_RES_ANY, 1, NULL, &result);

if (rc == -1)
  {
  char *matched, *error;
  #if LDAP_OPT_SIZELIMIT        /* Netscape SDK */
  (void)ldap_get_lderrno(lcp->ld, &matched, &error);
  #else
  matched = lcp->ld->ld_matched;
  error = lcp->ld->ld_error;
  #endif
  *errmsg = string_sprintf("ldap_url_search failed: %s (%s)", error, matched);
  DEBUG(9) debug_printf("%s\n", *errmsg);
  return DEFER;
  }

if (rc == 0)
  {
  *errmsg = string_sprintf("ldap_result: Timed out");
  DEBUG(9) debug_printf("%s\n", *errmsg);
  return (DEFER);
  }

/* Check if we have too many entries */

rc = ldap_count_entries(lcp->ld, result);
if (single && rc > 1)
  {
  *errmsg = string_sprintf("ldap_count_entries: too many (%d) results "
    "(filter not specific enough)", rc);
  DEBUG(9) debug_printf("%s\n", *errmsg);
  return DEFER;
  }

/* Check if we have too few (zero) entries - this is the only place we return
FAIL */

if (rc < 1)
  {
  *errmsg = string_sprintf("ldap_count_entries: no results");
  DEBUG(9) debug_printf("%s\n", *errmsg);
  return (FAIL);
  }

/* Initialize chunk of store in which to return the answer. */

size = 100;
ptr = 0;
data = store_get(size);

/* Loop through returned entries; we have already checked above for zero
entries. */

for(e = ldap_first_entry(lcp->ld, result);
    e != NULLMSG;
    e = ldap_next_entry(lcp->ld, e))
  {
  BOOL add_comma = FALSE;

  DEBUG(9) debug_printf("perform_ldap_search: LDAP entry loop\n");

  /* Results for multiple entries values are separated by newlines. */

  if (add_newline)
    data = string_cat(data, &size, &ptr, "\n", 1);
  else
    add_newline = TRUE;

  /* Loop through the entry, grabbing attribute values. Multiple attribute
  values are separated by commas. */

  for (attr = ldap_first_attribute(lcp->ld, e, &ber);
       attr != NULL;
       attr = ldap_next_attribute(lcp->ld, e, ber))
    {
    if (attr[0])
      {
      /* Get array of values for this attribute. */

      if ((firstval = values = ldap_get_values(lcp->ld, e, attr)) != NULL)
        {
        while (*values)
          {
          DEBUG(9) debug_printf("perform_ldap_search: LDAP attr loop %s:%s\n",
            attr, *values);

          if (add_comma)
            data = string_cat(data, &size, &ptr, ", ", 2);
          else
            add_comma = TRUE;

          if (attr_count > 1)
            {
            data = string_cat(data, &size, &ptr, attr, strlen(attr));
            data = string_cat(data, &size, &ptr, "=", 1);
            }

          data = string_cat(data, &size, &ptr, *values, strlen(*values));
          data[ptr] = '\0';
          values++;
          }
        ldap_value_free(firstval);
        }
      }

#ifdef HAVE_NETSCAPE_LDAP_SDK

    /* Netscape LDAP's attr's are dynamically allocated and need to be freed.
    UMich LDAP stores them in static storage and does not require this. */

    ldap_memfree(attr);
#endif
    }

#if HAVE_NETSCAPE_LDAP_SDK_AND_CHECK_IF_BER_FREE_IS_REALLY_REQUIRED

  /* TODO UMich ldap_next_attribute() calls ber_free().
  Need to check if Netscape LDAP SDK does or does not.... */

  if (ber != NULL) ber_free(ber, 0);
#endif
  }

DEBUG(9) debug_printf("perform_ldap_search: LDAP exit entry loop data: %s\n",
    (data)?(data):(""));

*res = data;
return OK;
}



/*************************************************
*              Open entry point                  *
*************************************************/

/* See local README for interface description. */

void *
eldap_open(char *filename, char **errmsg)
{
return (void *)(1);    /* Just return something non-null */
}

void *
eldapm_open(char *filename, char **errmsg)
{
return (void *)(1);    /* Just return something non-null */
}



/*************************************************
*               Find entry point                 *
*************************************************/

/* See local README for interface description. The two different searches are
handled by a common function, with a flag to differentiate between them. */

int
eldap_find(void *handle, char *filename, char *ldap_url, int length,
  char **result, char **errmsg)
{
return(perform_ldap_search(handle, ldap_url, TRUE, result, errmsg));
}

int
eldapm_find(void *handle, char *filename, char *ldap_url, int length,
  char **result, char **errmsg)
{
return(perform_ldap_search(handle, ldap_url, FALSE, result, errmsg));
}



/*************************************************
*               Tidy entry point                 *
*************************************************/

/* See local README for interface description. The two entry points
just call the same function, which unbinds all cached connections. */

static void
tidy(void)
{
LDAP *ld;
LDAPURLDesc *ludp;
LDAP_CONNECTION *lcp = NULL;

while ((lcp = ldap_connections) != NULL)
  {
  DEBUG(9) debug_printf("ldap_unbind connection to %s:%d\n",
    (lcp->ludp->lud_host == NULL)? "" : lcp->ludp->lud_host,
    lcp->ludp->lud_port);
  ldap_free_urldesc(lcp->ludp);
  ldap_unbind(lcp->ld);
  ldap_connections = lcp->next;
  store_free(lcp);
  }
}

void
eldap_tidy(void)
{
DEBUG(9) debug_printf("eldap_tidy called\n");
tidy();
}

void
eldapm_tidy(void)
{
DEBUG(9) debug_printf("eldapm_tidy called\n");
tidy();
}

#endif  /* LOOKUP_LDAP */

/* End of lookups/ldap.c */
