/* 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-disco-mscldap.h"
#include "realm-ldap.h"
#include "realm-options.h"

#include <glib/gi18n.h>

#include <errno.h>
#include <resolv.h>
#include <unistd.h>

typedef struct {
	gchar *explicit_server;
	GSocketAddress *address;
	GSource *source;
	gint count;
	gint fever_id;
	gint normal_id;
} Closure;

/* Number of rapid requets to do */
#define DISCO_FEVER  4

#ifndef HOST_NAME_MAX
#define HOST_NAME_MAX 255
#endif

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

	g_free (clo->explicit_server);
	g_object_unref (clo->address);
	if (clo->fever_id)
		g_source_remove (clo->fever_id);
	if (clo->normal_id)
		g_source_remove (clo->normal_id);
	g_source_destroy (clo->source);
	g_source_unref (clo->source);
	g_free (clo);
}

static gchar *
explicit_netbios_name (void)
{
	gchar hostname[HOST_NAME_MAX + 1];
	gchar *dot;

	/*
	 * Only return a explicit truncated host name if the
	 * computer host name cannot be made seamlessly translated
	 * to a netbios name due to it's length.
	 *
	 * We would love to leave this responsibility to our lower level
	 * tools, but unfortunately samba doesn't know how to do this
	 * properly, and expects us to set it properly in smb.conf
	 *
	 * In addition sssd falls over if truncation is done. So we have
	 * to tell sssd about it.
	 */

	if (gethostname (hostname, sizeof (hostname)) < 0) {
		g_warning ("Couldn't get the computer host name: %s", g_strerror (errno));
		return NULL;
	}

	dot = strchr (hostname, '.');
	if (dot != NULL)
		dot[0] = '\0';

	if (strlen (hostname) > 15) {
		hostname[15] = '\0';
		return g_ascii_strup (hostname, -1);
	}

	return NULL;
}

static gchar *
get_string (guchar *beg,
            guchar *end,
            guchar **at)
{
	gchar buffer[HOST_NAME_MAX];
	int n;

	n = dn_expand (beg, end, *at, buffer, sizeof (buffer));
	if (n < 0)
		return NULL;

	if (!realm_options_check_domain_name (buffer)) {
		g_message ("received invalid NetLogon string characters");
		return NULL;
	}

	(*at) += n;
	return g_strdup (buffer);
}

static gboolean
parse_string (guchar *beg,
              guchar *end,
              guchar **at,
              gchar **result)
{
	gchar *string;

	g_assert (result);

	string = get_string (beg, end, at);
	if (string == NULL)
		return FALSE;

	g_free (*result);
	*result = string;

	return TRUE;
}

static gboolean
get_32_le (unsigned char **at,
           unsigned char *end,
           unsigned int *val)
{
	unsigned char *p = *at;
	if (p + 4 > end)
		return FALSE;
	*val = p[0] | p[1] << 8 | p[2] << 16 | p[3] << 24;
	(*at) += 4;
	return TRUE;
}

static gboolean
skip_n (unsigned char **at,
        unsigned char *end,
        int n)
{
	if ((*at) + n > end)
		return FALSE;
	(*at) += n;
	return TRUE;
}

static gboolean
parse_netlogon (struct berval **bvs,
                RealmDisco *disco,
                GError **error)
{
	guchar *at, *end, *beg;
	gchar *unused = NULL;
	guint type, flags;
	gboolean success = FALSE;

	if (bvs != NULL && bvs[0] != NULL) {
		beg = (guchar *)bvs[0]->bv_val;
		end = beg + bvs[0]->bv_len;
		at = beg;
		success = TRUE;
	}

	/* domain forest */
	if (!success ||
	    !get_32_le (&at, end, &type) || type != 23 ||
	    !get_32_le (&at, end, &flags) ||
	    !skip_n (&at, end, 16) || /* guid */
	    !parse_string (beg, end, &at, &unused) || /* forest */
	    !parse_string (beg, end, &at, &disco->domain_name) ||
	    !parse_string (beg, end, &at, &disco->netlogon_server_name) ||
	    !parse_string (beg, end, &at, &disco->workgroup) ||
	    !parse_string (beg, end, &at, &unused) || /* shorthost */
	    !parse_string (beg, end, &at, &unused) || /* user */
	    !parse_string (beg, end, &at, &unused) || /* server site */
	    !parse_string (beg, end, &at, &unused)) { /* client site */
		success = FALSE;
	}

	g_free (unused);

	if (!success) {
		g_set_error (error, REALM_LDAP_ERROR, LDAP_PROTOCOL_ERROR,
		             _("Received invalid or unsupported Netlogon data from server"));
		return FALSE;
	}

	disco->server_software = REALM_DBUS_IDENTIFIER_ACTIVE_DIRECTORY;
	disco->explicit_netbios = explicit_netbios_name ();
	disco->kerberos_realm = g_ascii_strup (disco->domain_name, -1);
	return TRUE;
}

gboolean
realm_disco_mscldap_result (LDAP *ldap,
                            LDAPMessage *message,
                            RealmDisco *disco,
                            GError **error)
{
	struct berval **bvs = NULL;
	LDAPMessage *entry;
	gboolean ret;

	entry = ldap_first_entry (ldap, message);
	if (entry != NULL)
		bvs = ldap_get_values_len (ldap, entry, "NetLogon");
	ret = parse_netlogon (bvs, disco, error);
	ldap_value_free_len (bvs);

	return ret;
}

gboolean
realm_disco_mscldap_request (LDAP *ldap,
                             int *msgidp,
                             GError **error)
{
	char *attrs[] = { "NetLogon", NULL };
	int rc;

	rc = ldap_search_ext (ldap, "", LDAP_SCOPE_BASE,
	                      "(&(NtVer=\\06\\00\\00\\00)(AAC=\\00\\00\\00\\00))",
	                      attrs, 0, NULL, NULL, NULL,
	                      -1, msgidp);

	if (rc != LDAP_SUCCESS) {
		realm_ldap_set_error (error, ldap, rc);
		return FALSE;
	}

	return TRUE;
}

static gboolean
on_resend (gpointer user_data)
{
	realm_ldap_set_condition (user_data, G_IO_OUT | G_IO_IN);
	return TRUE;
}

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;
	RealmDisco *disco;
	int msgid;

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

	/* Ready for input */
	if (cond & G_IO_OUT) {
		g_debug ("Sending NetLogon ping");
		if (!realm_disco_mscldap_request (ldap, &msgid, &error)) {
			g_task_return_error (task, error);
			return G_IO_NVAL;
		}

		/* Remove rapid fire after sending a feverish batch */
		if (clo->count++ > DISCO_FEVER && clo->fever_id != 0) {
			g_source_remove (clo->fever_id);
			clo->fever_id = 0;
		}
	}

	/* Ready to get a result */
	if (cond & G_IO_IN) {
		switch (ldap_result (ldap, LDAP_RES_ANY, 0, &tvpoll, &message)) {
		case LDAP_RES_SEARCH_ENTRY:
		case LDAP_RES_SEARCH_RESULT:
			g_debug ("Received response");
			disco = realm_disco_new (NULL);
			disco->server_address = g_object_ref (clo->address);
			if (realm_disco_mscldap_result (ldap, message, disco, &error)) {
				disco->explicit_server = g_strdup (clo->explicit_server);
				g_task_return_pointer (task, disco, realm_disco_unref);
			} else {
				realm_disco_unref (disco);
				g_task_return_error (task, error);
			}
			ldap_msgfree (message);
			return G_IO_NVAL;
		case -1:
			realm_ldap_set_error (&error, ldap, -1);
			g_task_return_error (task, error);
			return G_IO_NVAL;
		case 0:
			break;
		default:
			/* Ignore and keep waiting */
			ldap_msgfree (message);
			break;
		}
	}

	/* Now wait for a response */
	return G_IO_IN;
}

void
realm_disco_mscldap_async (GSocketAddress *address,
                           GSocketProtocol protocol,
                           const gchar *explicit_server,
                           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->explicit_server = g_strdup (explicit_server);
	clo->address = g_object_ref (address);
	g_task_set_task_data (task, clo, closure_free);

	if (protocol == G_SOCKET_PROTOCOL_UDP &&
	    !ldap_is_ldap_url ("cldap://hostname")) {
		g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
		                         _("LDAP on this system does not support UDP connections"));
		g_object_unref (task);
		return;
	}

	clo->source = realm_ldap_connect_anonymous (address, protocol, FALSE, cancellable);
	if (clo->source == NULL) {
		g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_CONNECTED,
		                         _("Failed to setup LDAP connection"));
		g_object_unref (task);
		return;
	}

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

	if (protocol == G_SOCKET_PROTOCOL_UDP) {
		clo->fever_id = g_timeout_add (100, on_resend, clo->source);
		clo->normal_id = g_timeout_add (1000, on_resend, clo->source);
	}

	g_object_unref (task);
}

RealmDisco *
realm_disco_mscldap_finish (GAsyncResult *result,
                            GError **error)
{
	g_return_val_if_fail (g_task_is_valid (result, NULL), NULL);
	g_return_val_if_fail (error == NULL || *error == NULL, NULL);
	return g_task_propagate_pointer (G_TASK (result), error);
}
