/*
    Authors:
        Sumit Bose <sbose@redhat.com>

    Copyright (C) 2009 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 "config.h"

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <netdb.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <ctype.h>

#include "util/sss_krb5.h"
#include <krb5/locate_plugin.h>

#include "providers/krb5/krb5_common.h"

#define DEFAULT_KERBEROS_PORT 88
#define DEFAULT_KADMIN_PORT 749
#define DEFAULT_KPASSWD_PORT 464

#define BUFSIZE 512
#define PORT_STR_SIZE 7
#define SSSD_KRB5_LOCATOR_DEBUG "SSSD_KRB5_LOCATOR_DEBUG"
#define SSSD_KRB5_LOCATOR_DISABLE "SSSD_KRB5_LOCATOR_DISABLE"
#define DEBUG_KEY "[sssd_krb5_locator] "
#define PLUGIN_DEBUG(body) do { \
    if (ctx->debug) { \
        plugin_debug_fn body; \
    } \
} while(0)

struct sssd_ctx {
    char *sssd_realm;
    char *kdc_addr;
    uint16_t kdc_port;
    char *kpasswd_addr;
    uint16_t kpasswd_port;
    bool debug;
    bool disabled;
};

void plugin_debug_fn(const char *format, ...)
{
    va_list ap;
    char *s = NULL;
    int ret;

    va_start(ap, format);

    ret = vasprintf(&s, format, ap);
    va_end(ap);
    if (ret < 0) {
        /* ENOMEM */
        return;
    }

    fprintf(stderr, DEBUG_KEY "%s", s);
    free(s);
}

static int get_krb5info(const char *realm, struct sssd_ctx *ctx,
                        enum locate_service_type svc)
{
    int ret;
    char *krb5info_name = NULL;
    size_t len;
    uint8_t buf[BUFSIZE + 1];
    int fd = -1;
    const char *name_tmpl = NULL;
    char *port_str;
    long port;
    char *endptr;

    switch (svc) {
        case locate_service_kdc:
            name_tmpl = KDCINFO_TMPL;
            break;
        case locate_service_kpasswd:
            name_tmpl = KPASSWDINFO_TMPL;
            break;
        default:
            PLUGIN_DEBUG(("Unsupported service [%d].\n", svc));
            return EINVAL;
    }


    len = strlen(realm) + strlen(name_tmpl);

    krb5info_name = calloc(1, len + 1);
    if (krb5info_name == NULL) {
        PLUGIN_DEBUG(("malloc failed.\n"));
        return ENOMEM;
    }

    ret = snprintf(krb5info_name, len, name_tmpl, realm);
    if (ret < 0) {
        PLUGIN_DEBUG(("snprintf failed.\n"));
        ret = EINVAL;
        goto done;
    }
    krb5info_name[len] = '\0';

    fd = open(krb5info_name, O_RDONLY);
    if (fd == -1) {
        PLUGIN_DEBUG(("open failed [%s][%d][%s].\n",
                      krb5info_name, errno, strerror(errno)));
        ret = errno;
        goto done;
    }

    memset(buf, 0, BUFSIZE+1);

    errno = 0;
    len = sss_atomic_read_s(fd, buf, BUFSIZE);
    if (len == -1) {
        ret = errno;
        PLUGIN_DEBUG(("read failed [%d][%s].\n", ret, strerror(ret)));
        close(fd);
        goto done;
    }
    close(fd);

    if (len == BUFSIZE) {
        PLUGIN_DEBUG(("Content of krb5info file [%s] is [%d] or larger.\n",
                      krb5info_name, BUFSIZE));
    }
    PLUGIN_DEBUG(("Found [%s] in [%s].\n", buf, krb5info_name));

    port_str = strrchr((char *) buf, ':');
    if (port_str == NULL) {
        port = 0;
    } else {
        *port_str = '\0';
        ++port_str;

        if (isdigit(*port_str)) {
            errno = 0;
            port = strtol(port_str, &endptr, 10);
            if (errno != 0) {
                ret = errno;
                PLUGIN_DEBUG(("strtol failed on [%s]: [%d][%s], "
                            "assuming default.\n", port_str, ret, strerror(ret)));
                port = 0;
            }
            if (*endptr != '\0') {
                PLUGIN_DEBUG(("Found additional characters [%s] in port number "
                            "[%s], assuming default.\n", endptr, port_str));
                port = 0;
            }

            if (port < 0 || port > 65535) {
                PLUGIN_DEBUG(("Illegal port number [%ld], assuming default.\n",
                            port));
                port = 0;
            }
        } else {
            PLUGIN_DEBUG(("Illegal port number [%s], assuming default.\n",
                        port_str));
            port = 0;
        }
    }

    switch (svc) {
        case locate_service_kdc:
            free(ctx->kdc_addr);
            ctx->kdc_addr = strdup((char *) buf);
            if (ctx->kdc_addr == NULL) {
                PLUGIN_DEBUG(("strdup failed.\n"));
                ret = ENOMEM;
                goto done;
            }
            ctx->kdc_port = (uint16_t) port;
            break;
        case locate_service_kpasswd:
            free(ctx->kpasswd_addr);
            ctx->kpasswd_addr = strdup((char *) buf);
            if (ctx->kpasswd_addr == NULL) {
                PLUGIN_DEBUG(("strdup failed.\n"));
                ret = ENOMEM;
                goto done;
            }
            ctx->kpasswd_port = (uint16_t) port;
            break;
        default:
            PLUGIN_DEBUG(("Unsupported service [%d].\n", svc));
            ret = EINVAL;
            goto done;
    }

    ret = 0;
done:
    free(krb5info_name);
    return ret;
}

krb5_error_code sssd_krb5_locator_init(krb5_context context,
                                       void **private_data)
{
    struct sssd_ctx *ctx;
    const char *dummy;

    ctx = calloc(1,sizeof(struct sssd_ctx));
    if (ctx == NULL) return KRB5_PLUGIN_NO_HANDLE;

    dummy = getenv(SSSD_KRB5_LOCATOR_DEBUG);
    if (dummy == NULL) {
        ctx->debug = false;
    } else {
        ctx->debug = true;
        PLUGIN_DEBUG(("sssd_krb5_locator_init called\n"));
    }

    dummy = getenv(SSSD_KRB5_LOCATOR_DISABLE);
    if (dummy == NULL) {
        ctx->disabled = false;
    } else {
        ctx->disabled = true;
        PLUGIN_DEBUG(("SSSD KRB5 locator plugin is disabled.\n"));
    }

    *private_data = ctx;

    return 0;
}

void sssd_krb5_locator_close(void *private_data)
{
    struct sssd_ctx *ctx;

    if (private_data == NULL) return;

    ctx = (struct sssd_ctx *) private_data;
    PLUGIN_DEBUG(("sssd_krb5_locator_close called\n"));

    free(ctx->kdc_addr);
    free(ctx->kpasswd_addr);
    free(ctx->sssd_realm);
    free(ctx);

    return;
}

krb5_error_code sssd_krb5_locator_lookup(void *private_data,
                    enum locate_service_type svc,
                    const char *realm,
                    int socktype,
                    int family,
                    int (*cbfunc)(void *, int, struct sockaddr *),
                    void *cbdata)
{
    int ret;
    struct addrinfo *ai;
    struct sssd_ctx *ctx;
    struct addrinfo ai_hints;
    uint16_t port = 0;
    const char *addr = NULL;
    char port_str[PORT_STR_SIZE];

    if (private_data == NULL) return KRB5_PLUGIN_NO_HANDLE;
    ctx = (struct sssd_ctx *) private_data;

    if (ctx->disabled) {
        PLUGIN_DEBUG(("Plugin disabled, nothing to do.\n"));
        return KRB5_PLUGIN_NO_HANDLE;
    }

    if (ctx->sssd_realm == NULL || strcmp(ctx->sssd_realm, realm) != 0) {
        free(ctx->sssd_realm);
        ctx->sssd_realm = strdup(realm);
        if (ctx->sssd_realm == NULL) {
            PLUGIN_DEBUG(("strdup failed.\n"));
            return KRB5_PLUGIN_NO_HANDLE;
        }

        ret = get_krb5info(realm, ctx, locate_service_kdc);
        if (ret != EOK) {
            PLUGIN_DEBUG(("get_krb5info failed.\n"));
            return KRB5_PLUGIN_NO_HANDLE;
        }

        if (svc == locate_service_kadmin || svc == locate_service_kpasswd ||
            svc == locate_service_master_kdc) {
            ret = get_krb5info(realm, ctx, locate_service_kpasswd);
            if (ret != EOK) {
                PLUGIN_DEBUG(("reading kpasswd address failed, "
                              "using kdc address.\n"));
                free(ctx->kpasswd_addr);
                ctx->kpasswd_addr = strdup(ctx->kdc_addr);
                ctx->kpasswd_port = 0;
            }
        }
    }

    PLUGIN_DEBUG(("sssd_realm[%s] requested realm[%s] family[%d] socktype[%d] "
                  "locate_service[%d]\n", ctx->sssd_realm, realm, family,
                                          socktype, svc));

    switch (svc) {
        case locate_service_kdc:
            addr = ctx->kdc_addr;
            port = ctx->kdc_port ? ctx->kdc_port : DEFAULT_KERBEROS_PORT;
            break;
        case locate_service_master_kdc:
            addr = ctx->kpasswd_addr;
            port = DEFAULT_KERBEROS_PORT;
            break;
        case locate_service_kadmin:
            addr = ctx->kpasswd_addr;
            port = DEFAULT_KADMIN_PORT;
            break;
        case locate_service_kpasswd:
            addr = ctx->kpasswd_addr;
            port = ctx->kpasswd_port ? ctx->kpasswd_port : DEFAULT_KPASSWD_PORT;
            break;
        case locate_service_krb524:
            return KRB5_PLUGIN_NO_HANDLE;
        default:
            return KRB5_PLUGIN_NO_HANDLE;
    }

    switch (family) {
        case AF_UNSPEC:
        case AF_INET:
        case AF_INET6:
            break;
        default:
            return KRB5_PLUGIN_NO_HANDLE;
    }

    switch (socktype) {
        case SOCK_STREAM:
        case SOCK_DGRAM:
            break;
        default:
            return KRB5_PLUGIN_NO_HANDLE;
    }

    if (strcmp(realm, ctx->sssd_realm) != 0)
        return KRB5_PLUGIN_NO_HANDLE;

    memset(port_str, 0, PORT_STR_SIZE);
    ret = snprintf(port_str, PORT_STR_SIZE-1, "%u", port);
    if (ret < 0 || ret >= (PORT_STR_SIZE-1)) {
        PLUGIN_DEBUG(("snprintf failed.\n"));
        return KRB5_PLUGIN_NO_HANDLE;
    }

    memset(&ai_hints, 0, sizeof(struct addrinfo));
    ai_hints.ai_flags = AI_NUMERICHOST|AI_NUMERICSERV;
    ai_hints.ai_socktype = socktype;

    ret = getaddrinfo(addr, port_str, &ai_hints, &ai);
    if (ret != 0) {
        PLUGIN_DEBUG(("getaddrinfo failed [%d][%s].\n", ret,
                                                        gai_strerror(ret)));
        if (ret == EAI_SYSTEM) {
            PLUGIN_DEBUG(("getaddrinfo failed [%d][%s].\n", errno,
                                                            strerror(errno)));
        }
        return KRB5_PLUGIN_NO_HANDLE;
    }

    PLUGIN_DEBUG(("addr[%s:%s] family[%d] socktype[%d]\n", addr, port_str,
                 ai->ai_family, ai->ai_socktype));

    if ((family == AF_UNSPEC || ai->ai_family == family) &&
        ai->ai_socktype == socktype) {

        ret = cbfunc(cbdata, socktype, ai->ai_addr);
        if (ret != 0) {
            PLUGIN_DEBUG(("cbfunc failed\n"));
            freeaddrinfo(ai);
            return ret;
        } else {
            PLUGIN_DEBUG(("[%s] used\n", addr));
        }
    } else {
        PLUGIN_DEBUG(("[%s] NOT used\n", addr));
    }
    freeaddrinfo(ai);

    return 0;
}

const krb5plugin_service_locate_ftable service_locator = {
    0, /* version */
    sssd_krb5_locator_init,
    sssd_krb5_locator_close,
    sssd_krb5_locator_lookup,
};
