/*
 * adcli
 *
 * Copyright (C) 2012 Red Hat Inc.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This program 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301 USA
 *
 * Author: Stef Walter <stefw@gnome.org>
 */

#include "config.h"

#include "adcli.h"
#include "adprivate.h"
#include "addisco.h"

#include <gssapi/gssapi_krb5.h>
#include <krb5/krb5.h>
#include <ldap.h>
#include <sasl/sasl.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>

#include <assert.h>
#include <errno.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

struct _adcli_conn_ctx {
	int refs;

	/* Input/output params */
	char *user_name;
	char *user_password;
	char *computer_name;
	char *computer_password;
	char *login_ccache_name;
	int login_ccache_name_is_krb5;
	char *login_keytab_name;
	int login_keytab_name_is_krb5;
	adcli_login_type login_type;
	int logins_allowed;

	char *krb5_conf_dir;
	char *krb5_conf_snippet;

	adcli_password_func password_func;
	adcli_destroy_func password_destroy;
	void *password_data;

	char *host_fqdn;
	char *domain_name;
	char *domain_realm;
	char *domain_controller;
	char *canonical_host;
	char *domain_short;
	char *domain_sid;
	adcli_disco *domain_disco;
	char *default_naming_context;
	char *configuration_naming_context;
	char **supported_capabilities;

	/* Connect state */
	LDAP *ldap;
	int ldap_authenticated;
	krb5_context k5;
	krb5_ccache ccache;
	krb5_keytab keytab;
};

static char *try_to_get_fqdn (const char *host_name)
{
	int ret;
	char *fqdn = NULL;
	struct addrinfo *res;
	struct addrinfo hints;

	memset (&hints, 0, sizeof (struct addrinfo));
	hints.ai_socktype = SOCK_DGRAM;
	hints.ai_flags = AI_CANONNAME;

	ret = getaddrinfo (host_name, NULL, &hints, &res);
	if (ret != 0) {
		_adcli_err ("Failed to find FQDN: %s", gai_strerror (ret));
		return NULL;
	}

	fqdn = strdup (res->ai_canonname);

	freeaddrinfo (res);

	return fqdn;
}

static adcli_result
ensure_host_fqdn (adcli_result res,
                  adcli_conn *conn)
{
	char hostname[HOST_NAME_MAX + 1];
	char *fqdn = NULL;
	int ret;

	if (res != ADCLI_SUCCESS)
		return res;

	if (conn->host_fqdn) {
		_adcli_info ("Using fully qualified name: %s", conn->host_fqdn);
		return ADCLI_SUCCESS;
	}

	ret = gethostname (hostname, sizeof (hostname));
	if (ret < 0) {
		_adcli_err ("Couldn't get local hostname: %s", strerror (errno));
		return ADCLI_ERR_UNEXPECTED;
	}

	if (strchr (hostname, '.') == NULL) {
		fqdn = try_to_get_fqdn (hostname);
	}
	conn->host_fqdn = fqdn != NULL ? fqdn : strdup (hostname);
	return_unexpected_if_fail (conn->host_fqdn != NULL);
	return ADCLI_SUCCESS;
}

static void
disco_dance_if_necessary (adcli_conn *conn)
{
	if (conn->domain_disco)
		return;

	if (conn->domain_controller)
		adcli_disco_host (conn->domain_controller, &conn->domain_disco);

	else if (conn->domain_name)
		adcli_disco_domain (conn->domain_name, &conn->domain_disco);

	if (conn->domain_disco) {
		if (!conn->domain_short && conn->domain_disco->domain_short) {
			conn->domain_short = strdup (conn->domain_disco->domain_short);
			return_if_fail (conn->domain_short != NULL);
		}
	}
}

static void
no_more_disco (adcli_conn *conn)
{
	if (conn->domain_disco)
		adcli_disco_free (conn->domain_disco);
	conn->domain_disco = NULL;
}

static adcli_result
ensure_domain_and_host (adcli_result res,
                        adcli_conn *conn)
{
	const char *dom;

	if (res != ADCLI_SUCCESS)
		return res;

	if (conn->domain_name) {
		_adcli_info ("Using domain name: %s", conn->domain_name);
		return ADCLI_SUCCESS;
	}

	assert (conn->host_fqdn != NULL);

	disco_dance_if_necessary (conn);

	if (conn->domain_disco && conn->domain_disco->domain) {
		conn->domain_name = strdup (conn->domain_disco->domain);
		return_unexpected_if_fail (conn->domain_name != NULL);

		_adcli_info ("Discovered domain name: %s", conn->domain_name);
		return ADCLI_SUCCESS;
	}

	/* Use the FQDN minus the last part */
	dom = strchr (conn->host_fqdn, '.');

	/* If no dot, or dot is first or last, then fail */
	if (dom == NULL || dom == conn->host_fqdn || dom[1] == '\0') {
		_adcli_err ("Couldn't determine the domain name from host name: %s",
		            conn->host_fqdn);
		return ADCLI_ERR_FAIL;
	}

	conn->domain_name = strdup (dom + 1);
	return_unexpected_if_fail (conn->domain_name != NULL);

	_adcli_info ("Calculated domain name from host fqdn: %s",
	             conn->domain_name);

	return ADCLI_SUCCESS;
}

char *
_adcli_calc_netbios_name (const char *host_fqdn)
{
	const char *dom;
	char *computer_name;

	/* Use the FQDN minus the last part */
	dom = strchr (host_fqdn, '.');

	/* If dot is first then fail */
	if (dom == host_fqdn) {
		_adcli_err ("Couldn't determine the computer account name from host name: %s",
		            host_fqdn);
		return NULL;

	} else if (dom == NULL) {
		computer_name = strdup (host_fqdn);
		return_val_if_fail (computer_name != NULL, NULL);

	} else {
		computer_name = strndup (host_fqdn, dom - host_fqdn);
		return_val_if_fail (computer_name != NULL, NULL);
	}

	_adcli_str_up (computer_name);
	if (strlen (computer_name) > 15) {
		computer_name[15] = 0;
		_adcli_info ("Truncated computer account name from fqdn: %s", computer_name);
	} else {
		_adcli_info ("Calculated computer account name from fqdn: %s", computer_name);
	}

	return computer_name;
}

static adcli_result
ensure_computer_name (adcli_result res,
                      adcli_conn *conn)
{

	if (res != ADCLI_SUCCESS)
		return res;

	if (conn->computer_name) {
		_adcli_info ("Using computer account name: %s", conn->computer_name);
		return ADCLI_SUCCESS;
	}

	assert (conn->host_fqdn != NULL);

	conn->computer_name = _adcli_calc_netbios_name (conn->host_fqdn);
	if (conn->computer_name == NULL)
		return ADCLI_ERR_CONFIG;

	return ADCLI_SUCCESS;
}


static adcli_result
ensure_domain_realm (adcli_result res,
                     adcli_conn *conn)
{
	if (res != ADCLI_SUCCESS)
		return res;

	if (conn->domain_realm) {
		_adcli_info ("Using domain realm: %s", conn->domain_name);
		return ADCLI_SUCCESS;
	}

	conn->domain_realm = strdup (conn->domain_name);
	return_unexpected_if_fail (conn->domain_realm != NULL);

	_adcli_str_up (conn->domain_realm);
	_adcli_info ("Calculated domain realm from name: %s",
	             conn->domain_realm);
	return ADCLI_SUCCESS;
}

static adcli_result
ensure_user_password (adcli_conn *conn)
{
	if (conn->login_ccache_name != NULL ||
	    conn->user_password != NULL)
		return ADCLI_SUCCESS;

	if (conn->password_func) {
		conn->user_password = (conn->password_func) (ADCLI_LOGIN_USER_ACCOUNT,
		                                             conn->user_name, 0,
		                                             conn->password_data);
	}

	if (conn->user_password == NULL) {
		_adcli_err ("No admin password or credential cache specified");
		return ADCLI_ERR_CREDENTIALS;
	}

	return ADCLI_SUCCESS;
}

char *
_adcli_calc_reset_password (const char *computer_name)
{
	char *password;

	assert (computer_name != NULL);
	password = strdup (computer_name);
	return_val_if_fail (password != NULL, NULL);
	_adcli_str_down (password);

	return password;
}

static adcli_result
handle_kinit_krb5_code (adcli_conn *conn,
                        adcli_login_type type,
                        const char *name,
                        krb5_error_code code)
{
	if (code == 0) {
		return ADCLI_SUCCESS;

	} else if (code == ENOMEM) {
		return_unexpected_if_reached ();

	} else if (code == KRB5KDC_ERR_PREAUTH_FAILED ||
	           code == KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN ||
	           code == KRB5KDC_ERR_KEY_EXP ||
	           code == KRB5KDC_ERR_CLIENT_REVOKED ||
	           code == KRB5KDC_ERR_POLICY ||
	           code == KRB5KDC_ERR_ETYPE_NOSUPP ||
	           code == KRB5_PREAUTH_FAILED) {
		if (type == ADCLI_LOGIN_COMPUTER_ACCOUNT) {
			_adcli_err ("Couldn't authenticate as machine account: %s: %s",
			            name, krb5_get_error_message (conn->k5, code));
		} else {
			_adcli_err ("Couldn't authenticate as: %s: %s",
			            name, krb5_get_error_message (conn->k5, code));
		}
		return ADCLI_ERR_CREDENTIALS;

	} else {
		if (type == ADCLI_LOGIN_COMPUTER_ACCOUNT) {
			_adcli_err ("Couldn't get kerberos ticket for machine account: %s: %s",
			            name, krb5_get_error_message (conn->k5, code));
		} else {
			_adcli_err ("Couldn't get kerberos ticket for: %s: %s",
			            name, krb5_get_error_message (conn->k5, code));
		}
		return ADCLI_ERR_DIRECTORY;
	}
}

static void
clear_krb5_conf_snippet (adcli_conn *conn)
{
	if (conn->krb5_conf_snippet) {
		if (unlink (conn->krb5_conf_snippet) < 0) {
			_adcli_warn ("Couldn't remove krb5.conf snippet file: %s: %s",
			             conn->krb5_conf_snippet, strerror (errno));
		}
		free (conn->krb5_conf_snippet);
		conn->krb5_conf_snippet = NULL;
	}
}

static adcli_result
setup_krb5_conf_snippet (adcli_conn *conn)
{
	char *filename;
	char *snippet;
	char *controller;
	int errn;
	int ret;
	int fd;
	mode_t old_mask;

	if (!conn->krb5_conf_dir)
		return ADCLI_SUCCESS;

	/* Already written out the conf snippet */
	if (conn->krb5_conf_snippet)
		return ADCLI_SUCCESS;

	clear_krb5_conf_snippet (conn);

	if (asprintf (&filename, "%s/adcli-krb5-conf-XXXXXX", conn->krb5_conf_dir) < 0)
		return_unexpected_if_reached ();

	if (strchr (conn->domain_controller, ':')) {
		if (asprintf (&controller, "[%s]", conn->domain_controller) < 0)
			controller = NULL;
	} else {
		controller = strdup (conn->domain_controller);
	}

	return_unexpected_if_fail (controller != NULL);

	if (asprintf (&snippet, "[realms]\n"
	                        "  %s = {\n"
	                        "    kdc = %s:88\n"
	                        "    master_kdc = %s:88\n"
	                        "    kpasswd_server = %s\n"
	                        "  }\n"
	                        "[domain_realm]\n"
	                        "  %s = %s\n"
	                        "  %s = %s\n",
	              conn->domain_realm, controller, controller, controller,
	              conn->canonical_host, conn->domain_realm,
	              conn->domain_controller, conn->domain_realm) < 0)
		return_unexpected_if_reached ();

	old_mask = umask (0177);
	fd = mkstemp (filename);
	umask (old_mask);
	if (fd < 0) {
		_adcli_warn ("Couldn't create krb5.conf snippet file in: %s: %s",
		             conn->krb5_conf_dir, strerror (errno));

	} else {
		conn->krb5_conf_snippet = filename;
		ret = _adcli_write_all (fd, snippet, -1);
		errn = errno;

		if (ret >= 0) {
			ret = close (fd);
			errn = errno;

		} else {
			close (fd);
		}

		if (ret < 0) {
			_adcli_warn ("Couldn't write krb5.conf snippet file in: %s: %s",
			             filename, strerror (errn));
			clear_krb5_conf_snippet (conn);
		} else {
			_adcli_info ("Wrote out krb5.conf snippet to %s", filename);
		}
	}

	free (controller);
	free (snippet);

	/* This shouldn't stop joining */
	return ADCLI_SUCCESS;
}

/*
 * HACK: This is to work around a bug in krb5 where if an empty password
 * preauth will fail unless a prompter is present.
 */
static krb5_error_code
null_prompter (krb5_context context,
               void *data,
               const char *name,
               const char *banner,
               int num_prompts,
               krb5_prompt prompts[])
{
	int i;

	for (i = 0; i < num_prompts; i++)
		prompts[i].reply->length = 0;

	return 0;
}

krb5_error_code
_adcli_kinit_computer_creds (adcli_conn *conn,
                             const char *in_tkt_service,
                             krb5_ccache ccache,
                             krb5_creds *creds)
{
	krb5_get_init_creds_opt *opt;
	krb5_principal principal;
	krb5_error_code code;
	krb5_context k5;
	krb5_creds dummy;
	char *new_password;
	const char *password;
	char *sam;

	assert (conn != NULL);

	k5 = adcli_conn_get_krb5_context (conn);

	if (asprintf (&sam, "%s$", conn->computer_name) < 0)
		return_unexpected_if_reached();

	code = _adcli_krb5_build_principal (k5, sam, conn->domain_realm, &principal);
	return_val_if_fail (code == 0, code);

	code = krb5_get_init_creds_opt_alloc (k5, &opt);
	return_val_if_fail (code == 0, code);

	if (ccache) {
		code = krb5_get_init_creds_opt_set_out_ccache (k5, opt, ccache);
		return_val_if_fail (code == 0, code);
	}

	memset (&dummy, 0, sizeof (dummy));
	if (!creds)
		creds = &dummy;

	password = conn->computer_password;
	new_password = NULL;

	/*
	 * Note that we only prompt for computer account passwords if
	 * explicitly requested.
	 */

	if (conn->keytab) {
		code = krb5_get_init_creds_keytab (k5, creds, principal, conn->keytab,
		                                   0, (char *)in_tkt_service, opt);

	} else {
		if (!password && conn->password_func &&
		    conn->logins_allowed == ADCLI_LOGIN_COMPUTER_ACCOUNT) {
			new_password = (conn->password_func) (ADCLI_LOGIN_COMPUTER_ACCOUNT,
			                                      sam, 0, conn->password_data);
			password = new_password;
		}

		if (password == NULL) {
			new_password = _adcli_calc_reset_password (conn->computer_name);
			password = new_password;
		}

		code = krb5_get_init_creds_password (k5, creds, principal, (char *)password,
		                                     null_prompter, NULL, 0, (char *)in_tkt_service, opt);

		if (code == 0 && new_password) {
			_adcli_password_free (conn->computer_password);
			conn->computer_password = new_password;
		}
	}

	krb5_free_principal (k5, principal);
	krb5_get_init_creds_opt_free (k5, opt);
	krb5_free_cred_contents (k5, &dummy);

	free (sam);
	return code;
}

krb5_error_code
_adcli_kinit_user_creds (adcli_conn *conn,
                         const char *in_tkt_service,
                         krb5_ccache ccache,
                         krb5_creds *creds)
{
	krb5_get_init_creds_opt *opt;
	krb5_principal principal;
	krb5_error_code code;
	krb5_context k5;
	krb5_creds dummy;

	assert (conn != NULL);

	k5 = adcli_conn_get_krb5_context (conn);

	code = krb5_parse_name (k5, conn->user_name, &principal);
	return_val_if_fail (code == 0, code);

	code = krb5_get_init_creds_opt_alloc (k5, &opt);
	return_val_if_fail (code == 0, code);

	if (ccache) {
		code = krb5_get_init_creds_opt_set_out_ccache (k5, opt, ccache);
		return_val_if_fail (code == 0, code);
	}

	memset (&dummy, 0, sizeof (dummy));
	if (!creds)
		creds = &dummy;

	code = krb5_get_init_creds_password (k5, creds, principal,
	                                     conn->user_password, null_prompter, NULL,
	                                     0, (char *)in_tkt_service, opt);

	krb5_free_principal (k5, principal);
	krb5_get_init_creds_opt_free (k5, opt);
	krb5_free_cred_contents (k5, &dummy);

	return code;
}

static adcli_result
kinit_with_computer_credentials (adcli_conn *conn,
                                 krb5_ccache ccache)
{
	adcli_result res;
	krb5_error_code code;
	int use_default;

	use_default = (conn->computer_password == NULL);

	code = _adcli_kinit_computer_creds (conn, NULL, ccache, NULL);

	if (code == 0) {
		_adcli_info ("Authenticated as %scomputer account: %s",
		             use_default ? "default/reset " : "", conn->computer_name);

		conn->login_type = ADCLI_LOGIN_COMPUTER_ACCOUNT;
		res = ADCLI_SUCCESS;

	} else {
		res = handle_kinit_krb5_code (conn, ADCLI_LOGIN_COMPUTER_ACCOUNT,
		                              conn->computer_name, code);
	}

	return res;
}

static adcli_result
kinit_with_user_credentials (adcli_conn *conn,
                             krb5_ccache ccache)
{
	adcli_result res;
	krb5_error_code code;
	char *name;

	/* Build out the admin principal name */
	if (!conn->user_name) {
		if (asprintf (&conn->user_name, "Administrator@%s", conn->domain_realm) < 0)
			return_unexpected_if_reached ();
	} else if (strchr (conn->user_name, '@') == NULL) {
		if (asprintf (&name, "%s@%s", conn->user_name, conn->domain_realm) < 0)
			return_unexpected_if_reached ();
		free (conn->user_name);
		conn->user_name = name;
	}

	res = ensure_user_password (conn);
	if (res != ADCLI_SUCCESS)
		return res;

	code = _adcli_kinit_user_creds (conn, NULL, ccache, NULL);

	if (code == 0) {
		conn->login_type = ADCLI_LOGIN_USER_ACCOUNT;
		_adcli_info ("Authenticated as user: %s", conn->user_name);
		return ADCLI_SUCCESS;
	}

	return handle_kinit_krb5_code (conn, ADCLI_LOGIN_USER_ACCOUNT, conn->user_name, code);
}

static adcli_result
prep_kerberos_and_kinit (adcli_conn *conn)
{
	krb5_error_code code;
	int logged_in = 0;
	krb5_ccache ccache;
	adcli_result res;

	if (conn->login_ccache_name != NULL) {
		if (!conn->ccache) {

			/*
			 * If we already have a kerberos ccache file, just open it. This
			 * serves two purposes:
			 * a) We want to make sure it's present, so we can provide more
			 *    intelligible messages than ldap_sasl_interactive_bind_s()
			 * b) We want to have the ccache member populated so we can use
			 *    it in other operations such as changing the computer password.
			 */

			if (strcmp (conn->login_ccache_name, "") == 0) {
				code = krb5_cc_default (conn->k5, &conn->ccache);
				if (code == 0) {
					free (conn->login_ccache_name);
					conn->login_ccache_name = NULL;
					code = krb5_cc_get_full_name (conn->k5, conn->ccache,
					                              &conn->login_ccache_name);
					conn->login_ccache_name_is_krb5 = 1;
					return_unexpected_if_fail (code == 0);
				}
			} else {
				code = krb5_cc_resolve (conn->k5, conn->login_ccache_name, &conn->ccache);
			}

			if (code != 0) {
				_adcli_err ("Couldn't open kerberos credential cache: %s: %s",
				            conn->login_ccache_name, krb5_get_error_message (NULL, code));
				return ADCLI_ERR_CONFIG;
			}
		}
		return ADCLI_SUCCESS;
	}

	if (conn->login_keytab_name != NULL) {
		if (!conn->keytab) {
			res = _adcli_krb5_open_keytab (conn->k5, conn->login_keytab_name, &conn->keytab);
			if (res != ADCLI_SUCCESS) {
				if (res == ADCLI_ERR_FAIL)
					res = ADCLI_ERR_CONFIG;
				return res;
			}

			if (strcmp (conn->login_keytab_name, "") == 0) {
				free (conn->login_keytab_name);
				conn->login_keytab_name = malloc (MAX_KEYTAB_NAME_LEN);
				code = krb5_kt_get_name (conn->k5, conn->keytab,
				                         conn->login_keytab_name, MAX_KEYTAB_NAME_LEN);
				conn->login_keytab_name_is_krb5 = 1;
				return_unexpected_if_fail (code == 0);
			}
		}
	}

	/* Initialize the credential cache */
	code = krb5_cc_new_unique (conn->k5, "MEMORY", NULL, &ccache);
	return_unexpected_if_fail (code == 0);

	/*
	 * Should we try to connect with computer account default password?
	 * This is the password set by 'Reset Accuont' on a computer object.
	 * If the caller explicitly specified a login name or password, then
	 * go straight to that.
	 */

	if (conn->logins_allowed & ADCLI_LOGIN_COMPUTER_ACCOUNT) {
		res = kinit_with_computer_credentials (conn, ccache);
		logged_in = (res == ADCLI_SUCCESS);
	}

	/* Use login credentials */
	if (!logged_in && (conn->logins_allowed & ADCLI_LOGIN_USER_ACCOUNT)) {
		res = kinit_with_user_credentials (conn, ccache);
		logged_in = (res == ADCLI_SUCCESS);
	}

	if (logged_in) {
		code = krb5_cc_get_full_name (conn->k5, ccache,
		                              &conn->login_ccache_name);
		return_unexpected_if_fail (code == 0);

		conn->ccache = ccache;
		conn->login_ccache_name_is_krb5 = 1;
		ccache = NULL;
		res = ADCLI_SUCCESS;
	} else {
		res = ADCLI_ERR_FAIL;
	}

	if (ccache != NULL)
		krb5_cc_close (conn->k5, ccache);
	return res;

}

/* Not included in ldap.h but documented */
int ldap_init_fd (ber_socket_t fd, int proto, LDAP_CONST char *url, struct ldap **ldp);

static LDAP *
connect_to_address (const char *host,
                    const char *canonical_host)
{
	struct addrinfo *res = NULL;
	struct addrinfo *ai;
	struct addrinfo hints;
	LDAP *ldap = NULL;
	int error = 0;
	char *url;
	int sock;
	int rc;

	memset (&hints, '\0', sizeof(hints));
#ifdef AI_ADDRCONFIG
	hints.ai_flags |= AI_ADDRCONFIG;
#endif
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_protocol = IPPROTO_TCP;

	if (!canonical_host)
		canonical_host = host;

	rc = getaddrinfo (host, "389", &hints, &res);
	if (rc != 0) {
		_adcli_err ("Couldn't resolve host name: %s: %s", host, gai_strerror (rc));
		return NULL;
	}

	for (ai = res; ai != NULL; ai = ai->ai_next) {
		/* coverity[overwrite_var] */
		sock = socket (ai->ai_family, ai->ai_socktype, ai->ai_protocol);
		if (sock < 0) {
			error = errno;
		} else if (connect (sock, ai->ai_addr, ai->ai_addrlen) < 0) {
			error = errno;
			close (sock);
		} else {
			error = 0;
			if (asprintf (&url, "ldap://%s", canonical_host) < 0)
				return_val_if_reached (NULL);
			rc = ldap_init_fd (sock, 1, url, &ldap);
			free (url);

			if (rc != LDAP_SUCCESS) {
				_adcli_err ("Couldn't initialize LDAP connection: %s:",
				            ldap_err2string (rc));
				break;
			}
		}
	}

	if (!ldap && error)
		_adcli_err ("Couldn't connect to host: %s: %s", host, strerror (error));

	freeaddrinfo (res);
	/* coverity[leaked_handle] - the socket is carried inside the ldap struct */
	return ldap;
}

static adcli_result
connect_and_lookup_naming (adcli_conn *conn,
                           adcli_disco *disco)
{
	char *canonical_host;
	LDAPMessage *results;
	adcli_result res;
	LDAP *ldap;
	int ret;
	int ver;

	char *attrs[] = {
		"defaultNamingContext",
		"configurationNamingContext",
		"supportedCapabilities",
		NULL
	};

	assert (conn->ldap == NULL);

	canonical_host = disco->host_name;
	if (!canonical_host)
		canonical_host = disco->host_addr;

	ldap = connect_to_address (disco->host_addr, canonical_host);
	if (ldap == NULL)
		return ADCLI_ERR_DIRECTORY;

	ver = LDAP_VERSION3;
	if (ldap_set_option (ldap, LDAP_OPT_PROTOCOL_VERSION, &ver) != 0)
		return_unexpected_if_reached ();

	if (ldap_set_option (ldap, LDAP_OPT_REFERRALS, LDAP_OPT_OFF) != 0)
		return_unexpected_if_reached ();

	/* Don't force GSSAPI to use reverse DNS */
	if (ldap_set_option (ldap, LDAP_OPT_X_SASL_NOCANON, LDAP_OPT_ON) != 0)
		return_unexpected_if_reached ();

	/*
	 * We perform this lookup whether or not we want to lookup the
	 * naming context, as it also connects to the LDAP server.
	 */
	ret = ldap_search_ext_s (ldap, "", LDAP_SCOPE_BASE, "(objectClass=*)",
	                         attrs, 0, NULL, NULL, NULL, -1, &results);
	if (ret != LDAP_SUCCESS) {
		res = _adcli_ldap_handle_failure (ldap, ADCLI_ERR_DIRECTORY,
		                                  "Couldn't connect to LDAP server: %s", disco->host_addr);
		ldap_unbind_ext_s (ldap, NULL, NULL);
		return res;
	}

	if (conn->default_naming_context == NULL) {
		conn->default_naming_context = _adcli_ldap_parse_value (ldap, results,
		                                                        "defaultNamingContext");
	}

	if (conn->configuration_naming_context == NULL) {
		conn->configuration_naming_context = _adcli_ldap_parse_value (ldap, results,
		                                                              "configurationNamingContext");
	}

	if (conn->supported_capabilities == NULL) {
		conn->supported_capabilities = _adcli_ldap_parse_values (ldap, results,
		                                                         "supportedCapabilities");
	}

	ldap_msgfree (results);

	if (conn->default_naming_context == NULL) {
		_adcli_err ("No valid LDAP naming context on domain controller: %s", disco->host_addr);
		ldap_unbind_ext_s (ldap, NULL, NULL);
		return ADCLI_ERR_DIRECTORY;
	}

	if (conn->configuration_naming_context == NULL) {
		if (asprintf (&conn->configuration_naming_context,
		              "CN=Configuration,%s", conn->default_naming_context))
			return_unexpected_if_reached ();
	}

	conn->ldap = ldap;

	free (conn->canonical_host);
	conn->canonical_host = strdup (canonical_host);
	return_unexpected_if_fail (conn->canonical_host != NULL);

	adcli_conn_set_domain_controller (conn, disco->host_addr);

	return ADCLI_SUCCESS;
}

static int
sasl_interact (LDAP *ld,
               unsigned flags,
               void *defaults,
               void *interact)
{
	sasl_interact_t *in = (sasl_interact_t *)interact;
	return_val_if_fail (ld != NULL, LDAP_PARAM_ERROR);

	while (in->id != SASL_CB_LIST_END) {
		switch (in->id) {
		case SASL_CB_GETREALM:
		case SASL_CB_USER:
		case SASL_CB_PASS:
			if (in->defresult)
				in->result = in->defresult;
			else
				in->result = "";
			in->len = strlen (in->result);
			break;
		case SASL_CB_AUTHNAME:
			if (in->defresult)
				in->result = in->defresult;
			else
				in->result = "";
			in->len = strlen (in->result);
			break;
		case SASL_CB_NOECHOPROMPT:
		case SASL_CB_ECHOPROMPT:
			return LDAP_UNAVAILABLE;
		}
		in++;
	}

	return LDAP_SUCCESS;
}

static adcli_disco *
desperate_for_disco (adcli_conn *conn)
{
	adcli_disco *disco;

	if (!conn->domain_name || !conn->domain_controller)
		return NULL;

	disco = calloc (1, sizeof (adcli_disco));
	return_val_if_fail (disco != NULL, NULL);

	disco->domain = strdup (conn->domain_name);
	return_val_if_fail (disco->domain != NULL, NULL);

	disco->host_addr = strdup (conn->domain_controller);
	return_val_if_fail (disco->host_addr, NULL);

	disco->host_name = strdup (conn->domain_controller);
	return_val_if_fail (disco->host_name, NULL);

	assert (adcli_disco_usable (disco) != ADCLI_DISCO_UNUSABLE);
	return disco;
}

static adcli_result
connect_to_directory (adcli_conn *conn)
{
	adcli_result res = ADCLI_ERR_UNEXPECTED;
	adcli_disco *disco;
	int had_any = 0;

	if (conn->ldap)
		return ADCLI_SUCCESS;

	disco_dance_if_necessary (conn);

	if (!conn->domain_disco)
		conn->domain_disco = desperate_for_disco (conn);

	for (disco = conn->domain_disco; disco != NULL; disco = disco->next) {
		if (!adcli_disco_usable (disco))
			continue;
		res = connect_and_lookup_naming (conn, disco);
		if (res == ADCLI_SUCCESS || res == ADCLI_ERR_UNEXPECTED)
			return res;
		had_any = 1;
	}

	if (!had_any) {
		_adcli_err ("Couldn't find usable domain controller to connect to");
		return ADCLI_ERR_CONFIG;
	}

	return res;
}

static adcli_result
authenticate_to_directory (adcli_conn *conn)
{
	OM_uint32 status;
	OM_uint32 minor;
	ber_len_t ssf;
	int ret;

	if (conn->ldap_authenticated)
		return ADCLI_SUCCESS;

	assert (conn->ldap);
	assert (conn->login_ccache_name != NULL);

	/* Sets the credential cache GSSAPI to use (for this thread) */
	status = gss_krb5_ccache_name (&minor, conn->login_ccache_name, NULL);
	return_unexpected_if_fail (status == 0);

	/* Clumsily tell ldap + cyrus-sasl that we want encryption */
	ssf = 1;
	ret = ldap_set_option (conn->ldap, LDAP_OPT_X_SASL_SSF_MIN, &ssf);
	return_unexpected_if_fail (ret == 0);

	ret = ldap_sasl_interactive_bind_s (conn->ldap, NULL, "GSSAPI", NULL, NULL,
	                                    LDAP_SASL_QUIET, sasl_interact, NULL);

	/* Clear the credential cache GSSAPI to use (for this thread) */
	status = gss_krb5_ccache_name (&minor, NULL, NULL);
	return_unexpected_if_fail (status == 0);

	if (ret != 0) {
		return _adcli_ldap_handle_failure (conn->ldap, ADCLI_ERR_CREDENTIALS,
		                                   "Couldn't authenticate to active directory");
	}

	conn->ldap_authenticated = 1;
	return ADCLI_SUCCESS;
}

static void
lookup_short_name (adcli_conn *conn)
{
	char *attrs[] = { "nETBIOSName", NULL, };
	LDAPMessage *results;
	char *partition_dn;
	char *value;
	char *filter;
	int ret;

	free (conn->domain_short);
	conn->domain_short = NULL;

	if (asprintf (&partition_dn, "CN=Partitions,%s", conn->configuration_naming_context) < 0)
		return_if_reached ();

	value = _adcli_ldap_escape_filter (conn->default_naming_context);
	return_if_fail (value != NULL);

	if (asprintf (&filter, "(&(nCName=%s)(nETBIOSName=*))", value) < 0)
		return_if_reached ();

	ret = ldap_search_ext_s (conn->ldap, partition_dn, LDAP_SCOPE_ONELEVEL,
	                         filter, attrs, 0, NULL, NULL, NULL, -1, &results);

	free (partition_dn);
	free (filter);
	free (value);

	if (ret == LDAP_SUCCESS) {
		conn->domain_short = _adcli_ldap_parse_value (conn->ldap, results, "nETBIOSName");
		ldap_msgfree (results);

		if (conn->domain_short)
			_adcli_info ("Looked up short domain name: %s", conn->domain_short);
		else
			_adcli_err ("No short domain name found");
	} else {
		_adcli_ldap_handle_failure (conn->ldap, ADCLI_ERR_DIRECTORY,
		                            "Couldn't lookup domain short name");
	}
}

static void
lookup_domain_sid (adcli_conn *conn)
{
	char *attrs[] = { "objectSid", NULL, };
	LDAPMessage *results;
	int ret;

	free (conn->domain_sid);
	conn->domain_sid = NULL;

	ret = ldap_search_ext_s (conn->ldap, conn->default_naming_context, LDAP_SCOPE_BASE,
	                         NULL, attrs, 0, NULL, NULL, NULL, -1, &results);
	if (ret == LDAP_SUCCESS) {
		conn->domain_sid = _adcli_ldap_parse_sid (conn->ldap, results, "objectSid");
		ldap_msgfree (results);

		if (conn->domain_sid)
			_adcli_info ("Looked up domain SID: %s", conn->domain_sid);
		else
			_adcli_err ("No domain SID found");
	} else {
		_adcli_ldap_handle_failure (conn->ldap, ADCLI_ERR_DIRECTORY,
		                            "Couldn't lookup domain SID");
	}
}

static void
conn_clear_state (adcli_conn *conn)
{
	conn->ldap_authenticated = 0;

	if (conn->ldap)
		ldap_unbind_ext_s (conn->ldap, NULL, NULL);
	conn->ldap = NULL;

	free (conn->canonical_host);
	conn->canonical_host = NULL;

	if (conn->ccache)
		krb5_cc_close (conn->k5, conn->ccache);
	conn->ccache = NULL;

	if (conn->keytab)
		krb5_kt_close (conn->k5, conn->keytab);
	conn->keytab = NULL;

	if (conn->k5)
		krb5_free_context (conn->k5);
	conn->k5 = NULL;
}

adcli_result
adcli_conn_discover (adcli_conn *conn)
{
	adcli_result res = ADCLI_SUCCESS;

	return_unexpected_if_fail (conn != NULL);

	adcli_clear_last_error ();

	/* Basic discovery and figuring out conn params */
	res = ensure_host_fqdn (res, conn);
	res = ensure_domain_and_host (res, conn);
	res = ensure_computer_name (res, conn);
	res = ensure_domain_realm (res, conn);

	return res;
}

adcli_result
adcli_conn_connect (adcli_conn *conn)
{
	adcli_result res = ADCLI_SUCCESS;

	return_unexpected_if_fail (conn != NULL);

	res = adcli_conn_discover (conn);
	if (res != ADCLI_SUCCESS)
		return res;

	/* - Connect to LDAP server */
	res = connect_to_directory (conn);
	if (res != ADCLI_SUCCESS)
		return res;

	/* Guarantee consistency and communication with one dc */
	res = setup_krb5_conf_snippet (conn);
	if (res != ADCLI_SUCCESS)
		return res;

	return_unexpected_if_fail (conn->k5 == NULL);
	res = _adcli_krb5_init_context (&conn->k5);
	if (res != ADCLI_SUCCESS)
		return res;

	/* Login with admin credentials now, setup login ccache */
	res = prep_kerberos_and_kinit (conn);
	if (res != ADCLI_SUCCESS)
		return res;

	/* - And finally authenticate */
	res = authenticate_to_directory (conn);
	if (res != ADCLI_SUCCESS)
		return res;

	lookup_short_name (conn);
	lookup_domain_sid (conn);
	return ADCLI_SUCCESS;
}

adcli_conn *
adcli_conn_new (const char *domain_name)
{
	adcli_conn *conn;

	conn = calloc (1, sizeof (adcli_conn));
	return_val_if_fail (conn != NULL, NULL);

	conn->refs = 1;
	conn->logins_allowed = ADCLI_LOGIN_COMPUTER_ACCOUNT | ADCLI_LOGIN_USER_ACCOUNT;
	adcli_conn_set_domain_name (conn, domain_name);
	return conn;
}

static void
conn_free (adcli_conn *conn)
{
	free (conn->domain_name);
	free (conn->domain_realm);
	free (conn->domain_controller);
	free (conn->domain_short);
	free (conn->default_naming_context);
	free (conn->configuration_naming_context);
	_adcli_strv_free (conn->supported_capabilities);

	free (conn->computer_name);
	free (conn->host_fqdn);
	free (conn->krb5_conf_dir);

	if (conn->krb5_conf_snippet) {
		unlink (conn->krb5_conf_snippet);
		free (conn->krb5_conf_snippet);
	}

	adcli_conn_set_login_user (conn, NULL);
	adcli_conn_set_user_password (conn, NULL);
	adcli_conn_set_password_func (conn, NULL, NULL, NULL);

	conn_clear_state (conn);
	no_more_disco (conn);

	free (conn);
}

adcli_conn *
adcli_conn_ref (adcli_conn *conn)
{
	return_val_if_fail (conn != NULL, NULL);

	conn->refs++;
	return conn;
}

void
adcli_conn_unref (adcli_conn *conn)
{
	if (conn == NULL)
		return;

	if (--(conn->refs) > 0)
		return;

	conn_free (conn);
}

const char *
adcli_conn_get_host_fqdn (adcli_conn *conn)
{
	return_val_if_fail (conn != NULL, NULL);
	return conn->host_fqdn;
}

void
adcli_conn_set_host_fqdn (adcli_conn *conn,
                          const char *value)
{
	return_if_fail (conn != NULL);
	_adcli_str_set (&conn->host_fqdn, value);
}

const char *
adcli_conn_get_computer_name (adcli_conn *conn)
{
	return_val_if_fail (conn != NULL, NULL);
	return conn->computer_name;
}

void
adcli_conn_set_computer_name (adcli_conn *conn,
                              const char *value)
{
	return_if_fail (conn != NULL);
	_adcli_str_set (&conn->computer_name, value);
}

const char *
adcli_conn_get_computer_password (adcli_conn *conn)
{
	return_val_if_fail (conn != NULL, NULL);
	return conn->computer_password;
}

void
adcli_conn_set_computer_password (adcli_conn *conn,
                                  const char *password)
{
	char *newval = NULL;

	return_if_fail (conn != NULL);

	if (password) {
		newval = strdup (password);
		return_if_fail (newval != NULL);
	}

	if (conn->computer_password)
		_adcli_password_free (conn->computer_password);

	conn->computer_password = newval;
}

const char *
adcli_conn_get_domain_name (adcli_conn *conn)
{
	return_val_if_fail (conn != NULL, NULL);
	return conn->domain_name;
}

void
adcli_conn_set_domain_name (adcli_conn *conn,
                            const char *value)
{
	return_if_fail (conn != NULL);
	_adcli_str_set (&conn->domain_name, value);
	no_more_disco (conn);
}

const char *
adcli_conn_get_domain_realm (adcli_conn *conn)
{
	return_val_if_fail (conn != NULL, NULL);
	return conn->domain_realm;
}

void
adcli_conn_set_domain_realm (adcli_conn *conn,
                             const char *value)
{
	return_if_fail (conn != NULL);
	_adcli_str_set (&conn->domain_realm, value);
	no_more_disco (conn);
}

const char *
adcli_conn_get_domain_controller (adcli_conn *conn)
{
	return_val_if_fail (conn != NULL, NULL);
	return conn->domain_controller;
}

void
adcli_conn_set_domain_controller (adcli_conn *conn,
                                  const char *value)
{
	return_if_fail (conn != NULL);
	_adcli_str_set (&conn->domain_controller, value);
	no_more_disco (conn);
}

const char *
adcli_conn_get_domain_short (adcli_conn *conn)
{
	return_val_if_fail (conn != NULL, NULL);
	return conn->domain_short;
}

const char *
adcli_conn_get_domain_sid (adcli_conn *conn)
{
	return_val_if_fail (conn != NULL, NULL);
	return conn->domain_sid;
}


LDAP *
adcli_conn_get_ldap_connection (adcli_conn *conn)
{
	return_val_if_fail (conn != NULL, NULL);
	return conn->ldap;
}

krb5_context
adcli_conn_get_krb5_context (adcli_conn *conn)
{
	return_val_if_fail (conn != NULL, NULL);
	return_val_if_fail (conn->k5 != NULL, NULL);
	return conn->k5;
}

void
adcli_conn_set_krb5_context (adcli_conn *conn,
                             krb5_context k5)
{
	return_if_fail (conn != NULL);

	if (conn->k5 != NULL) {
		krb5_free_context (conn->k5);
	}

	conn->k5 = k5;
}

const char *
adcli_conn_get_login_user (adcli_conn *conn)
{
	return_val_if_fail (conn != NULL, NULL);
	return conn->user_name;
}

void
adcli_conn_set_login_user (adcli_conn *conn,
                           const char *value)
{
	return_if_fail (conn != NULL);
	_adcli_str_set (&conn->user_name, value);
}

const char *
adcli_conn_get_user_password (adcli_conn *conn)
{
	return_val_if_fail (conn != NULL, NULL);
	return conn->user_password;
}

void
adcli_conn_set_user_password (adcli_conn *conn,
                               const char *value)
{
	return_if_fail (conn != NULL);
	_adcli_str_set (&conn->user_password, value);
}

void
adcli_conn_set_password_func (adcli_conn *conn,
                              adcli_password_func password_func,
                              void *data,
                              adcli_destroy_func destroy_data)
{
	return_if_fail (conn != NULL);

	if (conn->password_destroy)
		(conn->password_destroy) (conn->password_data);
	conn->password_func = password_func;
	conn->password_data = data;
	conn->password_destroy = destroy_data;
}

adcli_login_type
adcli_conn_get_login_type (adcli_conn *conn)
{
	return_val_if_fail (conn != NULL, ADCLI_LOGIN_UNKNOWN);
	return conn->login_type;
}

adcli_login_type
adcli_conn_get_allowed_login_types (adcli_conn *conn)
{
	return_val_if_fail (conn != NULL, ADCLI_LOGIN_UNKNOWN);
	return conn->logins_allowed;
}

void
adcli_conn_set_allowed_login_types (adcli_conn *conn,
                                    adcli_login_type types)
{
	return_if_fail (conn != NULL);
	conn->logins_allowed = types;
}

krb5_ccache
adcli_conn_get_login_ccache (adcli_conn *conn)
{
	return_val_if_fail (conn != NULL, NULL);
	return conn->ccache;
}

const char *
adcli_conn_get_login_ccache_name (adcli_conn *conn)
{
	return_val_if_fail (conn != NULL, NULL);
	return conn->login_ccache_name;
}

void
adcli_conn_set_login_ccache_name (adcli_conn *conn,
                                  const char *ccname)
{
	char *newval = NULL;

	return_if_fail (conn != NULL);

	if (ccname) {
		newval = strdup (ccname);
		return_if_fail (newval != NULL);
	}

	if (conn->login_ccache_name) {
		if (conn->login_ccache_name_is_krb5)
			krb5_free_string (conn->k5, conn->login_ccache_name);
		else
			free (conn->login_ccache_name);
	}

	if (conn->ccache) {
		krb5_cc_close (conn->k5, conn->ccache);
		conn->ccache = NULL;
	}

	conn->login_ccache_name = newval;
	conn->login_ccache_name_is_krb5 = 0;
}

const char *
adcli_conn_get_login_keytab_name (adcli_conn *conn)
{
	return_val_if_fail (conn != NULL, NULL);
	return conn->login_keytab_name;
}

void
adcli_conn_set_login_keytab_name (adcli_conn *conn,
                                  const char *ktname)
{
	char *newval = NULL;

	return_if_fail (conn != NULL);

	if (ktname) {
		newval = strdup (ktname);
		return_if_fail (newval != NULL);
	}

	if (conn->login_keytab_name) {
		if (conn->login_keytab_name_is_krb5)
			krb5_free_string (conn->k5, conn->login_keytab_name);
		else
			free (conn->login_keytab_name);
	}

	if (conn->keytab) {
		krb5_kt_close (conn->k5, conn->keytab);
		conn->keytab = NULL;
	}

	conn->login_keytab_name = newval;
	conn->login_keytab_name_is_krb5 = 0;
}

const char *
adcli_conn_get_default_naming_context (adcli_conn *conn)
{
	return conn->default_naming_context;
}

const char *
adcli_conn_get_krb5_conf_dir (adcli_conn *conn)
{
	return_val_if_fail (conn != NULL, NULL);
	return conn->krb5_conf_dir;
}

void
adcli_conn_set_krb5_conf_dir (adcli_conn *conn,
                              const char *value)
{
	return_if_fail (conn != NULL);
	_adcli_str_set (&conn->krb5_conf_dir, value);
}

int
adcli_conn_server_has_capability (adcli_conn *conn,
                                  const char *capability)
{
	int i;

	return_val_if_fail (conn != NULL, 0);
	return_val_if_fail (capability != NULL, 0);

	if (!conn->supported_capabilities)
		return 0;

	for (i = 0; conn->supported_capabilities[i] != NULL; i++) {
		if (strcmp (capability, conn->supported_capabilities[i]) == 0)
			return 1;
	}

	return 0;
}

bool adcli_conn_is_writeable (adcli_conn *conn)
{
	disco_dance_if_necessary (conn);

	if (conn->domain_disco == NULL) {
		return false;
	}

	return ( (conn->domain_disco->flags & ADCLI_DISCO_WRITABLE) != 0);
}
