/* 
   Copyright © 1998, 1999 Enbridge Pipelines Inc. 
   Copyright © 1999-2001 Dave Carrigan
   All rights reserved.

   This module is free software; you can redistribute it and/or modify
   it under the same terms as Apache itself. This module is
   distributed in the hope that it will be useful, but WITHOUT ANY
   WARRANTY; without even the implied warranty of MERCHANTABILITY or
   FITNESS FOR A PARTICULAR PURPOSE. The copyright holder of this
   module can not be held liable for any general, special, incidental
   or consequential damages arising out of the use of the module.

   $Id: auth_ldap.c,v 1.22 2001/07/04 14:38:03 dave Exp $
*/

const char *auth_ldap_version = "1.6.0";

#define __AUTH_LDAP_C__
#include "auth_ldap.h"

module MODULE_VAR_EXPORT auth_ldap_module;

/* Backwards compatibility for Apache 1.2 */
#if APACHE_RELEASE < 1030000
int ldap_loglevel = 4; 
void ap_log_error(const char *file, int line, int level,
		  const server_rec *s, const char *fmt, ...)
{
  va_list args;
  if (level >= ldap_loglevel)
    return;
  fprintf(stderr, "(%s:%d) ", file, line);
  fflush(stderr);
  va_start(args, fmt);
  vfprintf(stderr, fmt, args);
  fprintf(stderr, "\n");
  fflush(stderr);
  va_end(args);
}
#endif /* if APACHE_RELEASE */

/* 
 * Compatibility for LDAPv2 libraries. Differences between LDAPv2 and 
 * LDAPv3, as they affect this module
 * 
 *  LDAPv3 uses ldap_search_ext_s; LDAPv2 uses only basic ldap_search_s
 *
 *  LDAPv3 uses ldap_memfree; LDAPv2 just uses free().
 *
 * In this section, we just implement the LDAPv3 SDK functions that are 
 * missing in LDAPv2. 
 * 
 */
#if LDAP_SDK_VERSION == 2

/*
 * LDAPv2 doesn't support extended search. Since auth_ldap doesn't use
 * it anyway, we just translate the extended search into a normal search.
 */
int
ldap_search_ext_s(LDAP *ldap, char *base, int scope, char *filter,
		  char **attrs, int attrsonly, void *servertrls, void *clientctrls,
		  void *timeout, int sizelimit, LDAPMessage **res)
{
  return ldap_search_s(ldap, base, scope, filter, attrs, attrsonly, res);
}

void
ldap_memfree(void *p)
{
  free(p);
}

#endif /* if LDAP_SDK_VERSION */

/* This function calls log_reason in 1.2, while it calls ap_log_error in 1.3+ */
void auth_ldap_log_reason(request_rec *r, const char *fmt, ...)
{
  va_list args;
  char buf[MAX_STRING_LEN];

  va_start(args, fmt);
  ap_vsnprintf(buf, sizeof(buf), fmt, args);
  va_end(args);

#if APACHE_RELEASE < 1030000
  log_reason(buf, r->uri, r);
#else
  ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, r, buf);
#endif
}

/*
 * Closes an LDAP connection by unbinding. Sets the boundas value for the
 * http connection config record and clears the bound dn string in the
 * global connection record. The next time connect_to_server is called, the
 * connection will be recreated.
 *
 * If the log parameter is set, adds a debug entry to the log that the
 * server was down and it's reconnecting.
 *
 * The mutex for the LDAPConnection should already be acquired.
 */
void
auth_ldap_free_connection(request_rec *r, int log)
{
  auth_ldap_config_rec *sec =
    (auth_ldap_config_rec *)ap_get_module_config(r->per_dir_config, &auth_ldap_module);

  if (log)
    ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		  "{%d} Server is down; reconnecting and starting over", (int)getpid());

  if (sec->ldc->ldap) {
    ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		  "{%d} Freeing connection to ldap server(s) `%s'", 
		  (int)getpid(), sec->host);
    ldap_unbind_s(sec->ldc->ldap);
    sec->ldc->ldap = NULL;
    sec->ldc->boundas = bind_none;
    if (sec->ldc->bounddn) {
      free(sec->ldc->bounddn);
      sec->ldc->bounddn = NULL;
    }
  }
}

/*
 * Connect to the LDAP server and binds. Does not connect if already
 * connected (i.e.sec->ldc->ldap is non-NULL.) Does not bind if already bound.
 * Returns 1 on success; 0 on failure
 *
 * The mutex for the LDAPConnection should already be acquired.
 */
int
auth_ldap_connect_to_server(request_rec *r)
{
  int result;
  auth_ldap_config_rec *sec;
  auth_ldap_server_conf *conf;
  int failures = 0;


  ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		"{%d} Entering auth_ldap_connect_to_server", (int)getpid());

  conf = (auth_ldap_server_conf *)ap_get_module_config(r->server->module_config,
						       &auth_ldap_module);
  sec = (auth_ldap_config_rec *)ap_get_module_config(r->per_dir_config, 
						     &auth_ldap_module);

 start_over:
  if (failures++ > 10) {
    auth_ldap_log_reason(r, "Too many failures connecting to LDAP server");
    return 0;
  }

  if (!sec->ldc->ldap) {
    sec->ldc->boundas = bind_none;
    if (sec->ldc->bounddn) {
      free(sec->ldc->bounddn);
      sec->ldc->bounddn = NULL;
    }
    ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		  "{%d} Opening connection to ldap server(s) `%s'", 
		  (int)getpid(), sec->host);
    ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		  "{%d} LDAP OP: init", (int)getpid());
    
    if ((sec->ldc->ldap = ldap_init(sec->host, sec->port)) == NULL) {
      extern int errno;
      auth_ldap_log_reason(r, "Could not connect to LDAP server: %s", strerror(errno));
      return 0;
    }

    /* Set the alias dereferencing option */
#if LDAP_SDK_VERSION == 2
    sec->ldc->ldap->ld_deref = sec->deref;
#else
    result = ldap_set_option(sec->ldc->ldap, LDAP_OPT_DEREF, &(sec->deref));
    if (result != LDAP_SUCCESS) {
      ap_log_rerror(APLOG_MARK, APLOG_WARNING|APLOG_NOERRNO, r,
		    "Setting LDAP dereference option failed: %s", 
		    ldap_err2string(result));
    }
#endif /* LDAP_SDK_VERSION */

#ifdef WITH_SSL
    if (sec->secure) {
      ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		    "{%d} Initializing SSL for this connection", 
		    (int)getpid());
      if (!conf->have_certdb) {
	auth_ldap_log_reason(r, 
			     "Secure LDAP requested, but no "
			     "AuthLDAPCertDBPath directive in config");
	return 0;
      } else {
	result = ldapssl_install_routines(sec->ldc->ldap);
	if (result != LDAP_SUCCESS) {
	  auth_ldap_log_reason(r, "SSL initialization failed: %s", 
			       ldap_err2string(result));
	  return 0;
	}
	result = ldap_set_option(sec->ldc->ldap, LDAP_OPT_SSL, LDAP_OPT_ON);
	if (result != LDAP_SUCCESS) {
	  auth_ldap_log_reason(r, "SSL option failed: %s", 
			       ldap_err2string(result));
	  return 0;
	}
      }
    } 
#endif

#ifdef HAVE_TLS
    if (sec->starttls) {
      int version = LDAP_VERSION3;
      /* 
	 In auth_ldap_find_connection, we compare ldc->withtls to
	 sec->starttls to see if we have a cache match. On the off
	 chance that apache's config processing rotines set starttls to
	 some other true value besides 1, we set it to 1 here to ensure
	 that the comparison succeeds.
      */

      /* Also we have to set the connection to use protocol version 3,
	 since we're using TLS. */
      if (result = ldap_set_option(sec->ldc->ldap, LDAP_OPT_PROTOCOL_VERSION,
				   &version) != LDAP_SUCCESS) {
	ap_log_rerror(APLOG_MARK, APLOG_WARNING|APLOG_NOERRNO, r,
		      "Setting LDAP version option failed: %s", 
		      ldap_err2string(result));
      }

      sec->starttls = 1;
      ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		    "{%d} Starting TLS for this connection", 
		    (int)getpid());

      result = ldap_start_tls_s( sec->ldc->ldap, NULL, NULL );
      sec->ldc->withtls = 1;
      if (result != LDAP_SUCCESS) {
	auth_ldap_log_reason(r, "Start TLS failed: %s", 
                             ldap_err2string(result));
	return 0;
      }
    } else {
      sec->ldc->withtls = 0;
    }
#endif /* HAVE_TLS */
  }

  /* 
   * At this point the LDAP connection is guaranteed alive. If boundas says
   * that we're bound as system, we can just return.
   */
  if (sec->ldc->boundas == bind_system) {
    return 1;
  }

  ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		"{%d} Binding to server `%s' as %s/%s", (int)getpid(),
		sec->host, 
		sec->binddn? sec->binddn : "(null)",
		sec->bindpw? sec->bindpw : "(null)");
  /* 
   * Now bind with the username/password provided by the
   * configuration. It will be an anonymous bind if no u/p was
   * provided. 
   */
  ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		"{%d} LDAP OP: simple bind", (int)getpid());
  if ((result = ldap_simple_bind_s(sec->ldc->ldap, sec->binddn, sec->bindpw))
      == LDAP_SERVER_DOWN) {
    auth_ldap_free_connection(r, 1);
    goto start_over;
  }

  if (result != LDAP_SUCCESS) {
    auth_ldap_free_connection(r, 0);
    auth_ldap_log_reason(r, "Could not bind to LDAP server `%s' as %s: %s", 
			 sec->host, 
			 sec->binddn? sec->binddn : "(null)", 
			 ldap_err2string(result));
    return 0;
  } 

  sec->ldc->bounddn = sec->binddn? strdup(sec->binddn) : NULL;
  sec->ldc->boundas = bind_system;

  return 1;
}

/*
 * Find an existing ldap connection struct that matches the
 * host. Create a new one if none are found.
 * Will set the ldc field of the sec struct.
 */
void
auth_ldap_find_connection(auth_ldap_config_rec *sec, request_rec *r)
{
  struct LDAPconnection *l, *p;	/* To traverse the linked list */
  auth_ldap_server_conf *conf;

  ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		"{%d} Entering auth_ldap_find_connection", (int)getpid());

  conf = (auth_ldap_server_conf *)ap_get_module_config(r->server->module_config,
						       &auth_ldap_module);

  GETMUTEX(conf->mtx);

  /* Find if this connection exists in the linked list yet */
  for (l=conf->ldapconnections,p=NULL; l; l=l->next) {
    if (l->port == sec->port
	&& strcmp(l->host, sec->host) == 0
#ifdef HAVE_TLS
	&& l->withtls == sec->starttls
#endif
	)
      break;
    p = l;
  }

  if (l) {
    /* 
     * Existing connection. Now determine if we need to rebind because
     * the last time this connection was used, it bound as a differert
     * dn than this time.
     */
    if ((sec->binddn && !l->bounddn) ||
	(!sec->binddn && l->bounddn) ||
	(sec->binddn && l->bounddn && strcmp(sec->binddn, l->bounddn) != 0))
      l->boundas = bind_none;
    else
      l->boundas = bind_system;
  } else {
    /* 
       Create a new connection entry in the linked list. Note that we
       don't actually establish an LDAP connection yet; that happens
       the first time authentication is requested.

       We allocate these connections out of the real heap, not from a
       pool. This is because the connections should last as long as
       possible.
    */
    l = (struct LDAPconnection *)malloc(sizeof(struct LDAPconnection));
    if (!l) {
      RELMUTEX(conf->mtx);
      return;
    }

    l->ldap = NULL;
    l->host = strdup(sec->host);
    l->port = sec->port;
    l->bounddn = NULL;
    l->next = NULL;
    l->mtx = ap_create_mutex(NULL);
    if (p) {
      p->next = l;
    } else {
      conf->ldapconnections = l;
    }
    l->boundas = bind_none;
  }
  sec->ldc = l;
  RELMUTEX(conf->mtx);
}

/*
  Build the search filter, or at least as much of the search filter that
  will fit in the buffer. We don't worry about the buffer not being able
  to hold the entire filter. If the buffer wasn't big enough to hold the
  filter, ldap_search_s will complain, but the only situation where this
  is likely to happen is if the client sent a really, really long
  username, most likely as part of an attack.

  The search filter consists of the filter provided with the URL,
  combined with a filter made up of the attribute provided with the URL,
  and the actual username passed by the HTTP client. For example, assume
  that the LDAP URL is
  
    ldap://ldap.airius.com/ou=People, o=Airius?uid??(posixid=*)

  Further, assume that the userid passed by the client was `userj'.  The
  search filter will be (&(posixid=*)(uid=userj)).
*/
#define FILTER_LENGTH MAX_STRING_LEN
void
build_filter(char *filtbuf, 
	     request_rec *r, 
	     auth_ldap_config_rec *sec)
{
  char *p, *q, *filtbuf_end;
  /* 
     Create the first part of the filter, which consists of the 
     config-supplied portions.
  */
  ap_snprintf(filtbuf, FILTER_LENGTH, "(&(%s)(%s=", sec->filter, sec->attribute);

  /* 
     Now add the client-supplied username to the filter, ensuring that any
     LDAP filter metachars are escaped.
  */
  filtbuf_end = filtbuf + FILTER_LENGTH - 1;
  for (p = r->connection->user, q=filtbuf + strlen(filtbuf);
       *p && q < filtbuf_end; *q++ = *p++) {
    if (strchr("*()\\", *p) != NULL) {
      *q++ = '\\';
      if (q >= filtbuf_end)
	break;
    }
  }
  *q = '\0';

  /* 
     Append the closing parens of the filter, unless doing so would 
     overrun the buffer.
  */
  if (q + 2 <= filtbuf_end)
    strcat(filtbuf, "))");
}

url_node *
auth_ldap_create_caches(request_rec *r,
			auth_ldap_config_rec *sec,
			auth_ldap_server_conf *conf)
{
  url_node *curl;

  ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		"{%d} inserting `%s' into URL cache", (int)getpid(), sec->url);

  curl = (url_node *)ap_pcalloc(r->pool, sizeof(url_node));
  curl->url = sec->url;
  curl->search_cache = ald_create_cache(conf->search_cache_size,
					auth_ldap_search_node_hash,
					auth_ldap_search_node_compare,
					auth_ldap_search_node_copy,
					auth_ldap_search_node_free);
  curl->compare_cache = ald_create_cache(conf->compare_cache_size,
					 auth_ldap_compare_node_hash,
					 auth_ldap_compare_node_compare,
					 auth_ldap_compare_node_copy,
					 auth_ldap_compare_node_free);
  curl->dn_compare_cache = ald_create_cache(conf->compare_cache_size,
					    auth_ldap_dn_compare_node_hash,
					    auth_ldap_dn_compare_node_compare,
					    auth_ldap_dn_compare_node_copy,
					    auth_ldap_dn_compare_node_free);
  ald_cache_insert(auth_ldap_cache, curl);
  return curl;
}

int
ldap_authenticate_basic_user(request_rec *r)
{
  char filtbuf[FILTER_LENGTH];
  auth_ldap_config_rec *sec =
    (auth_ldap_config_rec *)ap_get_module_config(r->per_dir_config, &auth_ldap_module);
  auth_ldap_server_conf *conf = 
    (auth_ldap_server_conf *)ap_get_module_config(r->server->module_config,
						  &auth_ldap_module);
  const char *sent_pw;
  int result;
  LDAPMessage *res, *entry;
  int count;
  char *dn;
  int failures = 0;
  url_node *curl;		/* Cached URL node */
  url_node curnode;
  search_node *search_nodep;	/* Cached search node */
  search_node the_search_node;
  time_t curtime;

  if (!sec->enabled)
    return DECLINED;

  ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		"{%d} Entering ldap_authenticate_basic_user", (int)getpid());

  /* 
     Basic sanity checks before any LDAP operations even happen.
  */
  if (!sec->have_ldap_url)
    return DECLINED;

  /* There is a good AuthLDAPURL, right? */
  if (sec->ldc == NULL) {
    auth_ldap_find_connection(sec, r);
    if (sec->ldc == NULL) {
      auth_ldap_log_reason(r, "Could not find/create LDAPconnection struct");
      return sec->auth_authoritative? AUTH_REQUIRED : DECLINED;
    }
  }

  ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		"{%d} authenticate: using URL %s", (int)getpid(), sec->url);

  /* Get the password that the client sent */
  if ((result = ap_get_basic_auth_pw(r, &sent_pw))) {
    ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		 "{%d} authenticate: result is %d", (int)getpid(), result);
    return result;
  }


  GETMUTEX(conf->mtx);
  /* Get the cache node for this url */
  curnode.url = sec->url;
  curl = (url_node *)ald_cache_fetch(auth_ldap_cache, &curnode);
  if (curl == NULL) {
    curl = auth_ldap_create_caches(r, sec, conf);
  }

  sec->user = ap_pstrdup(r->pool, r->connection->user);

  the_search_node.username = sec->user;
  search_nodep = ald_cache_fetch(curl->search_cache, &the_search_node);
  if (search_nodep != NULL && search_nodep->bindpw) {
    ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		  "{%d} found entry in search cache for `%s'...", 
		  (int)getpid(), sec->user);
    
    time(&curtime);

    /*
     * Remove this item from the cache if its expired, or if the 
     * sent password doesn't match the storepassword.
     */
    if (curtime - search_nodep->lastbind > conf->search_cache_ttl) {
      ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		    "{%d} ...but entry is too old (%d seconds)", 
		    (int)getpid(), (int)(curtime - search_nodep->lastbind));
      ald_cache_remove(curl->search_cache, search_nodep);
    } else if (strcmp(search_nodep->bindpw, sent_pw) != 0) {
      ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		    "{%d} ...but cached password doesn't match sent password", 
		    (int)getpid());
      ald_cache_remove(curl->search_cache, search_nodep);
    } else {
      ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		    "{%d} ...and entry is valid", (int)getpid());
      sec->dn = ap_pstrdup(r->pool, search_nodep->dn);
      RELMUTEX(conf->mtx);
      return OK;
    }
  }

  ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		"{%d} entry for `%s' is not in the cache", 
		(int)getpid(), sec->user);
  /*	
   * At this point, there is no valid cached search, so lets do the search.
   */

  build_filter(filtbuf, r, sec);

  GETMUTEX(sec->ldc->mtx);
  /*
   * If any LDAP operation fails due to LDAP_SERVER_DOWN, control returns here.
   */
 start_over:
  if (failures++ > 10) {
    auth_ldap_log_reason(r, "Too many failures connecting to LDAP server");
    RELMUTEX(conf->mtx);
    RELMUTEX(sec->ldc->mtx);
    return 0;
  }
  if (!auth_ldap_connect_to_server(r)) {
    RELMUTEX(conf->mtx);
    RELMUTEX(sec->ldc->mtx);
    return sec->auth_authoritative? AUTH_REQUIRED : DECLINED;
  }

  ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		"{%d} Peforming a search (scope=%d) with filter %s", 
		(int)getpid(), sec->scope, filtbuf);

  ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		"{%d} LDAP OP: search", (int)getpid());
  
  if ((result = ldap_search_ext_s(sec->ldc->ldap,
				  sec->basedn, sec->scope, 
				  filtbuf, NULL, 1, 
				  NULL, NULL, NULL, -1, &res)) == LDAP_SERVER_DOWN) {
    ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		  "{%d} Server is down; reconnecting and starting over", (int)getpid());
    auth_ldap_free_connection(r, 1);
    goto start_over;
  }

  if (result != LDAP_SUCCESS) {
    auth_ldap_log_reason(r, "LDAP search for %s failed: LDAP error: %s; URI %s", 
			 filtbuf, ldap_err2string(result), r->uri);
    RELMUTEX(conf->mtx);
    RELMUTEX(sec->ldc->mtx);
    return sec->auth_authoritative? AUTH_REQUIRED : DECLINED;
  }

  /* 
     We should have found exactly one entry; to find a different
     number is an error.
  */
  count = ldap_count_entries(sec->ldc->ldap, res);
  if (count != 1) {
    auth_ldap_log_reason(r, 
			 "Search must return exactly 1 entry; found "
			 "%d entries for search %s: URI %s", 
			 count, filtbuf, r->uri);
    ldap_msgfree(res);
    RELMUTEX(conf->mtx);
    RELMUTEX(sec->ldc->mtx);
    return sec->auth_authoritative? AUTH_REQUIRED: DECLINED;
  }

  entry = ldap_first_entry(sec->ldc->ldap, res);

  /* Grab the dn, copy it into the pool, and free it again */
  dn = ldap_get_dn(sec->ldc->ldap, entry);
  sec->dn = ap_pstrdup(r->pool, dn);
  ldap_memfree(dn);
    
  ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		"{%d} DN returned from search is %s", (int)getpid(), sec->dn);
    
  /* 
     A bind to the server with an empty password always succeeds, so
     we check to ensure that the password is not empty. This implies
     that users who actually do have empty passwords will never be
     able to authenticate with this module. I don't see this as a big
     problem.
  */
  if (strlen(sent_pw) <= 0) {
    auth_ldap_log_reason(r, "AuthLDAP: user %s provided an empty password: %s", 
			 r->connection->user, r->uri);
    ap_note_basic_auth_failure(r);
    ldap_msgfree(res);
    RELMUTEX(conf->mtx);
    RELMUTEX(sec->ldc->mtx);
    return AUTH_REQUIRED;
  }

  ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		"{%d} Validating user `%s' via bind", 
		(int)getpid(), sec->dn);

  /* 
   * Attempt to bind with the retrieved dn and the password. If the bind
   * fails, it means that the password is wrong (the dn obviously
   * exists, since we just retrieved it)
   */
  ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		"{%d} LDAP OP: simple bind", (int)getpid());
  sec->ldc->boundas = bind_user;
  if ((result = 
       ldap_simple_bind_s(sec->ldc->ldap, sec->dn, const_cast(sent_pw))) == 
      LDAP_SERVER_DOWN) {
    ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		  "{%d} Server is down; reconnecting and starting over", (int)getpid());
    auth_ldap_free_connection(r, 1);
    goto start_over;
  }
  
  if (result != LDAP_SUCCESS) {
    auth_ldap_log_reason(r, "User bind as %s failed: LDAP error: %s; URI %s",
			 sec->dn, ldap_err2string(result), r->uri);
    ap_note_basic_auth_failure(r);
    RELMUTEX(conf->mtx);
    RELMUTEX(sec->ldc->mtx);
    return AUTH_REQUIRED;
  }

  ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r, 
		"{%d} authenticate: accepting", (int)getpid());

  ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		"{%d} Adding user `%s' to the cache", (int)getpid(), sec->dn);

  ldap_msgfree(res);

  RELMUTEX(sec->ldc->mtx);
  /* 		
   * Add the new username to the search cache.
   */
  the_search_node.username = sec->user;
  the_search_node.dn = sec->dn;
  the_search_node.bindpw = (char *)sent_pw;
  time(&the_search_node.lastbind);
  
  ald_cache_insert(curl->search_cache, &the_search_node);
  RELMUTEX(conf->mtx);

  return OK;
}

int
ldap_check_auth(request_rec *r)
{
  url_node *curl;		/* Cached URL node */
  url_node curnode;
  auth_ldap_config_rec *sec =
    (auth_ldap_config_rec *)ap_get_module_config(r->per_dir_config, &auth_ldap_module);
  auth_ldap_server_conf *conf = 
    (auth_ldap_server_conf *)ap_get_module_config(r->server->module_config,
						  &auth_ldap_module);

  int m = r->method_number;

  const array_header *reqs_arr = ap_requires(r);
  require_line *reqs = reqs_arr ? (require_line *)reqs_arr->elts : NULL;

  register int x;
  const char *t;
  char *w;
  int method_restricted = 0;

  if (!sec->enabled)
    return DECLINED;

  ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		"{%d} Entering ldap_check_auth", (int)getpid());

 start_over:
  if (!sec->have_ldap_url)
    return DECLINED;

  if (sec->ldc == NULL) {
    auth_ldap_find_connection(sec, r);
    if (sec->ldc == NULL) {
      auth_ldap_log_reason(r, "Could not find/create LDAPconnection struct");
      return sec->auth_authoritative? AUTH_REQUIRED : DECLINED;
    }
  }

  /* 
   * If there are no elements in the group attribute array, the default should be
   * member and uniquemember; populate the array now.
   */

  if (sec->groupattr->nelts == 0) {
    struct groupattr_entry *grp;
    GETMUTEX(conf->mtx);
    grp = ap_push_array(sec->groupattr);
    grp->name = "member";
    grp = ap_push_array(sec->groupattr);
    grp->name = "uniquemember";
    RELMUTEX(conf->mtx);
  }

  if (sec->user_is_dn) 
    r->connection->user = sec->dn;

  if (!reqs_arr) {
    ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		  "{%d} No requirements array", (int)getpid());
    return sec->auth_authoritative? AUTH_REQUIRED : DECLINED;
  }

  GETMUTEX(conf->mtx);
  curnode.url = sec->url;
  curl = ald_cache_fetch(auth_ldap_cache, &curnode);
  if (curl == NULL) {
    curl = auth_ldap_create_caches(r, sec, conf);
  }

  /* Loop through the requirements array until there's no elements
     left, or something causes a return from inside the loop */
  for(x=0; x < reqs_arr->nelts; x++) {
    if (! (reqs[x].method_mask & (1 << m))) continue;

    method_restricted = 1;
	
    t = reqs[x].requirement;
    w = ap_getword(r->pool, &t, ' ');
    
    if (strcmp(w, "valid-user") == 0) {
#ifdef AUTH_LDAP_FRONTPAGE_HACK
      /*
	Valid user will always be true if we authenticated with ldap,
	but when using front page, valid user should only be true if
	he exists in the frontpage password file. This hack will get
	auth_ldap to look up the user in the the pw file to really be
	sure that he's valid. Naturally, it requires mod_auth to be
	compiled in, but if mod_auth wasn't in there, then the need
	for this hack wouldn't exist anyway.
      */
      if (sec->frontpage_hack) {
	  ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r, 
			"{%d} deferring authorization to mod_auth (FP Hack)", 
			(int)getpid());
	  RELMUTEX(conf->mtx);
	  return OK;
      } else {
#endif
	ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r, 
		      "{%d} agreeing to authenticate because user "
		      "is valid-user", (int)getpid());
	RELMUTEX(conf->mtx);
	return OK;
#ifdef AUTH_LDAP_FRONTPAGE_HACK
      }
#endif
    } else if (strcmp(w, "user") == 0) {
      if (sec->dn == NULL || strlen(sec->dn) == 0) {
	ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		      "{%d} The user's DN has not been defined; failing auth", 
		      (int)getpid());
	return sec->auth_authoritative? AUTH_REQUIRED : DECLINED;
      }
      /* 
       * First do a whole-line compare, in case it's something like
       *   require user Babs Jensen
       */
      if (auth_ldap_compare(sec->dn, sec->attribute, t, r, curl->compare_cache)) { 
	ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r, 
		      "{%d} agreeing to authenticate because of "
		      "require user directive", (int)getpid());
	RELMUTEX(conf->mtx);
	return OK;
      }
      /* 
       * Now break apart the line and compare each word on it 
       */
      while (t[0]) {
	w = ap_getword_conf(r->pool, &t);
	if (auth_ldap_compare(sec->dn, sec->attribute, w, r, curl->compare_cache)) {
	  ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r, 
			"{%d} agreeing to authenticate because of"
			"require user directive", (int)getpid());
	  RELMUTEX(conf->mtx);
	  return OK;
	}
      } 
    } else if (strcmp(w, "dn") == 0) {
      if (sec->dn == NULL || strlen(sec->dn) == 0) {
	ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		      "{%d} The user's DN has not been defined; failing auth", 
		      (int)getpid());
	return sec->auth_authoritative? AUTH_REQUIRED : DECLINED;
      }
      if (auth_ldap_comparedn(sec->dn, t, r, curl)) { 
	ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r, 
		      "{%d} agreeing to authenticate because of "
		      "require dn directive", (int)getpid());
	RELMUTEX(conf->mtx);
	return OK;
      }
    } else if (strcmp(w, "group") == 0) {
      struct groupattr_entry *ent = (struct groupattr_entry *) sec->groupattr->elts;
      int i;

      if (sec->group_attrib_is_dn) {
	if (sec->dn == NULL || strlen(sec->dn) == 0) {
	  ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
			"{%d} The user's DN has not been defined; failing auth", 
			(int)getpid());
	  return sec->auth_authoritative? AUTH_REQUIRED : DECLINED;
	}
      } else {
	if (sec->user == NULL || strlen(sec->user) == 0) {
	  /* We weren't called in the authentication phase, so we didn't have a 
	     chance to set the user field. Do so now. */
	  sec->user = ap_pstrdup(r->pool, r->connection->user);
	}
      }

      ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r, 
		    "{%d} testing for group membership in `%s'", 
		    (int)getpid(), t);

      for (i = 0; i < sec->groupattr->nelts; i++) {
	ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r, 
		      "{%d} testing for %s=%s", (int)getpid(),
		      ent[i].name, sec->group_attrib_is_dn? sec->dn : sec->user);
	if (auth_ldap_compare(t, ent[i].name, 
			      sec->group_attrib_is_dn? sec->dn : sec->user, 
			      r, curl->compare_cache)) {
	  ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r, 
			"{%d} agreeing to authenticate because "
			"of group membership (attribute %s)", (int)getpid(), 
			ent[i].name);
	  RELMUTEX(conf->mtx);
	  return OK;
	}
      }
    }
  } 

  RELMUTEX(conf->mtx);

  if (!method_restricted) {
    ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r, 
		  "{%d} agreeing to authenticate because non-restricted", 
		  (int)getpid());
    return OK;
  }

  if (!sec->auth_authoritative) {
    ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r, 
		  "{%d} declining to authenticate", (int)getpid());
    return DECLINED;
  }

  ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r, 
		"{%d} denying authentication", (int)getpid());
  ap_note_basic_auth_failure (r);
  return AUTH_REQUIRED;
}

command_rec auth_ldap_cmds[] = {
  {"AuthLDAPURL", parse_auth_ldap_url, NULL, OR_AUTHCFG, RAW_ARGS, 
   "URL to define LDAP connection. This should be an RFC 2255 complaint\n"
   "URL of the form ldap://host[:port]/basedn[?attrib[?scope[?filter]]].\n"
   "<ul>\n"
   "<li>Host is the name of the LDAP server. Use a space separated list of hosts \n"
   "to specify redundant servers.\n"
   "<li>Port is optional, and specifies the port to connect to.\n"
   "<li>basedn specifies the base DN to start searches from\n"
   "<li>Attrib specifies what attribute to search for in the directory. If not "
   "provided, it defaults to <b>uid</b>.\n"
   "<li>Scope is the scope of the search, and can be either <b>sub</b> or "
   "<b>one</b>. If not provided, the default is <b>sub</b>.\n"
   "<li>Filter is a filter to use in the search. If not provided, "
   "defaults to <b>(objectClass=*)</b>.\n"
   "</ul>\n"
   "Searches are performed using the attribute and the filter combined. "
   "For example, assume that the\n"
   "LDAP URL is <b>ldap://ldap.airius.com/ou=People, o=Airius?uid?sub?(posixid=*)</b>. "
   "Searches will\n"
   "be done using the filter <b>(&((posixid=*))(uid=<i>username</i>))</b>, "
   "where <i>username</i>\n"
   "is the user name passed by the HTTP client. The search will be a subtree "
   "search on the branch <b>ou=People, o=Airius</b>."
  },

  {"AuthLDAPBindDN", ap_set_string_slot,
   (void *)XtOffsetOf(auth_ldap_config_rec, binddn), OR_AUTHCFG, TAKE1, 
   "DN to use to bind to LDAP server. If not provided, will do an anonymous bind."},

  {"AuthLDAPBindPassword", ap_set_string_slot,
   (void *)XtOffsetOf(auth_ldap_config_rec, bindpw), OR_AUTHCFG, TAKE1, 
   "Password to use to bind to LDAP server. If not provided, will do an anonymous bind "
   "will be done."},

  {"AuthLDAPRemoteUserIsDN", ap_set_flag_slot,
   (void *)XtOffsetOf(auth_ldap_config_rec, user_is_dn), OR_AUTHCFG, FLAG,
   "Set to 'on' to set the REMOTE_USER environment variable to be the full "
   "DN of the remote user. By default, this is set to off, meaning that "
   "the REMOTE_USER variable will contain whatever value the remote user sent."},

  {"AuthLDAPAuthoritative", ap_set_flag_slot,
   (void *)XtOffsetOf(auth_ldap_config_rec, auth_authoritative), OR_AUTHCFG, FLAG,
   "Set to 'off' to allow access control to be passed along to lower modules if "
   "the UserID and/or group is not known to this module"},

  {"AuthLDAPCompareDNOnServer", ap_set_flag_slot,
   (void *)XtOffsetOf(auth_ldap_config_rec, compare_dn_on_server), OR_AUTHCFG, FLAG,
   "Set to 'on' to force auth_ldap to do DN compares (for the \"require dn\" "
   "directive) using the server, and set it 'off' to do the compares locally "
   "(at the expense of possible false matches). See the documentation for "
   "a complete description of this option."},

  {"AuthLDAPGroupAttribute", auth_ldap_add_group_attribute, NULL, OR_AUTHCFG, ITERATE,
   "A list of attributes used to define group membership - defaults to "
   "member and uniquemember"},

  {"AuthLDAPGroupAttributeIsDN", ap_set_flag_slot,
   (void *)XtOffsetOf(auth_ldap_config_rec, group_attrib_is_dn), OR_AUTHCFG, FLAG,
   "If set to 'on', auth_ldap uses the DN that is retrieved from the server for"
   "subsequent group comparisons. If set to 'off', auth_ldap uses the string"
   "provided by the client directly. Defaults to 'on'."},

#ifdef WITH_SSL
  {"AuthLDAPCertDBPath", auth_ldap_set_certdbpath, NULL, RSRC_CONF, TAKE1,
   "Specifies the file containing Certificate Authority certificates "
   "for validating secure LDAP server certificates. This file must be the "
   "cert7.db database used by Netscape Communicator"},
#endif

  {"AuthLDAPCacheSize", auth_ldap_set_cache_size, NULL, RSRC_CONF, TAKE1,
   "Sets the maximum amount of memory in bytes that the LDAP search cache will use. "
   "Zero means no limit; -1 disables the cache. Defaults to 10KB."},

  {"AuthLDAPCacheTTL", auth_ldap_set_cache_ttl, NULL, RSRC_CONF, TAKE1,
   "Sets the maximum time (in seconds) that an item can be cached in the LDAP "
   "search cache. Zero means no limit. Defaults to 600 seconds (10 minutes)."},

  {"AuthLDAPOpCacheSize", auth_ldap_set_opcache_size, NULL, RSRC_CONF, TAKE1,
   "Sets the initial size of the LDAP operation cache (for bind and compare "
   "operations). Set to 0 to disable the cache. The default is 1024, which is "
   "sufficient to efficiently cache approximately 3000 distinct DNs."},

  {"AuthLDAPOpCacheTTL", auth_ldap_set_opcache_ttl, NULL, RSRC_CONF, TAKE1,
   "Sets the maximum time (in seconds) that an item is cached in the LDAP "
   "operation cache. Zero means no limit. Defaults to 600 seconds (10 minutes)."},

  {"AuthLDAPCacheCompareOps", auth_ldap_set_compare_flag, NULL, RSRC_CONF, TAKE1,
   "Set to no to disable caching of LDAP compare operations. Defaults to yes."},

  {"AuthLDAPDereferenceAliases", auth_ldap_set_deref, NULL, OR_AUTHCFG, TAKE1,
   "Determines how aliases are handled during a search. Can bo one of the"
   "values \"never\", \"searching\", \"finding\", or \"always\". "
   "Defaults to always."},

  {"AuthLDAPEnabled", ap_set_flag_slot,
   (void *)XtOffsetOf(auth_ldap_config_rec, enabled), OR_AUTHCFG, FLAG,
   "Set to off to disable auth_ldap, even if it's been enabled in a higher tree"
  },
 
#ifdef AUTH_LDAP_FRONTPAGE_HACK
  {"AuthLDAPFrontPageHack", ap_set_flag_slot,
   (void *)XtOffsetOf(auth_ldap_config_rec, frontpage_hack), OR_AUTHCFG, FLAG,
   "Set to 'on' to support Microsoft FrontPage"},
#endif

#ifdef HAVE_TLS
  {"AuthLDAPStartTLS", ap_set_flag_slot,
   (void *)XtOffsetOf(auth_ldap_config_rec, starttls), OR_AUTHCFG, FLAG,
   "Set to 'on' to start TLS after connecting to the LDAP server."
  },
#endif /* HAVE_TLS */

  {NULL}
};

static const handler_rec auth_ldap_handlers[] =
{
  {"auth-ldap-info", auth_ldap_display_info},
  {NULL}
};

module auth_ldap_module = {
   STANDARD_MODULE_STUFF,
   auth_ldap_init_module,	/* initializer */
   create_auth_ldap_dir_config,	/* dir config creater */
   NULL,			/* dir merger --- default is to override */
   create_auth_ldap_config,	/* server config */
   NULL,			/* merge server config */
   auth_ldap_cmds,		/* command table */
   auth_ldap_handlers,		/* handlers */
   NULL,			/* filename translation */
   ldap_authenticate_basic_user, /* check_user_id */
   ldap_check_auth,		/* check auth */
   NULL,			/* check access */
   NULL,			/* type_checker */
   NULL,			/* fixups */
   NULL,			/* logger */
   NULL,			/* header parser */
   NULL,			/* child_init */
   NULL,			/* child_exit */
   NULL				/* post read-request */
};

#ifdef NEED_EPRINTF
/* More subroutines needed by GCC output code on some machines.  */
/* Compile this one with gcc.  */
/* Copyright (C) 1989, 1992, 1993, 1994, 1995 Free Software Foundation, Inc.

This file is part if libgcc2.c, which is part of GNU CC. Auth_ldap just
needs the _eprintf function.

GNU CC is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2, or (at your option)
any later version.

GNU CC is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with GNU CC; see the file COPYING.  If not, write to
the Free Software Foundation, 59 Temple Place - Suite 330,
Boston, MA 02111-1307, USA.  */


#undef NULL /* Avoid errors if stdio.h and our stddef.h mismatch.  */
#include <stdio.h>
/* This is used by the `assert' macro.  */
void
__eprintf (string, expression, line, filename)
     const char *string;
     const char *expression;
     int line;
     const char *filename;
{
  fprintf (stderr, string, expression, line, filename);
  fflush (stderr);
  abort ();
}

#endif
