/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * Copyright (C) 2008 Imendio AB
 *
 * 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
 * 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, see <https://www.gnu.org/licenses>
 */

#include <config.h>

#include <string.h>

/* Needed on Mac OS X */
#if HAVE_ARPA_NAMESER_COMPAT_H
#include <arpa/nameser_compat.h>
#endif

#include <netinet/in.h>
#include <arpa/nameser.h>
#include <resolv.h>

#include "lm-asyncns-resolver.h"
#include "lm-blocking-resolver.h"
#include "lm-debug.h"
#include "lm-internals.h"
#include "lm-marshal.h"
#include "lm-resolver.h"

#define GET_PRIV(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), LM_TYPE_RESOLVER, LmResolverPriv))

typedef struct LmResolverPriv LmResolverPriv;
struct LmResolverPriv {
    GMainContext       *context;

    LmResolverCallback  callback;
    gpointer            user_data;

    /* -- Properties -- */
    LmResolverType      type;
    gchar              *host;
    guint               port;

    /* For SRV lookups */
    gchar              *domain;
    gchar              *service;
    gchar              *protocol;

    /* The results */
    LmResolverResult    result;
    struct addrinfo    *results;
    struct addrinfo    *current_result;
};

static void     resolver_finalize            (GObject           *object);
static void     resolver_get_property        (GObject           *object,
                                              guint              param_id,
                                              GValue            *value,
                                              GParamSpec        *pspec);
static void     resolver_set_property        (GObject           *object,
                                              guint              param_id,
                                              const GValue      *value,
                                              GParamSpec        *pspec);

G_DEFINE_TYPE (LmResolver, lm_resolver, G_TYPE_OBJECT)

enum {
    PROP_0,
    PROP_CONTEXT,
    PROP_TYPE,
    PROP_HOST,
    PROP_PORT,
    PROP_DOMAIN,
    PROP_SERVICE,
    PROP_PROTOCOL
};

static void
lm_resolver_class_init (LmResolverClass *class)
{
    GObjectClass *object_class = G_OBJECT_CLASS (class);

    object_class->finalize     = resolver_finalize;
    object_class->get_property = resolver_get_property;
    object_class->set_property = resolver_set_property;

    g_object_class_install_property (object_class,
                                     PROP_CONTEXT,
                                     g_param_spec_pointer ("context",
                                                           "Context",
                                                           "Main context to use",
                                                           G_PARAM_READWRITE));

    g_object_class_install_property (object_class,
                                     PROP_TYPE,
                                     g_param_spec_int ("type",
                                                       "Type",
                                                       "Resolver Type",
                                                       LM_RESOLVER_HOST,
                                                       LM_RESOLVER_SRV,
                                                       LM_RESOLVER_HOST,
                                                       G_PARAM_READWRITE));

    g_object_class_install_property (object_class,
                                     PROP_HOST,
                                     g_param_spec_string ("host",
                                                          "Host",
                                                          "Host to lookup",
                                                          NULL,
                                                          G_PARAM_READWRITE));

    g_object_class_install_property (object_class,
                                     PROP_PORT,
                                     g_param_spec_uint ("port",
                                                        "Port",
                                                        "Port number",
                                                        0,
                                                        LM_MAX_PORT,
                                                        0,
                                                        G_PARAM_READWRITE));

    g_object_class_install_property (object_class,
                                     PROP_DOMAIN,
                                     g_param_spec_string ("domain",
                                                          "Domain",
                                                          "Domain to lookup",
                                                          NULL,
                                                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));

    g_object_class_install_property (object_class,
                                     PROP_SERVICE,
                                     g_param_spec_string ("service",
                                                          "Service",
                                                          "Service to lookup",
                                                          NULL,
                                                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));

    g_object_class_install_property (object_class,
                                     PROP_PROTOCOL,
                                     g_param_spec_string ("protocol",
                                                          "Protocol",
                                                          "Protocol for SRV lookup",
                                                          NULL,
                                                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));

    g_type_class_add_private (object_class, sizeof (LmResolverPriv));
}

static void
lm_resolver_init (LmResolver *resolver)
{
    (void) GET_PRIV (resolver);
}

static void
resolver_finalize (GObject *object)
{
    LmResolverPriv *priv;

    priv = GET_PRIV (object);

    g_free (priv->host);
    g_free (priv->domain);
    g_free (priv->service);
    g_free (priv->protocol);

    if (priv->context) {
        g_main_context_unref (priv->context);
    }

    if (priv->results) {
        freeaddrinfo (priv->results);
    }

    (G_OBJECT_CLASS (lm_resolver_parent_class)->finalize) (object);
}

static void
resolver_get_property (GObject    *object,
                       guint       param_id,
                       GValue     *value,
                       GParamSpec *pspec)
{
    LmResolverPriv *priv;

    priv = GET_PRIV (object);

    switch (param_id) {
    case PROP_CONTEXT:
        g_value_set_pointer (value, priv->context);
        break;
    case PROP_TYPE:
        g_value_set_int (value, priv->type);
        break;
    case PROP_HOST:
        g_value_set_string (value, priv->host);
        break;
    case PROP_PORT:
        g_value_set_uint (value, priv->port);
        break;
    case PROP_DOMAIN:
        g_value_set_string (value, priv->domain);
        break;
    case PROP_SERVICE:
        g_value_set_string (value, priv->service);
        break;
    case PROP_PROTOCOL:
        g_value_set_string (value, priv->protocol);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
        break;
    };
}

static void
resolver_set_property (GObject      *object,
                       guint         param_id,
                       const GValue *value,
                       GParamSpec   *pspec)
{
    LmResolverPriv *priv;

    priv = GET_PRIV (object);

    switch (param_id) {
    case PROP_CONTEXT:
        if (priv->context) {
            g_main_context_unref (priv->context);
        }

        priv->context = (GMainContext *) g_value_get_pointer (value);
        g_main_context_ref (priv->context);
        break;
    case PROP_TYPE:
        priv->type = g_value_get_int (value);
        break;
    case PROP_HOST:
        g_free (priv->host);
        priv->host = g_value_dup_string (value);
        break;
    case PROP_PORT:
        priv->port = g_value_get_uint (value);
        break;
    case PROP_DOMAIN:
        g_free (priv->domain);
        priv->domain = g_value_dup_string (value);
        break;
    case PROP_SERVICE:
        g_free (priv->service);
        priv->service = g_value_dup_string (value);
        break;
    case PROP_PROTOCOL:
        g_free (priv->protocol);
        priv->protocol = g_value_dup_string (value);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
        break;
    };
}

LmResolver *
lm_resolver_new (GMainContext *context)
{
    LmResolver *resolver;

#ifdef HAVE_ASYNCNS
    resolver = g_object_new (LM_TYPE_ASYNCNS_RESOLVER, NULL);
#else
    resolver = g_object_new (LM_TYPE_BLOCKING_RESOLVER, NULL);
#endif

    g_object_set (resolver, "context", context, NULL);

    return resolver;
}

static GType
resolver_get_gtype (void)
{
#ifdef HAVE_ASYNCNS
    return LM_TYPE_ASYNCNS_RESOLVER;
#else
    return LM_TYPE_BLOCKING_RESOLVER;
#endif /* HAVE_ASYNCNS */
}

LmResolver *
lm_resolver_new_for_host (const gchar        *host,
                          LmResolverCallback  callback,
                          gpointer            user_data)
{
    LmResolver     *resolver;
    LmResolverPriv *priv;

    g_return_val_if_fail (host != NULL, NULL);
    g_return_val_if_fail (callback != NULL, NULL);

    resolver =  g_object_new (resolver_get_gtype (),
                              "type", LM_RESOLVER_HOST,
                              "host", host,
                              NULL);

    priv = GET_PRIV (resolver);

    priv->callback = callback;
    priv->user_data = user_data;

    return resolver;
}

LmResolver *
lm_resolver_new_for_service (const gchar        *domain,
                             const gchar        *service,
                             const gchar        *protocol,
                             LmResolverCallback  callback,
                             gpointer            user_data)
{
    LmResolver     *resolver;
    LmResolverPriv *priv;

    g_return_val_if_fail (domain != NULL, NULL);
    g_return_val_if_fail (service != NULL, NULL);
    g_return_val_if_fail (protocol != NULL, NULL);
    g_return_val_if_fail (callback != NULL, NULL);

    resolver = g_object_new (resolver_get_gtype (),
                             "type", LM_RESOLVER_SRV,
                             "domain", domain,
                             "service", service,
                             "protocol", protocol,
                             NULL);

    priv = GET_PRIV (resolver);

    priv->callback = callback;
    priv->user_data = user_data;

    return resolver;
}

void
lm_resolver_lookup (LmResolver *resolver)
{
    if (!LM_RESOLVER_GET_CLASS(resolver)) {
        g_assert_not_reached ();
    }

    LM_RESOLVER_GET_CLASS(resolver)->lookup (resolver);
}

void
lm_resolver_cancel (LmResolver *resolver)
{
    if (!LM_RESOLVER_GET_CLASS(resolver)->cancel) {
        g_assert_not_reached ();
    }

    LM_RESOLVER_GET_CLASS(resolver)->cancel (resolver);
}

/* To iterate through the results */
struct addrinfo *
lm_resolver_results_get_next (LmResolver *resolver)
{
    LmResolverPriv  *priv;
    struct addrinfo *ret_val;

    g_return_val_if_fail (LM_IS_RESOLVER (resolver), NULL);

    priv = GET_PRIV (resolver);

skipresult:
    if (!priv->current_result) {
        g_log (LM_LOG_DOMAIN, LM_LOG_LEVEL_VERBOSE,
               "no more results from resolver\n");
        return NULL;
    };

    ret_val = priv->current_result;
    priv->current_result = priv->current_result->ai_next;
    if (ret_val->ai_family != AF_INET) {
        g_log (LM_LOG_DOMAIN, LM_LOG_LEVEL_VERBOSE,
               "skipping non-IPv4 resolver entry\n");
        goto skipresult;
    };

    return ret_val;
}

void
lm_resolver_results_reset (LmResolver *resolver)
{
    LmResolverPriv *priv;

    g_return_if_fail (LM_IS_RESOLVER (resolver));

    priv = GET_PRIV (resolver);

    priv->current_result = priv->results;
}

gchar *
_lm_resolver_create_srv_string (const gchar *domain,
                                const gchar *service,
                                const gchar *protocol)
{
    g_return_val_if_fail (domain != NULL, NULL);
    g_return_val_if_fail (service != NULL, NULL);
    g_return_val_if_fail (protocol != NULL, NULL);

    return g_strdup_printf ("_%s._%s.%s", service, protocol, domain);
}

void
_lm_resolver_set_result (LmResolver       *resolver,
                         LmResolverResult  result,
                         struct addrinfo  *results)
{
    LmResolverPriv *priv;

    g_return_if_fail (LM_IS_RESOLVER (resolver));

    priv = GET_PRIV (resolver);

    priv->result = result;
    priv->results = priv->current_result = results;

    lm_verbose ("Calling resolver callback: %s\n", priv->host);

    priv->callback (resolver, result, priv->user_data);
}

gboolean
_lm_resolver_parse_srv_response (unsigned char  *srv,
                                 int             srv_len,
                                 gchar         **out_server,
                                 guint          *out_port)
{
    int                  qdcount;
    int                  ancount;
    int                  len;
    const unsigned char *pos;
    unsigned char       *end;
    HEADER              *head;
    char                 name[256];
    char                 pref_name[256];
    guint                pref_port = 0;
    guint                pref_prio = 9999;

    pref_name[0] = 0;

    pos = srv + sizeof (HEADER);
    end = srv + srv_len;
    head = (HEADER *) srv;

    qdcount = ntohs (head->qdcount);
    ancount = ntohs (head->ancount);

    /* Ignore the questions */
    while (qdcount-- > 0 && (len = dn_expand (srv, end, pos, name, 255)) >= 0) {
        g_assert (len >= 0);
        pos += len + QFIXEDSZ;
    }

    /* Parse the answers */
    while (ancount-- > 0 && (len = dn_expand (srv, end, pos, name, 255)) >= 0) {
        /* Ignore the initial string */
        uint16_t pref, port;

        g_assert (len >= 0);
        pos += len;
        /* Ignore type, ttl, class and dlen */
        pos += 10;
        GETSHORT (pref, pos);
        /* Ignore weight */
        pos += 2;
        GETSHORT (port, pos);

        len = dn_expand (srv, end, pos, name, 255);
        if (pref < pref_prio) {
            pref_prio = pref;
            strcpy (pref_name, name);
            pref_port = port;
        }
        pos += len;
    }

    if (pref_name[0]) {
        *out_server = g_strdup (pref_name);
        *out_port = pref_port;
        return TRUE;
    }
    return FALSE;
}

