/*
    SSSD

    sdap_dyndns.c: LDAP specific dynamic DNS update

    Authors:
        Jakub Hrozek <jhrozek@redhat.com>

    Copyright (C) 2013 Red Hat

    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 "util/util.h"
#include "resolv/async_resolv.h"
#include "providers/dp_backend.h"
#include "providers/dp_dyndns.h"
#include "providers/ldap/sdap_async_private.h"
#include "providers/ldap/sdap_id_op.h"
#include "providers/ldap/ldap_common.h"

static struct tevent_req *
sdap_dyndns_get_addrs_send(TALLOC_CTX *mem_ctx,
                           struct tevent_context *ev,
                           struct sdap_id_ctx *sdap_ctx,
                           const char *iface);
static errno_t
sdap_dyndns_get_addrs_recv(struct tevent_req *req,
                           TALLOC_CTX *mem_ctx,
                           struct sss_iface_addr **_addresses);

struct sdap_dyndns_update_state {
    struct tevent_context *ev;
    struct be_resolv_ctx *be_res;
    struct dp_option *opts;

    const char *hostname;
    const char *dns_zone;
    const char *realm;
    const char *servername;
    int ttl;

    struct sss_iface_addr *addresses;
    struct sss_iface_addr *dns_addrlist;
    uint8_t remove_af;

    bool update_ptr;
    bool check_diff;
    enum be_nsupdate_auth auth_type;
    bool use_server_with_nsupdate;
    char *update_msg;
};

static void sdap_dyndns_update_addrs_done(struct tevent_req *subreq);
static void sdap_dyndns_dns_addrs_done(struct tevent_req *subreq);
static errno_t sdap_dyndns_addrs_diff(struct sdap_dyndns_update_state *state,
                                      bool *_do_update);
static errno_t sdap_dyndns_update_step(struct tevent_req *req);
static errno_t sdap_dyndns_update_ptr_step(struct tevent_req *req);
static void sdap_dyndns_update_done(struct tevent_req *subreq);
static void sdap_dyndns_update_ptr_done(struct tevent_req *subreq);

struct tevent_req *
sdap_dyndns_update_send(TALLOC_CTX *mem_ctx,
                        struct tevent_context *ev,
                        struct be_ctx *be_ctx,
                        struct dp_option *opts,
                        struct sdap_id_ctx *sdap_ctx,
                        enum be_nsupdate_auth auth_type,
                        const char *ifname,
                        const char *hostname,
                        const char *dns_zone,
                        const char *realm,
                        const char *servername,
                        const int ttl,
                        bool check_diff)
{
    errno_t ret;
    struct tevent_req *req;
    struct tevent_req *subreq;
    struct sdap_dyndns_update_state *state;

    req = tevent_req_create(mem_ctx, &state, struct sdap_dyndns_update_state);
    if (req == NULL) {
        return NULL;
    }
    state->check_diff = check_diff;
    state->update_ptr = dp_opt_get_bool(opts, DP_OPT_DYNDNS_UPDATE_PTR);
    state->hostname = hostname;
    state->dns_zone = dns_zone;
    state->realm = realm;
    state->servername = servername;
    state->use_server_with_nsupdate = false;
    state->ttl = ttl;
    state->be_res = be_ctx->be_res;
    state->ev = ev;
    state->opts = opts;
    state->auth_type = auth_type;

    if (ifname) {
       /* Unless one family is restricted, just replace all
        * address families during the update
        */
        switch (state->be_res->family_order) {
        case IPV4_ONLY:
            state->remove_af |= DYNDNS_REMOVE_A;
            break;
        case IPV6_ONLY:
            state->remove_af |= DYNDNS_REMOVE_AAAA;
            break;
        case IPV4_FIRST:
        case IPV6_FIRST:
            state->remove_af |= (DYNDNS_REMOVE_A |
                                 DYNDNS_REMOVE_AAAA);
            break;
        }
    } else {
        /* If the interface isn't specified, we ONLY want to have the address
         * that's connected to the LDAP server stored, so we need to check
         * (and later remove) both address families.
         */
        state->remove_af = (DYNDNS_REMOVE_A | DYNDNS_REMOVE_AAAA);
    }

    subreq = sdap_dyndns_get_addrs_send(state, state->ev, sdap_ctx, ifname);
    if (!subreq) {
        ret = EIO;
        DEBUG(SSSDBG_OP_FAILURE, ("sdap_id_op_connect_send failed: [%d](%s)\n",
              ret, sss_strerror(ret)));
        goto done;
    }
    tevent_req_set_callback(subreq, sdap_dyndns_update_addrs_done, req);

    ret = EOK;
done:
    if (ret != EOK) {
        tevent_req_error(req, ret);
        tevent_req_post(req, ev);
    }
    return req;
}

static void
sdap_dyndns_update_addrs_done(struct tevent_req *subreq)
{
    errno_t ret;
    struct tevent_req *req;
    struct sdap_dyndns_update_state *state;

    req = tevent_req_callback_data(subreq, struct tevent_req);
    state = tevent_req_data(req, struct sdap_dyndns_update_state);

    ret = sdap_dyndns_get_addrs_recv(subreq, state, &state->addresses);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, ("Can't get addresses for DNS update\n"));
        tevent_req_error(req, ret);
        return;
    }

    if (state->check_diff || state->update_ptr) {
        /* Check if we need the update at all. In case we are updating the PTR
         * records as well, we need to know the old addresses to be able to
         * reliably delete the PTR records */
        subreq = nsupdate_get_addrs_send(state, state->ev,
                                         state->be_res, state->hostname);
        if (subreq == NULL) {
            DEBUG(SSSDBG_OP_FAILURE, ("Can't initiate address check\n"));
            tevent_req_error(req, ret);
            return;
        }
        tevent_req_set_callback(subreq, sdap_dyndns_dns_addrs_done, req);
        return;
    }

    /* Perform update */
    ret = sdap_dyndns_update_step(req);
    if (ret != EOK) {
        tevent_req_error(req, ret);
        return;
    }
    /* Execution will resume in sdap_dyndns_update_done */
}

static void
sdap_dyndns_dns_addrs_done(struct tevent_req *subreq)
{
    struct tevent_req *req;
    struct sdap_dyndns_update_state *state;
    errno_t ret;
    bool do_update;

    req = tevent_req_callback_data(subreq, struct tevent_req);
    state = tevent_req_data(req, struct sdap_dyndns_update_state);

    ret = nsupdate_get_addrs_recv(subreq, state, &state->dns_addrlist, NULL);
    talloc_zfree(subreq);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE,
              ("Could not receive list of current addresses [%d]: %s\n",
              ret, sss_strerror(ret)));
        tevent_req_error(req, ret);
        return;
    }

    if (state->check_diff) {
        ret = sdap_dyndns_addrs_diff(state, &do_update);
        if (ret != EOK) {
            DEBUG(SSSDBG_OP_FAILURE, ("Could not check the diff between DNS "
                  "and current addresses [%d]: %s\n", ret, strerror(ret)));
            tevent_req_error(req, ret);
            return;
        }

        if (do_update == false) {
            DEBUG(SSSDBG_TRACE_FUNC,
                  ("No DNS update needed, addresses did not change\n"));
            tevent_req_done(req);
            return;
        }
        DEBUG(SSSDBG_TRACE_FUNC,
              ("Detected IP addresses change, will perform an update\n"));
    }

    /* Either we needed the addresses for updating PTR records only or
     * the addresses have changed (or both) */
    ret = sdap_dyndns_update_step(req);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, ("Could not start the update [%d]: %s\n",
              ret, sss_strerror(ret)));
        tevent_req_error(req, ret);
    }
    return;
}

static errno_t
sdap_dyndns_addrs_diff(struct sdap_dyndns_update_state *state, bool *_do_update)
{
    errno_t ret;
    int i;
    char **str_dnslist = NULL, **str_local_list = NULL;
    char **dns_only = NULL, **local_only = NULL;
    bool do_update = false;

    ret = sss_iface_addr_list_as_str_list(state,
                                          state->dns_addrlist, &str_dnslist);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE,
               ("Converting DNS IP addresses to strings failed: [%d]: %s\n",
               ret, sss_strerror(ret)));
        return ret;
    }

    ret = sss_iface_addr_list_as_str_list(state,
                                          state->addresses, &str_local_list);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE,
               ("Converting local IP addresses to strings failed: [%d]: %s\n",
               ret, sss_strerror(ret)));
        return ret;
    }

    /* Compare the lists */
    ret = diff_string_lists(state, str_dnslist, str_local_list,
                            &dns_only, &local_only, NULL);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE,
              ("diff_string_lists failed: [%d]: %s\n", ret, sss_strerror(ret)));
        return ret;
    }

    if (dns_only) {
        for (i=0; dns_only[i]; i++) {
            DEBUG(SSSDBG_TRACE_LIBS,
                  ("Address in DNS only: %s\n", dns_only[i]));
            do_update = true;
        }
    }

    if (local_only) {
        for (i=0; local_only[i]; i++) {
            DEBUG(SSSDBG_TRACE_LIBS,
                  ("Address on localhost only: %s\n", local_only[i]));
            do_update = true;
        }
    }

    *_do_update = do_update;
    return EOK;
}

static errno_t
sdap_dyndns_update_step(struct tevent_req *req)
{
    errno_t ret;
    struct sdap_dyndns_update_state *state;
    const char *servername;
    struct tevent_req *subreq;

    state = tevent_req_data(req, struct sdap_dyndns_update_state);

    servername = NULL;
    if (state->use_server_with_nsupdate == true &&
        state->servername) {
        servername = state->servername;
    }

    ret = be_nsupdate_create_fwd_msg(state, state->realm, state->dns_zone,
                                     servername, state->hostname,
                                     state->ttl, state->remove_af,
                                     state->addresses, state->dns_addrlist,
                                     &state->update_msg);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, ("Can't get addresses for DNS update\n"));
        return ret;
    }

    /* Fork a child process to perform the DNS update */
    subreq = be_nsupdate_send(state, state->ev, state->auth_type,
                              state->update_msg,
                              dp_opt_get_bool(state->opts,
                                              DP_OPT_DYNDNS_FORCE_TCP));
    if (subreq == NULL) {
        return EIO;
    }

    tevent_req_set_callback(subreq, sdap_dyndns_update_done, req);
    return EOK;
}

static void
sdap_dyndns_update_done(struct tevent_req *subreq)
{
    errno_t ret;
    int child_status;
    struct tevent_req *req;
    struct sdap_dyndns_update_state *state;

    req = tevent_req_callback_data(subreq, struct tevent_req);
    state = tevent_req_data(req, struct sdap_dyndns_update_state);

    ret = be_nsupdate_recv(subreq, &child_status);
    talloc_zfree(subreq);
    if (ret != EOK) {
        /* If the update didn't succeed, we can retry using the server name */
        if (state->use_server_with_nsupdate == false && state->servername &&
            WIFEXITED(child_status) && WEXITSTATUS(child_status) != 0) {
            state->use_server_with_nsupdate = true;
            DEBUG(SSSDBG_MINOR_FAILURE,
                   ("nsupdate failed, retrying with server name\n"));
            ret = sdap_dyndns_update_step(req);
            if (ret == EOK) {
                return;
            }
        }

        tevent_req_error(req, ret);
        return;
    }

    if (state->update_ptr == false) {
        DEBUG(SSSDBG_TRACE_FUNC, ("No PTR update requested, done\n"));
        tevent_req_done(req);
        return;
    }

    talloc_free(state->update_msg);
    ret = sdap_dyndns_update_ptr_step(req);
    if (ret != EOK) {
        tevent_req_error(req, ret);
        return;
    }
    /* Execution will resume in sdap_dyndns_update_ptr_done */
}

static errno_t
sdap_dyndns_update_ptr_step(struct tevent_req *req)
{
    errno_t ret;
    struct sdap_dyndns_update_state *state;
    const char *servername;
    struct tevent_req *subreq;

    state = tevent_req_data(req, struct sdap_dyndns_update_state);

    servername = NULL;
    if (state->use_server_with_nsupdate == true &&
        state->servername) {
        servername = state->servername;
    }

    ret = be_nsupdate_create_ptr_msg(state, state->realm,
                                     servername, state->hostname,
                                     state->ttl, state->remove_af,
                                     state->addresses, state->dns_addrlist,
                                     &state->update_msg);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, ("Can't get addresses for DNS update\n"));
        return ret;
    }

    /* Fork a child process to perform the DNS update */
    subreq = be_nsupdate_send(state, state->ev, state->auth_type,
                              state->update_msg,
                              dp_opt_get_bool(state->opts,
                                              DP_OPT_DYNDNS_FORCE_TCP));
    if (subreq == NULL) {
        return EIO;
    }

    tevent_req_set_callback(subreq, sdap_dyndns_update_ptr_done, req);
    return EOK;
}

static void
sdap_dyndns_update_ptr_done(struct tevent_req *subreq)
{
    errno_t ret;
    int child_status;
    struct tevent_req *req;
    struct sdap_dyndns_update_state *state;

    req = tevent_req_callback_data(subreq, struct tevent_req);
    state = tevent_req_data(req, struct sdap_dyndns_update_state);

    ret = be_nsupdate_recv(subreq, &child_status);
    talloc_zfree(subreq);
    if (ret != EOK) {
        /* If the update didn't succeed, we can retry using the server name */
        if (state->use_server_with_nsupdate == false && state->servername &&
            WIFEXITED(child_status) && WEXITSTATUS(child_status) != 0) {
            state->use_server_with_nsupdate = true;
            DEBUG(SSSDBG_MINOR_FAILURE,
                   ("nsupdate failed, retrying with server name\n"));
            ret = sdap_dyndns_update_ptr_step(req);
            if (ret == EOK) {
                return;
            }
        }

        tevent_req_error(req, ret);
        return;
    }

    tevent_req_done(req);
}

errno_t
sdap_dyndns_update_recv(struct tevent_req *req)
{
    TEVENT_REQ_RETURN_ON_ERROR(req);
    return EOK;
}

/* A request to get addresses to update with */
struct sdap_dyndns_get_addrs_state {
    struct sdap_id_op* sdap_op;
    struct sss_iface_addr *addresses;
};

static void sdap_dyndns_get_addrs_done(struct tevent_req *subreq);
static errno_t sdap_dyndns_add_ldap_conn(struct sdap_dyndns_get_addrs_state *state,
                                         struct sdap_handle *sh);

static struct tevent_req *
sdap_dyndns_get_addrs_send(TALLOC_CTX *mem_ctx,
                           struct tevent_context *ev,
                           struct sdap_id_ctx *sdap_ctx,
                           const char *iface)
{
    errno_t ret;
    struct tevent_req *req;
    struct tevent_req *subreq;
    struct sdap_dyndns_get_addrs_state *state;

    req = tevent_req_create(mem_ctx, &state,
                            struct sdap_dyndns_get_addrs_state);
    if (req == NULL) {
        return NULL;
    }

    if (iface) {
        ret = sss_iface_addr_list_get(state, iface, &state->addresses);
        if (ret != EOK) {
            DEBUG(SSSDBG_OP_FAILURE,
                  ("Cannot get list of addresses from interface %s\n", iface));
        }
        /* We're done. Just fake an async request completion */
        goto done;
    }

    /* Detect DYNDNS address from LDAP connection */
    state->sdap_op = sdap_id_op_create(state, sdap_ctx->conn->conn_cache);
    if (!state->sdap_op) {
        ret = ENOMEM;
        DEBUG(SSSDBG_OP_FAILURE, ("sdap_id_op_create failed\n"));
        goto done;
    }

    subreq = sdap_id_op_connect_send(state->sdap_op, state, &ret);
    if (!subreq) {
        ret = EIO;
        DEBUG(SSSDBG_OP_FAILURE, ("sdap_id_op_connect_send failed: [%d](%s)\n",
              ret, sss_strerror(ret)));
        goto done;
    }
    tevent_req_set_callback(subreq, sdap_dyndns_get_addrs_done, req);

    ret = EAGAIN;
done:
    if (ret == EOK) {
        tevent_req_done(req);
        tevent_req_post(req, ev);
    } else if (ret != EAGAIN) {
        tevent_req_error(req, ret);
        tevent_req_post(req, ev);
    }

    /* EAGAIN - resolution in progress */
    return req;
}

static void
sdap_dyndns_get_addrs_done(struct tevent_req *subreq)
{
    errno_t ret;
    int dp_error;
    struct tevent_req *req;
    struct sdap_dyndns_get_addrs_state *state;

    req = tevent_req_callback_data(subreq, struct tevent_req);
    state = tevent_req_data(req, struct sdap_dyndns_get_addrs_state);

    ret = sdap_id_op_connect_recv(subreq, &dp_error);
    talloc_zfree(subreq);
    if (ret != EOK) {
        if (dp_error == DP_ERR_OFFLINE) {
            DEBUG(SSSDBG_MINOR_FAILURE, ("No LDAP server is available, "
                  "dynamic DNS update is skipped in offline mode.\n"));
            ret = ERR_DYNDNS_OFFLINE;
        } else {
            DEBUG(SSSDBG_OP_FAILURE,
                  ("Failed to connect to LDAP server: [%d](%s)\n",
                  ret, sss_strerror(ret)));
        }
        tevent_req_error(req, ret);
        return;
    }

    ret = sdap_dyndns_add_ldap_conn(state, sdap_id_op_handle(state->sdap_op));
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, ("Can't get addresses from LDAP connection\n"));
        tevent_req_error(req, ret);
        return;
    }

    /* Got the address! Done! */
    tevent_req_done(req);
}

static errno_t
sdap_dyndns_add_ldap_conn(struct sdap_dyndns_get_addrs_state *state,
                          struct sdap_handle *sh)
{
    int ret;
    int fd;
    struct sss_iface_addr *address;
    struct sockaddr_storage ss;
    socklen_t ss_len = sizeof(ss);

    if (sh == NULL) {
        return EINVAL;
    }

    /* Get the file descriptor for the primary LDAP connection */
    ret = get_fd_from_ldap(sh->ldap, &fd);
    if (ret != EOK) {
        return ret;
    }

    errno = 0;
    ret = getsockname(fd, (struct sockaddr *) &ss, &ss_len);
    if (ret == -1) {
        ret = errno;
        DEBUG(SSSDBG_CRIT_FAILURE, ("Failed to get socket name\n"));
        return ret;
    }

    switch(ss.ss_family) {
    case AF_INET:
    case AF_INET6:
        address = sss_iface_addr_add(state, &state->addresses, &ss);
        if (address == NULL) {
            return ENOMEM;
        }
        break;
    default:
        DEBUG(SSSDBG_CRIT_FAILURE,
              ("Connection to LDAP is neither IPv4 nor IPv6\n"));
        return EIO;
    }

    return EOK;
}

static errno_t
sdap_dyndns_get_addrs_recv(struct tevent_req *req,
                           TALLOC_CTX *mem_ctx,
                           struct sss_iface_addr **_addresses)
{
    struct sdap_dyndns_get_addrs_state *state;

    state = tevent_req_data(req, struct sdap_dyndns_get_addrs_state);

    TEVENT_REQ_RETURN_ON_ERROR(req);

    *_addresses = talloc_steal(mem_ctx, state->addresses);
    return EOK;
}

struct sdap_dyndns_timer_state {
    struct tevent_context *ev;
    struct sdap_id_ctx *sdap_ctx;
    struct be_nsupdate_ctx *dyndns_ctx;

    struct sdap_id_op *sdap_op;
};

static void sdap_dyndns_timer_conn_done(struct tevent_req *req);

struct tevent_req *
sdap_dyndns_timer_conn_send(TALLOC_CTX *mem_ctx,
                            struct tevent_context *ev,
                            struct sdap_id_ctx *sdap_ctx,
                            struct be_nsupdate_ctx *dyndns_ctx)
{
    struct sdap_dyndns_timer_state *state;
    struct tevent_req *req;
    struct tevent_req *subreq;
    errno_t ret;

    req = tevent_req_create(mem_ctx, &state, struct sdap_dyndns_timer_state);
    if (req == NULL) {
        return NULL;
    }
    state->ev = ev;
    state->sdap_ctx = sdap_ctx;
    state->dyndns_ctx = dyndns_ctx;

    /* In order to prevent the connection triggering an
     * online callback which would in turn trigger a concurrent DNS
     * update
     */
    state->dyndns_ctx->timer_in_progress = true;

    /* Make sure to have a valid LDAP connection */
    state->sdap_op = sdap_id_op_create(state, state->sdap_ctx->conn->conn_cache);
    if (state->sdap_op == NULL) {
        DEBUG(SSSDBG_OP_FAILURE, ("sdap_id_op_create failed\n"));
        ret = ENOMEM;
        goto fail;
    }

    subreq = sdap_id_op_connect_send(state->sdap_op, state, &ret);
    if (subreq == NULL) {
        DEBUG(SSSDBG_OP_FAILURE, ("sdap_id_op_connect_send failed: [%d](%s)\n",
              ret, sss_strerror(ret)));
        ret = ENOMEM;
        goto fail;
    }
    tevent_req_set_callback(subreq, sdap_dyndns_timer_conn_done, req);
    return req;

fail:
    dyndns_ctx->timer_in_progress = false;
    be_nsupdate_timer_schedule(ev, dyndns_ctx);
    tevent_req_error(req, ret);
    tevent_req_post(req, ev);
    return req;
}


static void
sdap_dyndns_timer_conn_done(struct tevent_req *subreq)
{
    struct tevent_req *req = tevent_req_callback_data(subreq,
                                                      struct tevent_req);
    struct sdap_dyndns_timer_state *state = tevent_req_data(req,
                                            struct sdap_dyndns_timer_state);
    errno_t ret;
    int dp_error;

    state->dyndns_ctx->timer_in_progress = false;

    ret = sdap_id_op_connect_recv(subreq, &dp_error);
    talloc_zfree(subreq);
    if (ret != EOK) {
        if (dp_error == DP_ERR_OFFLINE) {
            DEBUG(SSSDBG_MINOR_FAILURE, ("No server is available, "
                  "dynamic DNS update is skipped in offline mode.\n"));
            /* Another timer will be scheduled when provider goes online */
            tevent_req_error(req, ERR_DYNDNS_OFFLINE);
        } else {
            DEBUG(SSSDBG_OP_FAILURE,
                  ("Failed to connect to LDAP server: [%d](%s)\n",
                  ret, sss_strerror(ret)));

            /* Just schedule another dyndns retry */
            be_nsupdate_timer_schedule(state->ev, state->dyndns_ctx);
            tevent_req_error(req, ERR_NETWORK_IO);
        }
        return;
    }

    /* All OK, schedule another refresh and let the user call its
     * provider-specific update
     */
    be_nsupdate_timer_schedule(state->ev, state->dyndns_ctx);
    tevent_req_done(req);
}

errno_t
sdap_dyndns_timer_conn_recv(struct tevent_req *req)
{
    TEVENT_REQ_RETURN_ON_ERROR(req);
    return EOK;
}
