/* 
   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_cache.c,v 1.10 2001/02/16 23:06:20 dave Exp $

*/

#include "auth_ldap.h"
extern module MODULE_VAR_EXPORT auth_ldap_module;

/* ------------------------------------------------------------------ */

unsigned long
auth_ldap_url_node_hash(void *n)
{
  url_node *node = (url_node *)n;
  return ald_hash_string(1, node->url);
}

int
auth_ldap_url_node_compare(void *a, void *b)
{
  url_node *na = (url_node *)a;
  url_node *nb = (url_node *)b;

  return(strcmp(na->url, nb->url) == 0);
}

void *
auth_ldap_url_node_copy(void *c)
{
  url_node *n = (url_node *)c;
  url_node *node = (url_node *)ald_alloc(sizeof(url_node));

  node->url = ald_strdup(n->url);
  node->search_cache = n->search_cache;
  node->compare_cache = n->compare_cache;
  node->dn_compare_cache = n->dn_compare_cache;
  return node;
}

void
auth_ldap_url_node_free(void *n)
{
  url_node *node = (url_node *)n;

  ald_free(node->url);
  ald_destroy_cache(node->search_cache);
  ald_destroy_cache(node->compare_cache);
  ald_destroy_cache(node->dn_compare_cache);
  ald_free(node);
}

/* ------------------------------------------------------------------ */

/* Cache functions for search nodes */
unsigned long
auth_ldap_search_node_hash(void *n)
{
  search_node *node = (search_node *)n;
  return ald_hash_string(1, ((search_node *)(node))->username);
}

int
auth_ldap_search_node_compare(void *a, void *b)
{
  return(strcmp(((search_node *)a)->username,
		((search_node *)b)->username) == 0);
}

void *
auth_ldap_search_node_copy(void *c)
{
  search_node *node = (search_node *)c;
  search_node *newnode = ald_alloc(sizeof(search_node));
  newnode->username = ald_strdup(node->username);
  newnode->dn = ald_strdup(node->dn);
  newnode->bindpw = ald_strdup(node->bindpw);
  newnode->lastbind = node->lastbind;
  return (void *)newnode;
}

void
auth_ldap_search_node_free(void *n)
{
  search_node *node = (search_node *)n;
  ald_free(node->username);
  ald_free(node->dn);
  ald_free(node->bindpw);
  ald_free(node);
}

/* ------------------------------------------------------------------ */

unsigned long
auth_ldap_compare_node_hash(void *n)
{
  compare_node *node = (compare_node *)n;
  return ald_hash_string(3, node->dn, node->attrib, node->value);
}

int
auth_ldap_compare_node_compare(void *a, void *b)
{
  compare_node *na = (compare_node *)a;
  compare_node *nb = (compare_node *)b;
  return (strcmp(na->dn, nb->dn) == 0 &&
	  strcmp(na->attrib, nb->attrib) == 0 &&
	  strcmp(na->value, nb->value) == 0);
}

void *
auth_ldap_compare_node_copy(void *c)
{
  compare_node *n = (compare_node *)c;
  compare_node *node = (compare_node *)ald_alloc(sizeof(compare_node));
  node->dn = ald_strdup(n->dn);
  node->attrib = ald_strdup(n->attrib);
  node->value = ald_strdup(n->value);
  node->lastcompare = n->lastcompare;
  return node;
}

void
auth_ldap_compare_node_free(void *n)
{
  compare_node *node = (compare_node *)n;
  ald_free(node->dn);
  ald_free(node->attrib);
  ald_free(node->value);
  ald_free(node);
}

/* ------------------------------------------------------------------ */

unsigned long
auth_ldap_dn_compare_node_hash(void *n)
{
  return ald_hash_string(1, ((dn_compare_node *)n)->reqdn);
}

int
auth_ldap_dn_compare_node_compare(void *a, void *b)
{
  return (strcmp(((dn_compare_node *)a)->reqdn,
		 ((dn_compare_node *)b)->reqdn) == 0);
}

void *
auth_ldap_dn_compare_node_copy(void *c)
{
  dn_compare_node *n = (dn_compare_node *)c;
  dn_compare_node *node = (dn_compare_node *)ald_alloc(sizeof(dn_compare_node));
  node->reqdn = ald_strdup(n->reqdn);
  node->dn = ald_strdup(n->dn);
  return node;
}

void
auth_ldap_dn_compare_node_free(void *n)
{
  dn_compare_node *node = (dn_compare_node *)n;
  ald_free(node->reqdn);
  ald_free(node->dn);
  ald_free(node);
}

/* ------------------------------------------------------------------ */

/*
 * Compares two DNs to see if they're equal. The only way to do this correctly is to 
 * search for the dn and then do ldap_get_dn() on the result. This should match the 
 * initial dn, since it would have been also retrieved with ldap_get_dn(). This is
 * expensive, so if the configuration value AuthLDAPCompareDNOnServer is
 * false, just does an ordinary strcmp.
 *
 * The mutex for the ldap cache should already be acquired.
 */
int
auth_ldap_comparedn(const char *dn, const char *reqdn, 
		    request_rec *r, url_node *curl)
{
  int result;
  dn_compare_node *node;
  dn_compare_node newnode;
  auth_ldap_config_rec *sec;
  auth_ldap_server_conf *conf;
  int failures = 0;
  LDAPMessage *res, *entry;
  char *searchdn;
  int ret;

  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);

  if (!sec->compare_dn_on_server) {
    ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		  "{%d} Comparing the two DNs (doing local compare)", (int)getpid());
    return strcmp(dn, reqdn) == 0;
  } 

  ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		"{%d} Comparing the two DNs (using server-side compare)", (int)getpid());

  ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		"{%d} Searching for `%s'/`%s' in the dn compare cache", (int)getpid(),
		dn, reqdn);
  
  newnode.reqdn = (char *)reqdn;
  node = ald_cache_fetch(curl->dn_compare_cache, &newnode);

  GETMUTEX(sec->ldc->mtx);

  if (node != NULL) {
    /* If it's in the cache, it's good */
    ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		  "{%d} Found one", (int)getpid());
    RELMUTEX(sec->ldc->mtx);
    return 1;
  }
    
  ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		"{%d} No match in the dn compare cache", (int)getpid());

 start_over:
  if (failures++ > 10) {
    auth_ldap_log_reason(r, "Too many failures connecting to LDAP server");
    RELMUTEX(sec->ldc->mtx);
    return 0;
  }
  if (!auth_ldap_connect_to_server(r)) {
    RELMUTEX(sec->ldc->mtx);
    return 0;
  }

  ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		"{%d} Doing LDAP compare of uncached %s=%s", (int)getpid(),
		reqdn, dn);

  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, const_cast(reqdn), LDAP_SCOPE_BASE, 
				  "(objectclass=*)", 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", 
			 reqdn, ldap_err2string(result));
    RELMUTEX(sec->ldc->mtx);
    return 0;
  }

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

  ldap_msgfree(res);
  if (strcmp(dn, searchdn) != 0) {
    RELMUTEX(sec->ldc->mtx);
    ret = 0;
  } else {
    ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		  "{%d} Adding `%s'/`%s' to dn compare cache", (int)getpid(),
		  dn, reqdn);

    newnode.reqdn = (char *)reqdn;
    newnode.dn = (char *)dn;
    ald_cache_insert(curl->dn_compare_cache, &newnode);
    RELMUTEX(sec->ldc->mtx);
    ret = 1;
  }
  ldap_memfree(searchdn);
  return ret;
}

/*
 * Does an generic ldap_compare operation. It accepts a cache that it will use
 * to lookup the compare in the cache. We cache two kinds of compares 
 * (require group compares) and (require user compares). Each compare has a different
 * cache node: require group includes the DN; require user does not because the
 * require user cache is owned by the 
 *
 * The mutex for the ldap cache should already be acquired.
 */
int
auth_ldap_compare(const char *dn, 
		  const char *attrib, 
		  const char *value, 
		  request_rec *r, 
		  ald_cache *cache)
{
  int result;
  compare_node *compare_nodep;
  compare_node the_compare_node;
  auth_ldap_config_rec *sec;
  auth_ldap_server_conf *conf;
  time_t curtime;
  int failures = 0;

  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);

  time(&curtime);

  ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		"{%d} Searching cache for `%s'/`%s' and dn `%s'",
		(int)getpid(), attrib, value, dn);

  the_compare_node.dn = (char *)dn;
  the_compare_node.attrib = (char *)attrib;
  the_compare_node.value = (char *)value;

  compare_nodep = ald_cache_fetch(cache, &the_compare_node);

  if (compare_nodep != NULL) {
    ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		  "{%d} Found it...", (int)getpid());
    if (curtime - compare_nodep->lastcompare > conf->compare_cache_ttl) {
      ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		    "{%d} ...but it's too old.", (int)getpid());
      ald_cache_remove(cache, compare_nodep);
    } else {
      ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		    "{%d} ...and it's good.", (int)getpid());
      RELMUTEX(sec->ldc->mtx);
      return 1;
    }
  }

  GETMUTEX(sec->ldc->mtx);

 start_over:
  if (failures++ > 10) {
    auth_ldap_log_reason(r, "Too many failures connecting to LDAP server");
    RELMUTEX(sec->ldc->mtx);
    return 0;
  }
  if (!auth_ldap_connect_to_server(r)) {
    RELMUTEX(sec->ldc->mtx);
    return 0;
  }

  ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		"{%d} Doing LDAP compare of %s=%s in entry %s", (int)getpid(),
		attrib, value, dn);

  ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		"{%d} LDAP OP: compare", (int)getpid());
  if ((result = ldap_compare_s(sec->ldc->ldap, const_cast(dn), 
			       const_cast(attrib), const_cast(value))) == 
      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_COMPARE_TRUE) {
    ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		  "{%d} Compare succeeded; caching result", (int)getpid());
    the_compare_node.lastcompare = curtime;
    ald_cache_insert(cache, &the_compare_node);
    RELMUTEX(sec->ldc->mtx);
    return 1;
  } else {
    ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, r,
		  "{%d} Compare failed", (int)getpid());
    RELMUTEX(sec->ldc->mtx);
    return 0;
  }
}

/*
 * Some definitions to help between various versions of apache.
 */

#ifndef DOCTYPE_HTML_2_0
#define DOCTYPE_HTML_2_0  "<!DOCTYPE HTML PUBLIC \"-//IETF//" \
                          "DTD HTML 2.0//EN\">\n"
#endif

#ifndef DOCTYPE_HTML_3_2
#define DOCTYPE_HTML_3_2  "<!DOCTYPE HTML PUBLIC \"-//W3C//" \
                          "DTD HTML 3.2 Final//EN\">\n"
#endif

#ifndef DOCTYPE_HTML_4_0S
#define DOCTYPE_HTML_4_0S "<!DOCTYPE HTML PUBLIC \"-//W3C//" \
                          "DTD HTML 4.0//EN\"\n" \
                          "\"http://www.w3.org/TR/REC-html40/strict.dtd\">\n"
#endif

#ifndef DOCTYPE_HTML_4_0T
#define DOCTYPE_HTML_4_0T "<!DOCTYPE HTML PUBLIC \"-//W3C//" \
                          "DTD HTML 4.0 Transitional//EN\"\n" \
                          "\"http://www.w3.org/TR/REC-html40/loose.dtd\">\n"
#endif

#ifndef DOCTYPE_HTML_4_0F
#define DOCTYPE_HTML_4_0F "<!DOCTYPE HTML PUBLIC \"-//W3C//" \
                          "DTD HTML 4.0 Frameset//EN\"\n" \
                          "\"http://www.w3.org/TR/REC-html40/frameset.dtd\">\n"
#endif

int
auth_ldap_display_info(request_rec *r)
{
  int i;
  char buf[MAX_STRING_LEN];

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

  r->allowed |= (1 << M_GET);
  if (r->method_number != M_GET)
    return DECLINED;

  r->content_type = "text/html";
  ap_send_http_header(r);
  if (r->header_only)
    return 0;

  ap_hard_timeout("send auth_ldap info", r);

  ap_rputs(DOCTYPE_HTML_3_2
	   "<html><head><title>Auth_LDAP Information</title></head>\n", r);
  ap_rputs("<body bgcolor='#ffffff'><h1 align=center>Auth_LDAP Information</h1>\n", r);

  if (auth_ldap_cache == NULL) {
    ap_rputs("<i>URL cache is NULL</i>", r);
    ap_kill_timeout(r);
    return 0;
  }

  ap_rputs("<p>\n"
	   "<table border='0'>\n"
	   "<tr bgcolor='#000000'>\n"
	   "<td><font size='-1' face='Arial,Helvetica' color='#ffffff'><b>Cache Name</b></font></td>"
	   "<td><font size='-1' face='Arial,Helvetica' color='#ffffff'><b>Entries</b></font></td>"
	   "<td><font size='-1' face='Arial,Helvetica' color='#ffffff'><b>Avg. Chain Len.</b></font></td>"
	   "<td colspan='2'><font size='-1' face='Arial,Helvetica' color='#ffffff'><b>Hits</b></font></td>"
	   "<td><font size='-1' face='Arial,Helvetica' color='#ffffff'><b>Ins/Rem</b></font></td>"
	   "<td colspan='2'><font size='-1' face='Arial,Helvetica' color='#ffffff'><b>Purges</b></font></td>"
	   "<td><font size='-1' face='Arial,Helvetica' color='#ffffff'><b>Avg Purge Time</b></font></td>"
	   "</tr>\n", r
	   );

  ald_cache_display_stats(auth_ldap_cache, r, "LDAP URL Cache");

  for (i=0; i < auth_ldap_cache->size; ++i) {
    cache_node *p;
    for (p = auth_ldap_cache->nodes[i]; p != NULL; p = p->next) {
      url_node *n;

      n = (url_node *)p->payload;
      
      ap_snprintf(buf, sizeof(buf), "%s (Searches)", n->url);
      ald_cache_display_stats(n->search_cache, r, buf);

      ap_snprintf(buf, sizeof(buf), "%s (Compares)", n->url);
      ald_cache_display_stats(n->compare_cache, r, buf);

      ap_snprintf(buf, sizeof(buf), "%s (DNCompares)", n->url);
      ald_cache_display_stats(n->dn_compare_cache, r, buf);
    }
  }
  
  ap_rputs("</table>\n</p>\n", r);

  ap_kill_timeout(r);
  return 0;
}
