/*
   Unix SMB/CIFS implementation.

   Start MIT krb5kdc server within Samba AD

   Copyright (c) 2014-2016 Andreas Schneider <asn@samba.org>

   This program 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 3 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 General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "includes.h"
#include "talloc.h"
#include "tevent.h"
#include "system/filesys.h"
#include "lib/param/param.h"
#include "lib/util/samba_util.h"
#include "source4/samba/service.h"
#include "source4/samba/process_model.h"
#include "kdc/kdc-service-mit.h"
#include "dynconfig.h"
#include "libds/common/roles.h"
#include "lib/socket/netif.h"
#include "samba/session.h"
#include "dsdb/samdb/samdb.h"
#include "kdc/samba_kdc.h"
#include "kdc/kdc-server.h"
#include "kdc/kpasswd-service.h"
#include <kadm5/admin.h>
#include <kdb.h>

#include "source4/kdc/mit_kdc_irpc.h"

#undef DBGC_CLASS
#define DBGC_CLASS DBGC_KERBEROS

/* PROTOTYPES */
static void mitkdc_server_done(struct tevent_req *subreq);

static int kdc_server_destroy(struct kdc_server *kdc)
{
	if (kdc->private_data != NULL) {
		kadm5_destroy(kdc->private_data);
	}

	return 0;
}

static NTSTATUS startup_kpasswd_server(TALLOC_CTX *mem_ctx,
				       struct kdc_server *kdc,
				       struct loadparm_context *lp_ctx,
				       struct interface *ifaces)
{
	int num_interfaces;
	int i;
	TALLOC_CTX *tmp_ctx;
	NTSTATUS status = NT_STATUS_UNSUCCESSFUL;
	uint16_t kpasswd_port;
	bool done_wildcard = false;
	bool ok;

	kpasswd_port = lpcfg_kpasswd_port(lp_ctx);
	if (kpasswd_port == 0) {
		return NT_STATUS_OK;
	}

	tmp_ctx = talloc_named_const(mem_ctx, 0, "kpasswd");
	if (tmp_ctx == NULL) {
		return NT_STATUS_NO_MEMORY;
	}

	num_interfaces = iface_list_count(ifaces);

	ok = lpcfg_bind_interfaces_only(lp_ctx);
	if (!ok) {
		int num_binds = 0;
		char **wcard;

		wcard = iface_list_wildcard(tmp_ctx);
		if (wcard == NULL) {
			status = NT_STATUS_NO_MEMORY;
			goto out;
		}

		for (i = 0; wcard[i] != NULL; i++) {
			status = kdc_add_socket(kdc,
						kdc->task->model_ops,
						"kpasswd",
						wcard[i],
						kpasswd_port,
						kpasswd_process,
						false);
			if (NT_STATUS_IS_OK(status)) {
				num_binds++;
			}
		}
		talloc_free(wcard);

		if (num_binds == 0) {
			status = NT_STATUS_INVALID_PARAMETER_MIX;
			goto out;
		}

		done_wildcard = true;
	}

	for (i = 0; i < num_interfaces; i++) {
		const char *address = talloc_strdup(tmp_ctx, iface_list_n_ip(ifaces, i));

		status = kdc_add_socket(kdc,
					kdc->task->model_ops,
					"kpasswd",
					address,
					kpasswd_port,
					kpasswd_process,
					done_wildcard);
		if (NT_STATUS_IS_OK(status)) {
			goto out;
		}
	}

out:
	talloc_free(tmp_ctx);
	return status;
}

/*
 * Startup a copy of the krb5kdc as a child daemon
 */
NTSTATUS mitkdc_task_init(struct task_server *task)
{
	struct tevent_req *subreq;
	const char * const *kdc_cmd;
	struct interface *ifaces;
	char *kdc_config = NULL;
	struct kdc_server *kdc;
	krb5_error_code code;
	NTSTATUS status;
	kadm5_ret_t ret;
	kadm5_config_params config;
	void *server_handle;
	int dbglvl = 0;

	task_server_set_title(task, "task[mitkdc_parent]");

	switch (lpcfg_server_role(task->lp_ctx)) {
	case ROLE_STANDALONE:
		task_server_terminate(task,
				      "The KDC is not required in standalone "
				      "server configuration, terminate!",
				      false);
		return NT_STATUS_INVALID_DOMAIN_ROLE;
	case ROLE_DOMAIN_MEMBER:
		task_server_terminate(task,
				      "The KDC is not required in member "
				      "server configuration",
				      false);
		return NT_STATUS_INVALID_DOMAIN_ROLE;
	case ROLE_ACTIVE_DIRECTORY_DC:
		/* Yes, we want to start the KDC */
		break;
	}

	/* Load interfaces for kpasswd */
	load_interface_list(task, task->lp_ctx, &ifaces);
	if (iface_list_count(ifaces) == 0) {
		task_server_terminate(task,
				      "KDC: no network interfaces configured",
				      false);
		return NT_STATUS_UNSUCCESSFUL;
	}

	kdc_config = talloc_asprintf(task,
				     "%s/kdc.conf",
				     lpcfg_private_dir(task->lp_ctx));
	if (kdc_config == NULL) {
		task_server_terminate(task,
				      "KDC: no memory",
				      false);
		return NT_STATUS_NO_MEMORY;
	}
	setenv("KRB5_KDC_PROFILE", kdc_config, 0);
	TALLOC_FREE(kdc_config);

	dbglvl = debuglevel_get_class(DBGC_KERBEROS);
	if (dbglvl >= 10) {
		char *kdc_trace_file = talloc_asprintf(task,
						       "%s/mit_kdc_trace.log",
						       get_dyn_LOGFILEBASE());
		if (kdc_trace_file == NULL) {
			task_server_terminate(task,
					"KDC: no memory",
					false);
			return NT_STATUS_NO_MEMORY;
		}

		setenv("KRB5_TRACE", kdc_trace_file, 1);
	}

	/* start it as a child process */
	kdc_cmd = lpcfg_mit_kdc_command(task->lp_ctx);

	subreq = samba_runcmd_send(task,
				   task->event_ctx,
				   timeval_zero(),
				   1, /* stdout log level */
				   0, /* stderr log level */
				   kdc_cmd,
				   "-n", /* Don't go into background */
#if 0
				   "-w 2", /* Start two workers */
#endif
				   NULL);
	if (subreq == NULL) {
		DBG_ERR("Failed to start MIT KDC as child daemon\n");

		task_server_terminate(task,
				      "Failed to startup mitkdc task",
				      true);
		return NT_STATUS_INTERNAL_ERROR;
	}

	tevent_req_set_callback(subreq, mitkdc_server_done, task);

	DBG_INFO("Started krb5kdc process\n");

	status = samba_setup_mit_kdc_irpc(task);
	if (!NT_STATUS_IS_OK(status)) {
		task_server_terminate(task,
				      "Failed to setup kdc irpc service",
				      true);
	}

	DBG_INFO("Started irpc service for kdc_server\n");

	kdc = talloc_zero(task, struct kdc_server);
	if (kdc == NULL) {
		task_server_terminate(task, "KDC: Out of memory", true);
		return NT_STATUS_NO_MEMORY;
	}
	talloc_set_destructor(kdc, kdc_server_destroy);

	kdc->task = task;

	kdc->base_ctx = talloc_zero(kdc, struct samba_kdc_base_context);
	if (kdc->base_ctx == NULL) {
		task_server_terminate(task, "KDC: Out of memory", true);
		return NT_STATUS_NO_MEMORY;
	}

	kdc->base_ctx->ev_ctx = task->event_ctx;
	kdc->base_ctx->lp_ctx = task->lp_ctx;

	initialize_krb5_error_table();

	code = smb_krb5_init_context(kdc,
				     kdc->task->lp_ctx,
				     &kdc->smb_krb5_context);
	if (code != 0) {
		task_server_terminate(task,
				      "KDC: Unable to initialize krb5 context",
				      true);
		return NT_STATUS_INTERNAL_ERROR;
	}

	code = kadm5_init_krb5_context(&kdc->smb_krb5_context->krb5_context);
	if (code != 0) {
		task_server_terminate(task,
				      "KDC: Unable to init kadm5 krb5_context",
				      true);
		return NT_STATUS_INTERNAL_ERROR;
	}

	ZERO_STRUCT(config);
	config.mask = KADM5_CONFIG_REALM;
	config.realm = discard_const_p(char, lpcfg_realm(kdc->task->lp_ctx));

	ret = kadm5_init(kdc->smb_krb5_context->krb5_context,
			 discard_const_p(char, "kpasswd"),
			 NULL, /* pass */
			 discard_const_p(char, "kpasswd"),
			 &config,
			 KADM5_STRUCT_VERSION,
			 KADM5_API_VERSION_4,
			 NULL,
			 &server_handle);
	if (ret != 0) {
		task_server_terminate(task,
				      "KDC: Initialize kadm5",
				      true);
		return NT_STATUS_INTERNAL_ERROR;
	}
	kdc->private_data = server_handle;

	code = krb5_db_register_keytab(kdc->smb_krb5_context->krb5_context);
	if (code != 0) {
		task_server_terminate(task,
				      "KDC: Unable to KDB",
				      true);
		return NT_STATUS_INTERNAL_ERROR;
	}

	kdc->kpasswd_keytab_name = talloc_asprintf(kdc, "KDB:");
	if (kdc->kpasswd_keytab_name == NULL) {
		task_server_terminate(task,
				      "KDC: Out of memory",
				      true);
		return NT_STATUS_NO_MEMORY;
	}

	status = startup_kpasswd_server(kdc,
				    kdc,
				    task->lp_ctx,
				    ifaces);
	if (!NT_STATUS_IS_OK(status)) {
		task_server_terminate(task,
				      "KDC: Unable to start kpasswd server",
				      true);
		return status;
	}

	DBG_INFO("Started kpasswd service for kdc_server\n");

	return NT_STATUS_OK;
}

/*
 * This gets called the kdc exits.
 */
static void mitkdc_server_done(struct tevent_req *subreq)
{
	struct task_server *task =
		tevent_req_callback_data(subreq,
		struct task_server);
	int sys_errno;
	int ret;

	ret = samba_runcmd_recv(subreq, &sys_errno);
	if (ret != 0) {
		DBG_ERR("The MIT KDC daemon died with exit status %d\n",
			sys_errno);
	} else {
		DBG_ERR("The MIT KDC daemon exited normally\n");
	}

	task_server_terminate(task, "mitkdc child process exited", true);
}

/* Called at MIT KRB5 startup - register ourselves as a server service */
NTSTATUS server_service_mitkdc_init(TALLOC_CTX *mem_ctx);

NTSTATUS server_service_mitkdc_init(TALLOC_CTX *mem_ctx)
{
	static const struct service_details details = {
		.inhibit_fork_on_accept = true,
		/*
		 * Need to prevent pre-forking on kdc.
		 * The task_init function is run on the master process only
		 * and the irpc process name is registered in it's event loop.
		 * The child worker processes initialise their event loops on
		 * fork, so are not listening for the irpc event.
		 *
		 * The master process does not wait on that event context
		 * the master process is responsible for managing the worker
		 * processes not performing work.
		 */
		.inhibit_pre_fork = true,
		.task_init = mitkdc_task_init,
		.post_fork = NULL
	};
	return register_server_service(mem_ctx, "kdc", &details);
}
