/* realmd -- Realm configuration service
 *
 * Copyright 2013 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 of the licence or (at
 * your option) any later version.
 *
 * See the included COPYING file for more information.
 *
 * Author: Stef Walter <stefw@redhat.com>
 */

#include "config.h"

#include "realm-dbus-constants.h"
#include "realm-diagnostics.h"
#include "realm-disco-mscldap.h"
#include "realm-disco-rootdse.h"
#include "realm-ldap.h"

#include <glib/gi18n.h>

#include <resolv.h>

#define DOMAIN_NAME_VALID "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-."

typedef struct _Closure Closure;

struct _Closure {
	RealmDisco *disco;
	GSource *source;
	GDBusMethodInvocation *invocation;

	gchar *default_naming_context;
	gint msgid;

	gboolean (* request) (GTask *task,
	                      Closure *clo,
	                      LDAP *ldap);

	gboolean (* result) (GTask *task,
	                     Closure *clo,
	                     LDAP *ldap,
	                     LDAPMessage *msg);
};

static void
closure_free (gpointer data)
{
	Closure *clo = data;

	ldap_memfree (clo->default_naming_context);

	g_source_destroy (clo->source);
	g_source_unref (clo->source);
	g_clear_object (&clo->invocation);
	realm_disco_unref (clo->disco);
	g_free (clo);
}

static gboolean
entry_has_attribute (LDAP *ldap,
                     LDAPMessage *entry,
                     const gchar *field,
                     const gchar *value)
{
	struct berval **bvs = NULL;
	gboolean has = FALSE;
	gsize len;
	int i;

	len = strlen (value);
	if (entry != NULL)
		bvs = ldap_get_values_len (ldap, entry, field);

	for (i = 0; bvs && bvs[i]; i++) {
		if (bvs[i]->bv_len == len &&
		    memcmp (bvs[i]->bv_val, value, len) == 0) {
			has = TRUE;
			break;
		}
	}

	ldap_value_free_len (bvs);

	return has;
}

static gchar *
entry_get_attribute (LDAP *ldap,
                     LDAPMessage *entry,
                     const gchar *field,
                     const gchar *valid)
{
	struct berval **bvs = NULL;
	gchar *value = NULL;

	if (entry != NULL)
		bvs = ldap_get_values_len (ldap, entry, field);

	if (bvs && bvs[0]) {
		value = g_strndup (bvs[0]->bv_val, bvs[0]->bv_len);
		if (valid) {
		       if (strspn (value, valid) != bvs[0]->bv_len) {
			       g_free (value);
			       g_message ("Invalid value in LDAP %s field", field);
			       value = NULL;
		       }
		}
	}

	ldap_value_free_len (bvs);

	return value;
}

static gboolean
search_ldap (GTask *task,
             Closure *clo,
             LDAP *ldap,
             const gchar *base,
             int scope,
             const char *filter,
             const char **attrs)
{
	GError *error = NULL;
	int rc;

	if (!filter)
		filter = "(objectClass=*)";

	g_debug ("Searching %s for %s", base, filter);
	rc = ldap_search_ext (ldap, base, scope, filter,
	                      (char **)attrs, 0, NULL, NULL, NULL, -1, &clo->msgid);

	if (rc != 0) {
		realm_ldap_set_error (&error, ldap, rc);
		g_task_return_error (task, error);
		return FALSE;
	}

	return TRUE;
}

static gboolean
result_krb_realm (GTask *task,
                  Closure *clo,
                  LDAP *ldap,
                  LDAPMessage *message)
{
	LDAPMessage *entry;

	entry = ldap_first_entry (ldap, message);

	g_free (clo->disco->kerberos_realm);
	clo->disco->kerberos_realm = entry_get_attribute (ldap, entry, "cn", DOMAIN_NAME_VALID);

	g_debug ("Found realm: %s", clo->disco->kerberos_realm);

	/* All done */
	g_task_return_boolean (task, TRUE);
	return FALSE;
}

static gboolean
request_krb_realm (GTask *task,
                   Closure *clo,
                   LDAP *ldap)
{
	const char *attrs[] = { "cn", NULL };

	clo->request = NULL;
	clo->result = result_krb_realm;

	return search_ldap (task, clo, ldap, clo->default_naming_context,
	                    LDAP_SCOPE_SUB, "(objectClass=krbRealmContainer)", attrs);
}

static gboolean
result_domain_info (GTask *task,
                    Closure *clo,
                    LDAP *ldap,
                    LDAPMessage *message)
{
	LDAPMessage *entry;
	struct berval **bvs;

	entry = ldap_first_entry (ldap, message);

	/* If we can't retrieve this, then nothing more to do */
	if (entry == NULL) {
		g_debug ("Couldn't read default naming context");
		g_task_return_new_error (task, REALM_LDAP_ERROR, LDAP_NO_SUCH_OBJECT,
		                         "Couldn't lookup domain name on LDAP server");
		return FALSE;
	}

	/* What kind of server is it? */
	clo->disco->server_software = NULL;
	bvs = ldap_get_values_len (ldap, entry, "info");
	if (bvs && bvs[0] && bvs[0]->bv_len >= 3) {
		if (g_ascii_strncasecmp (bvs[0]->bv_val, "IPA", 3) == 0)
			clo->disco->server_software = REALM_DBUS_IDENTIFIER_IPA;
	}
	ldap_value_free_len (bvs);

	if (clo->disco->server_software)
		g_debug ("Got server software: %s", clo->disco->server_software);

	/* What is the domain name? */
	g_free (clo->disco->domain_name);
	clo->disco->domain_name = entry_get_attribute (ldap, entry, "associatedDomain", DOMAIN_NAME_VALID);

	g_debug ("Got associatedDomain: %s", clo->disco->domain_name);

	/* Next search for Kerberos container */
	clo->request = request_krb_realm;
	clo->result = NULL;
	return TRUE;
}

static gboolean
request_domain_info (GTask *task,
                     Closure *clo,
                     LDAP *ldap)
{
	const char *attrs[] = { "info", "associatedDomain", NULL };

	clo->request = NULL;
	clo->result = result_domain_info;

	return search_ldap (task, clo, ldap, clo->default_naming_context,
	                    LDAP_SCOPE_BASE, NULL, attrs);
}

static void
on_udp_mscldap_complete (GObject *source,
                         GAsyncResult *result,
                         gpointer user_data)
{
	GTask *task = G_TASK (user_data);
	Closure *clo = g_task_get_task_data (task);
	GError *error = NULL;

	realm_disco_unref (clo->disco);
	clo->disco = realm_disco_mscldap_finish (result, &error);

	if (error != NULL) {
		g_debug ("Failed UDP Netlogon response: %s", error->message);
		g_task_return_error (task, error);
	} else {
		g_debug ("Received UDP Netlogon response");
		g_task_return_boolean (task, TRUE);
	}

	g_object_unref (task);
}

static gboolean
result_netlogon (GTask *task,
                 Closure *clo,
                 LDAP *ldap,
                 LDAPMessage *message)
{
	GError *error = NULL;

	if (realm_disco_mscldap_result (ldap, message, clo->disco, &error)) {
		g_debug ("Received TCP Netlogon response");
		g_task_return_boolean (task, TRUE);
	} else {
		g_debug ("Failed TCP Netlogon response: %s", error->message);
		g_task_return_error (task, error);
	}

	/* All done */
	return FALSE;
}

static gboolean
request_netlogon (GTask *task,
                  Closure *clo,
                  LDAP *ldap)
{
	GError *error = NULL;

	g_debug ("Sending TCP Netlogon request");

	if (!realm_disco_mscldap_request (ldap, &clo->msgid, &error)) {
		g_task_return_error (task, error);
		return FALSE;
	}

	clo->request = NULL;
	clo->result = result_netlogon;
	return TRUE;
}

static gboolean
result_root_dse (GTask *task,
                 Closure *clo,
                 LDAP *ldap,
                 LDAPMessage *message)
{
	GInetSocketAddress *inet;
	LDAPMessage *entry;
	gchar *string;

	entry = ldap_first_entry (ldap, message);

	/* Parse out the default naming context */
	clo->default_naming_context = entry_get_attribute (ldap, entry, "defaultNamingContext", NULL);

	g_debug ("Got defaultNamingContext: %s", clo->default_naming_context);

	/* This means that this is an Active Directory server */
	if (entry_has_attribute (ldap, entry, "supportedCapabilities",
	                         "1.2.840.113556.1.4.800")) {

		/* This means that this is Windows 2003+ */
		if (entry_has_attribute (ldap, entry, "supportedCapabilities",
		                         "1.2.840.113556.1.4.1670")) {

			/*
			 * Do a TCP NetLogon request since doing this over
			 * TCP is supported, and we already have a connection
			 */
			clo->request = request_netlogon;
			clo->result = NULL;
			return TRUE;

		/* Prior to Windows 2003 we have to use UDP for netlogon lookup */
		} else {
			inet = G_INET_SOCKET_ADDRESS (clo->disco->server_address);
			string = g_inet_address_to_string (g_inet_socket_address_get_address (inet));
			realm_diagnostics_info (clo->invocation, "Sending MS-CLDAP ping to: %s", string);
			g_free (string);

			realm_disco_mscldap_async (clo->disco->server_address, G_SOCKET_PROTOCOL_UDP,
			                           clo->disco->explicit_server, g_task_get_cancellable (task),
			                           on_udp_mscldap_complete, g_object_ref (task));

			/* Disconnect from TCP at this point */
			return FALSE;
		}

	/* Not an Active Directory server, check for IPA */
	} else {

		if (clo->default_naming_context == NULL) {
			g_task_return_new_error (task, REALM_LDAP_ERROR, LDAP_NO_SUCH_OBJECT,
			                         "Couldn't find default naming context on LDAP server");
			return FALSE;
		}

		/* Next search for IPA field */
		clo->request = request_domain_info;
		clo->result = NULL;
		return TRUE;
	}
}

static gboolean
request_root_dse (GTask *task,
                  Closure *clo,
                  LDAP *ldap)
{
	const char *attrs[] = { "defaultNamingContext", "supportedCapabilities", NULL };

	clo->request = NULL;
	clo->result = result_root_dse;

	return search_ldap (task, clo, ldap, "", LDAP_SCOPE_BASE, NULL, attrs);
}

static GIOCondition
on_ldap_io (LDAP *ldap,
            GIOCondition cond,
            gpointer user_data)
{
	GTask *task = G_TASK (user_data);
	Closure *clo = g_task_get_task_data (task);
	struct timeval tvpoll = { 0, 0 };
	LDAPMessage *message;
	GError *error = NULL;
	gboolean ret;

	/* Some failure */
	if (cond & G_IO_ERR) {
		realm_ldap_set_error (&error, ldap, 0);
		g_task_return_error (task, error);
		return G_IO_NVAL;
	}

	/* Ready to get a result */
	if (cond & G_IO_IN && clo->result != NULL) {
		switch (ldap_result (ldap, clo->msgid, 0, &tvpoll, &message)) {
		case LDAP_RES_INTERMEDIATE:
		case LDAP_RES_SEARCH_REFERENCE:
			ret = TRUE;
			break;
		case -1:
			realm_ldap_set_error (&error, ldap, -1);
			g_task_return_error (task, error);
			ret = FALSE;
			break;
		case 0:
			ret = TRUE;
			break;
		default:
			ret = clo->result (task, clo, ldap, message);
			ldap_msgfree (message);
			break;
		}

		if (!ret)
			return G_IO_NVAL;
	}

	if (cond & G_IO_OUT && clo->request != NULL) {
		if (!(clo->request) (task, clo, ldap))
			return G_IO_NVAL;
	}

	return (clo->request ? G_IO_OUT : 0) |
	       (clo->result ? G_IO_IN : 0);
}

void
realm_disco_rootdse_async (GSocketAddress *address,
                           const gchar *explicit_server,
                           GDBusMethodInvocation *invocation,
                           GCancellable *cancellable,
                           GAsyncReadyCallback callback,
                           gpointer user_data)
{
	GTask *task;
	Closure *clo;

	g_return_if_fail (address != NULL);

	task = g_task_new (NULL, cancellable, callback, user_data);
	clo = g_new0 (Closure, 1);
	clo->disco = realm_disco_new (NULL);
	clo->disco->explicit_server = g_strdup (explicit_server);
	clo->disco->server_address = g_object_ref (address);

	clo->invocation = invocation ? g_object_ref (invocation) : NULL;
	clo->request = request_root_dse;
	g_task_set_task_data (task, clo, closure_free);

	clo->source = realm_ldap_connect_anonymous (address, G_SOCKET_PROTOCOL_TCP,
	                                            cancellable);
	g_source_set_callback (clo->source, (GSourceFunc)on_ldap_io,
	                       g_object_ref (task), g_object_unref);
	g_source_attach (clo->source, g_task_get_context (task));

	g_object_unref (task);
}

RealmDisco *
realm_disco_rootdse_finish (GAsyncResult *result,
                            GError **error)
{
	Closure *clo;
	RealmDisco *disco;

	g_return_val_if_fail (g_task_is_valid (result, NULL), NULL);
	g_return_val_if_fail (error == NULL || *error == NULL, NULL);

	if (!g_task_propagate_boolean (G_TASK (result), error))
		return FALSE;

	clo = g_task_get_task_data (G_TASK (result));
	disco = clo->disco;
	clo->disco = NULL;

	/* Should have been set above */
	g_return_val_if_fail (disco->domain_name, NULL);

	if (!disco->kerberos_realm)
		disco->kerberos_realm = g_ascii_strup (disco->domain_name, -1);

	return disco;
}
