/* ============================================================
 * Copyright (c) 2003-2004, Andreas Brenk
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the
 * following conditions are met:
 * 
 *     * Redistributions of source code must retain the above
 *       copyright notice, this list of conditions and the
 *       following disclaimer.
 *     * Redistributions in binary form must reproduce the
 *       above copyright notice, this list of conditions and
 *       the following disclaimer in the documentation and/or
 *       other materials provided with the distribution.
 *     * Neither the names of the copyright holders nor the
 *       names of its contributors may be used to endorse or
 *       promote products derived from this software without
 *       specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 * ============================================================
 */

/*
 * mod_cfg_ldap.c --- read vhost config from an LDAP directory
 */

#include "httpd.h"
#include "http_config.h"
#include "http_core.h"
#include "http_log.h"
#include "http_request.h"
#include "apr_strings.h"
#include "ldap.h"

#define CFG_LDAP_NO_SUCH_VHOST "cfg_ldap_no_such_vhost"

#define HOST_NOT_FOUND 0
#define HOST_FOUND 1
#define MULTIPLE_HOST_FOUND 2

module AP_MODULE_DECLARE_DATA cfg_ldap_module;

typedef struct cfg_ldap_cfg
{
  int enabled;
  apr_time_t cachettl;
  const char *hostname;
  int port;
  int usetls;
  const char *cert_auth_file;
  const char *username;
  const char *password;
  const char *basedn;
  const char *filter;
} cfg_ldap_cfg;

typedef struct cfg_ldap_vhost
{
  const char *name;
  const char *admin;
  const char *docroot;
  apr_time_t timestamp;
} cfg_ldap_vhost;

typedef struct cfg_ldap_novhost
{
  apr_time_t timestamp;
} cfg_ldap_novhost;

static apr_pool_t *pool = NULL;
static apr_hash_t *cache = NULL;
static LDAP *ld = NULL;

static const char *attrs[] =
  { "apacheServerName", "apacheServerAdmin", "apacheDocumentRoot", 0 };

static void
cfg_ldap_init_ldap (apr_pool_t * p, server_rec * s)
{
  int failures = 0;

  cfg_ldap_cfg *cfg;
  cfg =
    (cfg_ldap_cfg *) ap_get_module_config (s->module_config,
					   &cfg_ldap_module);

  int rc;

  do
    {
      if (cfg->usetls)
	{
	  ld = ldap_init (cfg->hostname, LDAPS_PORT);

	  int version = LDAP_VERSION3;

	  if ((rc =
	       ldap_set_option (ld, LDAP_OPT_PROTOCOL_VERSION,
				&version)) != LDAP_OPT_SUCCESS)
	    {
	      ap_log_error (APLOG_MARK, APLOG_NOERRNO | APLOG_ERR, 0, NULL,
			    "mod_cfg_ldap: Setting LDAP version option failed: %s",
			    ldap_err2string (rc));
	      ldap_unbind (ld);
	      ld = NULL;
	      return;
	    }

	  int SSLmode = LDAP_OPT_X_TLS_HARD;
	  if ((rc =
	       ldap_set_option (ld, LDAP_OPT_X_TLS,
				&SSLmode)) != LDAP_SUCCESS)
	    {
	      ldap_unbind_s (ld);
	      ap_log_error (APLOG_MARK, APLOG_NOERRNO | APLOG_ERR, 0, NULL,
			    "mod_cfg_ldap: ldap_set_option - LDAP_OPT_X_TLS_HARD failed");
	      ld = NULL;
	      return;
	    }

	  if ((rc =
	       ldap_set_option (NULL, LDAP_OPT_X_TLS_CACERTFILE,
				cfg->cert_auth_file)) != LDAP_SUCCESS)
	    {
	      ldap_unbind_s (ld);
	      ap_log_error (APLOG_MARK, APLOG_CRIT, 0, s,
			    "mod_cfg_ldap: Invalid LDAPTrustedCA directive (%s): %s",
			    cfg->cert_auth_file, ldap_err2string (rc));
	      ld = NULL;
	      return;
	    }
	}
      else
	{
	  ld = ldap_init (cfg->hostname, LDAP_PORT);
	}

      rc = ldap_simple_bind_s (ld, cfg->username, cfg->password);

      // TODO extract "5" to MAX_FAILURES
      if (rc == LDAP_SERVER_DOWN && failures++ <= 5)
	{
	  ap_log_error (APLOG_MARK, APLOG_WARNING, 0, s,
			"cfg_ldap: unknown ldap error %d", rc);
	  break;
	}
    }
  while (!rc == LDAP_SUCCESS);
}

static void *
cfg_ldap_create_server_config (apr_pool_t * p, server_rec * s)
{
  cfg_ldap_cfg *cfg;
  cfg = (cfg_ldap_cfg *) apr_pcalloc (p, sizeof (cfg_ldap_cfg));
  cfg->enabled = 0;

  return (void *) cfg;
}

static apr_status_t
cfg_ldap_child_exit (void *data)
{
  ldap_unbind (ld);

  return APR_SUCCESS;
}

static void
cfg_ldap_child_init (apr_pool_t * p, server_rec * s)
{
  cfg_ldap_cfg *cfg;
  cfg =
    (cfg_ldap_cfg *) ap_get_module_config (s->module_config,
					   &cfg_ldap_module);

  if (!cfg->enabled)
    {
      return;
    }

  if (pool == NULL)
    {
      apr_pool_create (&pool, NULL);
    }

  if (cache == NULL)
    {
      cache = apr_hash_make (p);
    }

  if (ld == NULL)
    {
      cfg_ldap_init_ldap (p, s);
    }

  apr_pool_cleanup_register (p, NULL, cfg_ldap_child_exit,
			     cfg_ldap_child_exit);
}

static cfg_ldap_vhost *
cfg_ldap_read_vhost_from_ldap (apr_pool_t * p, server_rec * s, char *hostname)
{
  cfg_ldap_cfg *cfg;
  cfg =
    (cfg_ldap_cfg *) ap_get_module_config (s->module_config,
					   &cfg_ldap_module);

  cfg_ldap_vhost *vhost;
  vhost = (cfg_ldap_vhost *) apr_pcalloc (p, sizeof (cfg_ldap_vhost));
  vhost->timestamp = apr_time_now ();
  vhost->name = CFG_LDAP_NO_SUCH_VHOST;

  char *filter;
  filter =
    apr_pstrcat (p, "(&(|(apacheServerName=", hostname,
		 ")(apacheServerAlias=", hostname, "))", cfg->filter, ")",
		 NULL);

  int rc;
  LDAPMessage *res;
  rc = ldap_search_s (ld, cfg->basedn, LDAP_SCOPE_SUBTREE, filter,
		      (char **) &attrs, 0, &res);

  if (!rc == LDAP_SUCCESS)
    {
      if (rc == LDAP_SERVER_DOWN)
	{
	  cfg_ldap_init_ldap (p, s);
	  return cfg_ldap_read_vhost_from_ldap (p, s, hostname);
	}
      ap_log_error (APLOG_MARK, APLOG_WARNING, 0, s,
		    "cfg_ldap: unknown ldap error %d", rc);
      return vhost;
    }

  LDAPMessage *entry;
  entry = ldap_first_entry (ld, res);

  // count vhosts entries
  int count = ldap_count_entries (ld, res);
  if (count == 0)
    switch (count)
      {
      case HOST_NOT_FOUND:
	return vhost;
	break;
      case HOST_FOUND:
	break;
      case MULTIPLE_HOST_FOUND:
      default:
	ap_log_error (APLOG_MARK, APLOG_WARNING, 0, s,
		      "cfg_ldap: more than one entry for %s", hostname);
	return vhost;
	break;
      }

  char *attr;
  BerElement *ber;
  char **val = NULL;
  char *vhost_name = NULL;
  char *vhost_admin = NULL;
  char *vhost_docroot = NULL;

  for (attr = ldap_first_attribute (ld, entry, &ber);
       attr != NULL; attr = ldap_next_attribute (ld, entry, ber))
    {
      val = ldap_get_values (ld, entry, attr);
      if (strcasecmp (attr, "apacheServerName") == 0)
	{
	  vhost_name = apr_pstrdup (p, val[0]);
	}
      if (strcasecmp (attr, "apacheServerAdmin") == 0)
	{
	  vhost_admin = apr_pstrdup (p, val[0]);
	}
      if (strcasecmp (attr, "apacheDocumentRoot") == 0)
	{
	  vhost_docroot = apr_pstrdup (p, val[0]);
	}
    }

  ldap_value_free (val);
  ldap_memfree (attr);

  if (ber != NULL)
    {
      ber_free (ber, 0);
    }

  vhost->name = apr_pstrdup (p, vhost_name);
  if (vhost_admin != NULL)
    {
      vhost->admin = apr_pstrdup (p, vhost_admin);
    }
  else
    {
      vhost->admin = apr_pstrdup (p, s->server_admin);
    }
  vhost->docroot = apr_pstrdup (p, vhost_docroot);

  return vhost;
}

static int
cfg_ldap_translate_name (request_rec * r)
{
  apr_table_t *e;
  cfg_ldap_cfg *cfg;
  cfg_ldap_vhost *vhost;

  cfg =
    (cfg_ldap_cfg *) ap_get_module_config (r->server->module_config,
					   &cfg_ldap_module);

  // mod_cfg_ldap is disabled
  if (!cfg->enabled)
    {
      return DECLINED;
    }

  vhost = apr_hash_get (cache, r->hostname, APR_HASH_KEY_STRING);

  if (vhost == NULL)
    {
      vhost =
	cfg_ldap_read_vhost_from_ldap (pool, r->server,
				       apr_pstrdup (r->pool, r->hostname));
      apr_hash_set (cache, r->hostname, APR_HASH_KEY_STRING, vhost);
    }
  else
    {
      if ((vhost->timestamp + cfg->cachettl) < apr_time_now ())
	{
	  apr_hash_set (cache, r->hostname, APR_HASH_KEY_STRING, NULL);
	  return cfg_ldap_translate_name (r);
	}
    }

  // host not found
  if (strcasecmp (vhost->name, CFG_LDAP_NO_SUCH_VHOST) == 0)
    {
      return DECLINED;
    }

  r->filename =
    apr_pstrcat (r->pool, vhost->docroot, r->parsed_uri.path, NULL);
  r->server->server_hostname = apr_pstrdup (r->pool, vhost->name);
  r->server->server_admin = apr_pstrdup (r->pool, vhost->admin);

  // set environment variables
  e = r->subprocess_env;
  apr_table_addn (e, "SERVER_ROOT", vhost->docroot);

  return OK;
}

static const char *
set_cfg_ldap_enabled (cmd_parms * cmd, void *mconfig, const int enabled)
{
  cfg_ldap_cfg *cfg;
  cfg =
    (cfg_ldap_cfg *) ap_get_module_config (cmd->server->module_config,
					   &cfg_ldap_module);
  cfg->enabled = enabled;

  return NULL;
}

static const char *
set_cfg_ldap_hostname (cmd_parms * cmd, void *mconfig, const char *hostname)
{
  cfg_ldap_cfg *cfg;
  cfg =
    (cfg_ldap_cfg *) ap_get_module_config (cmd->server->module_config,
					   &cfg_ldap_module);
  cfg->hostname = hostname;

  return NULL;
}

static const char *
set_cfg_ldap_usetls (cmd_parms * cmd, void *mconfig, const int usetls)
{
  cfg_ldap_cfg *cfg;
  cfg = (cfg_ldap_cfg *) ap_get_module_config (cmd->server->module_config,
					       &cfg_ldap_module);

  cfg->usetls = usetls;
  return NULL;
}

static const char *
set_cfg_ldap_cert_auth (cmd_parms * cmd, void *dummy, const char *file)
{
  cfg_ldap_cfg *cfg;
  cfg = (cfg_ldap_cfg *) ap_get_module_config (cmd->server->module_config,
					       &cfg_ldap_module);

  const char *err = ap_check_cmd_context (cmd, GLOBAL_ONLY);
  if (err != NULL)
    {
      return err;
    }

  ap_log_error (APLOG_MARK, APLOG_DEBUG | APLOG_NOERRNO, 0, cmd->server,
		"LDAP: SSL trusted certificate authority file - %s", file);
  cfg->cert_auth_file = ap_server_root_relative (cmd->pool, file);

  return NULL;
}

static const char *
set_cfg_ldap_username (cmd_parms * cmd, void *mconfig, const char *username)
{
  cfg_ldap_cfg *cfg;
  cfg =
    (cfg_ldap_cfg *) ap_get_module_config (cmd->server->module_config,
					   &cfg_ldap_module);
  cfg->username = username;

  return NULL;
}

static const char *
set_cfg_ldap_password (cmd_parms * cmd, void *mconfig, const char *password)
{
  cfg_ldap_cfg *cfg;
  cfg =
    (cfg_ldap_cfg *) ap_get_module_config (cmd->server->module_config,
					   &cfg_ldap_module);
  cfg->password = password;

  return NULL;
}

static const char *
set_cfg_ldap_basedn (cmd_parms * cmd, void *mconfig, const char *basedn)
{
  cfg_ldap_cfg *cfg;
  cfg =
    (cfg_ldap_cfg *) ap_get_module_config (cmd->server->module_config,
					   &cfg_ldap_module);
  cfg->basedn = basedn;

  return NULL;
}

static const char *
set_cfg_ldap_filter (cmd_parms * cmd, void *mconfig, const char *filter)
{
  cfg_ldap_cfg *cfg;
  cfg = (cfg_ldap_cfg *) ap_get_module_config (cmd->server->module_config,
					       &cfg_ldap_module);
  cfg->filter = filter;

  return NULL;
}

static const char *
set_cfg_ldap_cachettl (cmd_parms * cmd, void *mconfig, const char *cachettl)
{
  cfg_ldap_cfg *cfg;
  cfg =
    (cfg_ldap_cfg *) ap_get_module_config (cmd->server->module_config,
					   &cfg_ldap_module);
  cfg->cachettl = 1000 * 1000 * atol (cachettl);

  return NULL;
}

static const command_rec cfg_ldap_cmds[] = {
  AP_INIT_FLAG ("EnableCfgLdap", set_cfg_ldap_enabled, NULL, RSRC_CONF,
		"enable mod_cfg_ldap"),
  AP_INIT_TAKE1 ("CfgLdapServer", set_cfg_ldap_hostname, NULL, RSRC_CONF,
		 "the LDAP server to connect to"),
  AP_INIT_FLAG ("CfgLdapUseTLS", set_cfg_ldap_usetls, NULL, RSRC_CONF,
		"whether to use an encrypted connection to the LDAP server"),
  AP_INIT_TAKE1 ("CfgLdapTrustedCA", set_cfg_ldap_cert_auth, NULL, RSRC_CONF,
		 "Sets the file containing the trusted Certificate Authority certificate. "
		 "Used to validate the LDAP server certificate for SSL connections."),
  AP_INIT_TAKE1 ("CfgLdapBindDN", set_cfg_ldap_username, NULL, RSRC_CONF,
		 "the DN to bind with"),
  AP_INIT_TAKE1 ("CfgLdapCredentials", set_cfg_ldap_password, NULL, RSRC_CONF,
		 "the credentials to bind with"),
  AP_INIT_TAKE1 ("CfgLdapBaseDN", set_cfg_ldap_basedn, NULL, RSRC_CONF,
		 "the base DN to use in searches"),
  AP_INIT_TAKE1 ("CfgLdapFilter", set_cfg_ldap_filter, NULL, RSRC_CONF,
		 "an additional filter to use in searches"),
  AP_INIT_TAKE1 ("CfgLdapCacheTTL", set_cfg_ldap_cachettl, NULL, RSRC_CONF,
		 "number of seconds a config is cached"),
  {NULL}
};

static void
cfg_ldap_register_hooks (apr_pool_t * p)
{
  ap_hook_child_init (cfg_ldap_child_init, NULL, NULL, APR_HOOK_MIDDLE);
  ap_hook_translate_name (cfg_ldap_translate_name, NULL, NULL,
			  APR_HOOK_MIDDLE);
}

module AP_MODULE_DECLARE_DATA cfg_ldap_module = {
  STANDARD20_MODULE_STUFF,
  NULL,
  NULL,
  cfg_ldap_create_server_config,
  NULL,
  cfg_ldap_cmds,
  cfg_ldap_register_hooks,
};
