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

    Copyright (C) 2009-2010 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 <ctype.h>
#include <stdio.h>
#include <errno.h>
#include <talloc.h>
#include <profile.h>

#include "config.h"

#include "util/sss_iobuf.h"
#include "util/util.h"
#include "util/sss_krb5.h"

static char *
sss_krb5_get_primary(TALLOC_CTX *mem_ctx,
                     const char *pattern,
                     const char *hostname)
{
    char *primary;
    char *dot;
    char *c;
    char *shortname;

    if (strcmp(pattern, "%S$") == 0) {
        shortname = talloc_strdup(mem_ctx, hostname);
        if (!shortname) return NULL;

        dot = strchr(shortname, '.');
        if (dot) {
            *dot = '\0';
        }

        for (c=shortname; *c != '\0'; ++c) {
            *c = toupper(*c);
        }

        /* The samAccountName is recommended to be less than 20 characters.
         * This is only for users and groups. For machine accounts,
         * the real limit is caused by NetBIOS protocol.
         * NetBIOS names are limited to 16 (15 + $)
         * https://support.microsoft.com/en-us/help/163409/netbios-suffixes-16th-character-of-the-netbios-name
         */
        primary = talloc_asprintf(mem_ctx, "%.15s$", shortname);
        talloc_free(shortname);
        return primary;
    }

    return talloc_asprintf(mem_ctx, pattern, hostname);
}

errno_t select_principal_from_keytab(TALLOC_CTX *mem_ctx,
                                     const char *hostname,
                                     const char *desired_realm,
                                     const char *keytab_name,
                                     char **_principal,
                                     char **_primary,
                                     char **_realm)
{
    krb5_error_code kerr = 0;
    krb5_context krb_ctx = NULL;
    krb5_keytab keytab = NULL;
    krb5_principal client_princ = NULL;
    TALLOC_CTX *tmp_ctx;
    char *primary = NULL;
    char *realm = NULL;
    int i = 0;
    errno_t ret;
    char *principal_string;
    const char *realm_name;
    int realm_len;

    /**
     * The %s conversion is passed as-is, the %S conversion is translated to
     * "short host name"
     *
     * Priority of lookup:
     * - our.hostname@REALM or host/our.hostname@REALM depending on the input
     * - SHORT.HOSTNAME$@REALM (AD domain)
     * - host/our.hostname@REALM
     * - foobar$@REALM (AD domain)
     * - host/foobar@REALM
     * - host/foo@BAR
     * - pick the first principal in the keytab
     */
    const char *primary_patterns[] = {"%s", "%S$", "host/%s", "*$", "host/*",
                                      "host/*", NULL};
    const char *realm_patterns[] =   {"%s", "%s",  "%s",      "%s", "%s",
                                      NULL,     NULL};

    DEBUG(SSSDBG_FUNC_DATA,
          "trying to select the most appropriate principal from keytab\n");
    tmp_ctx = talloc_new(NULL);
    if (!tmp_ctx) {
        DEBUG(SSSDBG_CRIT_FAILURE, "talloc_new failed\n");
        return ENOMEM;
    }

    kerr = sss_krb5_init_context(&krb_ctx);
    if (kerr) {
        DEBUG(SSSDBG_OP_FAILURE, "Failed to init Kerberos context\n");
        ret = EFAULT;
        goto done;
    }

    if (keytab_name != NULL) {
        kerr = krb5_kt_resolve(krb_ctx, keytab_name, &keytab);
    } else {
        kerr = krb5_kt_default(krb_ctx, &keytab);
    }
    if (kerr) {
        DEBUG(SSSDBG_FATAL_FAILURE,
              "Failed to read keytab [%s]: %s\n",
               KEYTAB_CLEAN_NAME,
               sss_krb5_get_error_message(krb_ctx, kerr));
        ret = EFAULT;
        goto done;
    }

    if (!desired_realm) {
        desired_realm = "*";
    }
    if (!hostname) {
        hostname = "*";
    }

    do {
        if (primary_patterns[i]) {
            primary = sss_krb5_get_primary(tmp_ctx,
                                           primary_patterns[i],
                                           hostname);
            if (primary == NULL) {
                ret = ENOMEM;
                goto done;
            }
        } else {
            primary = NULL;
        }
        if (realm_patterns[i]) {
            realm = talloc_asprintf(tmp_ctx, realm_patterns[i], desired_realm);
            if (realm == NULL) {
                ret = ENOMEM;
                goto done;
            }
        } else {
            realm = NULL;
        }

        kerr = find_principal_in_keytab(krb_ctx, keytab, primary, realm,
                                        &client_princ);
        talloc_zfree(primary);
        talloc_zfree(realm);
        if (kerr == 0) {
            break;
        }
        if (client_princ != NULL) {
            krb5_free_principal(krb_ctx, client_princ);
            client_princ = NULL;
        }
        i++;
    } while(primary_patterns[i-1] != NULL || realm_patterns[i-1] != NULL);

    if (kerr == 0) {
        if (_principal) {
            kerr = krb5_unparse_name(krb_ctx, client_princ, &principal_string);
            if (kerr) {
                DEBUG(SSSDBG_CRIT_FAILURE, "krb5_unparse_name failed\n");
                ret = EFAULT;
                goto done;
            }

            *_principal = talloc_strdup(mem_ctx, principal_string);
            free(principal_string);
            if (!*_principal) {
                DEBUG(SSSDBG_CRIT_FAILURE, "talloc_strdup failed\n");
                ret = ENOMEM;
                goto done;
            }
            DEBUG(SSSDBG_FUNC_DATA, "Selected principal: %s\n", *_principal);
        }

        if (_primary) {
            kerr = sss_krb5_unparse_name_flags(krb_ctx, client_princ,
                                               KRB5_PRINCIPAL_UNPARSE_NO_REALM,
                                               &principal_string);
            if (kerr) {
                DEBUG(SSSDBG_CRIT_FAILURE, "krb5_unparse_name failed\n");
                ret = EFAULT;
                goto done;
            }

            *_primary = talloc_strdup(mem_ctx, principal_string);
            free(principal_string);
            if (!*_primary) {
                DEBUG(SSSDBG_CRIT_FAILURE, "talloc_strdup failed\n");
                if (_principal) talloc_zfree(*_principal);
                ret = ENOMEM;
                goto done;
            }
            DEBUG(SSSDBG_FUNC_DATA, "Selected primary: %s\n", *_primary);
        }

        if (_realm) {
            sss_krb5_princ_realm(krb_ctx, client_princ,
                                 &realm_name,
                                 &realm_len);
            if (realm_len == 0) {
                DEBUG(SSSDBG_CRIT_FAILURE, "sss_krb5_princ_realm failed.\n");
                if (_principal) talloc_zfree(*_principal);
                if (_primary) talloc_zfree(*_primary);
                ret = EINVAL;
                goto done;
            }

            *_realm = talloc_asprintf(mem_ctx, "%.*s",
                                      realm_len, realm_name);
            if (!*_realm) {
                DEBUG(SSSDBG_CRIT_FAILURE, "talloc_asprintf failed\n");
                if (_principal) talloc_zfree(*_principal);
                if (_primary) talloc_zfree(*_primary);
                ret = ENOMEM;
                goto done;
            }
            DEBUG(SSSDBG_FUNC_DATA, "Selected realm: %s\n", *_realm);
        }

        ret = EOK;
    } else {
        DEBUG(SSSDBG_MINOR_FAILURE, "No suitable principal found in keytab\n");
        ret = ENOENT;
    }

done:
    if (ret != EOK) {
        DEBUG(SSSDBG_FATAL_FAILURE, "Failed to read keytab [%s]: %s\n",
               KEYTAB_CLEAN_NAME, strerror(ret));
        sss_log(SSS_LOG_ERR, "Failed to read keytab [%s]: %s\n",
                KEYTAB_CLEAN_NAME, strerror(ret));
    }
    if (keytab) krb5_kt_close(krb_ctx, keytab);
    if (krb_ctx) krb5_free_context(krb_ctx);
    if (client_princ != NULL) {
        krb5_free_principal(krb_ctx, client_princ);
        client_princ = NULL;
    }
    talloc_free(tmp_ctx);
    return ret;
}

enum matching_mode {MODE_NORMAL, MODE_PREFIX, MODE_POSTFIX};
/**
 * We only have primary and instances stored separately, we need to
 * join them to one string and compare that string.
 *
 * @param ctx Kerberos context
 * @param principal principal we want to match
 * @param pattern_primary primary part of the principal we want to
 *        perform matching against. It is possible to use * wildcard
 *        at the beginning or at the end of the string. If NULL, it
 *        will act as "*"
 * @param pattern_realm realm part of the principal we want to perform
 *        the matching against. If NULL, it will act as "*"
 */
static bool match_principal(krb5_context ctx,
                     krb5_principal principal,
                     const char *pattern_primary,
                     const char *pattern_realm)
{
    char *primary = NULL;
    char *primary_str = NULL;
    int primary_str_len = 0;
    int tmp_len;
    int len_diff;
    const char *realm_name;
    int realm_len;

    enum matching_mode mode = MODE_NORMAL;
    TALLOC_CTX *tmp_ctx;
    bool ret = false;

    sss_krb5_princ_realm(ctx, principal, &realm_name, &realm_len);
    if (realm_len == 0) {
        DEBUG(SSSDBG_MINOR_FAILURE, "sss_krb5_princ_realm failed.\n");
        return false;
    }

    tmp_ctx = talloc_new(NULL);
    if (!tmp_ctx) {
        DEBUG(SSSDBG_CRIT_FAILURE, "talloc_new failed\n");
        return false;
    }

    if (pattern_primary) {
        tmp_len = strlen(pattern_primary);
        if (pattern_primary[tmp_len-1] == '*') {
            mode = MODE_PREFIX;
            primary_str = talloc_strdup(tmp_ctx, pattern_primary);
            primary_str[tmp_len-1] = '\0';
            primary_str_len = tmp_len-1;
        } else if (pattern_primary[0] == '*') {
            mode = MODE_POSTFIX;
            primary_str = talloc_strdup(tmp_ctx, pattern_primary+1);
            primary_str_len = tmp_len-1;
        }

        sss_krb5_unparse_name_flags(ctx, principal, KRB5_PRINCIPAL_UNPARSE_NO_REALM,
                                    &primary);

        len_diff = strlen(primary)-primary_str_len;

        if ((mode == MODE_NORMAL &&
                strcmp(primary, pattern_primary) != 0) ||
            (mode == MODE_PREFIX &&
                strncmp(primary, primary_str, primary_str_len) != 0) ||
            (mode == MODE_POSTFIX &&
                strcmp(primary+len_diff, primary_str) != 0)) {
            goto done;
        }
    }

    if (!pattern_realm || (realm_len == strlen(pattern_realm) &&
        strncmp(realm_name, pattern_realm, realm_len) == 0)) {
        DEBUG(SSSDBG_TRACE_LIBS,
              "Principal matched to the sample (%s@%s).\n", pattern_primary,
                                                                pattern_realm);
        ret = true;
    }

done:
    free(primary);
    talloc_free(tmp_ctx);
    return ret;
}

krb5_error_code find_principal_in_keytab(krb5_context ctx,
                                         krb5_keytab keytab,
                                         const char *pattern_primary,
                                         const char *pattern_realm,
                                         krb5_principal *princ)
{
    krb5_error_code kerr;
    krb5_error_code kt_err;
    krb5_error_code kerr_d;
    krb5_kt_cursor cursor;
    krb5_keytab_entry entry;
    bool principal_found = false;

    memset(&cursor, 0, sizeof(cursor));
    kerr = krb5_kt_start_seq_get(ctx, keytab, &cursor);
    if (kerr != 0) {
        DEBUG(SSSDBG_CRIT_FAILURE, "krb5_kt_start_seq_get failed.\n");
        return kerr;
    }

    DEBUG(SSSDBG_TRACE_ALL,
          "Trying to find principal %s@%s in keytab.\n", pattern_primary, pattern_realm);
    memset(&entry, 0, sizeof(entry));
    while ((kt_err = krb5_kt_next_entry(ctx, keytab, &entry, &cursor)) == 0) {
        principal_found = match_principal(ctx, entry.principal, pattern_primary, pattern_realm);
        if (principal_found) {
            break;
        }

        kerr = sss_krb5_free_keytab_entry_contents(ctx, &entry);
        if (kerr != 0) {
            DEBUG(SSSDBG_CRIT_FAILURE, "Failed to free keytab entry.\n");
        }
        memset(&entry, 0, sizeof(entry));
    }

    /* Close the keytab here.  Even though we're using cursors, the file
     * handle is stored in the krb5_keytab structure, and it gets
     * overwritten by other keytab calls, creating a leak. */
    kerr = krb5_kt_end_seq_get(ctx, keytab, &cursor);
    if (kerr != 0) {
        DEBUG(SSSDBG_CRIT_FAILURE, "krb5_kt_end_seq_get failed.\n");
        goto done;
    }

    if (!principal_found) {
        kerr = KRB5_KT_NOTFOUND;
        DEBUG(SSSDBG_TRACE_FUNC,
              "No principal matching %s@%s found in keytab.\n",
               pattern_primary, pattern_realm);
        goto done;
    }

    /* check if we got any errors from krb5_kt_next_entry */
    if (kt_err != 0 && kt_err != KRB5_KT_END) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Error while reading keytab.\n");
        goto done;
    }

    kerr = krb5_copy_principal(ctx, entry.principal, princ);
    if (kerr != 0) {
        DEBUG(SSSDBG_CRIT_FAILURE, "krb5_copy_principal failed.\n");
        goto done;
    }

    kerr = 0;

done:
    kerr_d = sss_krb5_free_keytab_entry_contents(ctx, &entry);
    if (kerr_d != 0) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Failed to free keytab entry.\n");
    }

    return kerr;
}

const char *KRB5_CALLCONV sss_krb5_get_error_message(krb5_context ctx,
                                               krb5_error_code ec)
{
#ifdef HAVE_KRB5_GET_ERROR_MESSAGE
    return krb5_get_error_message(ctx, ec);
#else
    int ret;
    char *s = NULL;
    int size = sizeof("Kerberos error [XXXXXXXXXXXX]");

    s = malloc(sizeof(char) * (size));
    if (s == NULL) {
        return NULL;
    }

    ret = snprintf(s, size, "Kerberos error [%12d]", ec);

    if (ret < 0 || ret >= size) {
        free(s);
        return NULL;
    }

    return s;
#endif
}

void KRB5_CALLCONV sss_krb5_free_error_message(krb5_context ctx, const char *s)
{
#ifdef HAVE_KRB5_GET_ERROR_MESSAGE
    krb5_free_error_message(ctx, s);
#else
    free(s);
#endif

    return;
}

krb5_error_code KRB5_CALLCONV sss_krb5_get_init_creds_opt_alloc(
                                                  krb5_context context,
                                                  krb5_get_init_creds_opt **opt)
{
#ifdef HAVE_KRB5_GET_INIT_CREDS_OPT_ALLOC
    return krb5_get_init_creds_opt_alloc(context, opt);
#else
    *opt = calloc(1, sizeof(krb5_get_init_creds_opt));
    if (*opt == NULL) {
        return ENOMEM;
    }
    krb5_get_init_creds_opt_init(*opt);

    return 0;
#endif
}

void KRB5_CALLCONV sss_krb5_get_init_creds_opt_free (krb5_context context,
                                                   krb5_get_init_creds_opt *opt)
{
#ifdef HAVE_KRB5_GET_INIT_CREDS_OPT_ALLOC
    krb5_get_init_creds_opt_free(context, opt);
#else
    free(opt);
#endif

    return;
}

void KRB5_CALLCONV sss_krb5_free_unparsed_name(krb5_context context, char *name)
{
#ifdef HAVE_KRB5_FREE_UNPARSED_NAME
    krb5_free_unparsed_name(context, name);
#else
    if (name != NULL) {
        memset(name, 0, strlen(name));
        free(name);
    }
#endif
}


krb5_error_code KRB5_CALLCONV sss_krb5_get_init_creds_opt_set_expire_callback(
                                                   krb5_context context,
                                                   krb5_get_init_creds_opt *opt,
                                                   krb5_expire_callback_func cb,
                                                   void *data)
{
#ifdef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_EXPIRE_CALLBACK
    return krb5_get_init_creds_opt_set_expire_callback(context, opt, cb, data);
#else
    DEBUG(SSSDBG_FUNC_DATA,
          "krb5_get_init_creds_opt_set_expire_callback not available.\n");
    return 0;
#endif
}

errno_t check_fast(const char *str, bool *use_fast)
{
#if HAVE_KRB5_GET_INIT_CREDS_OPT_SET_FAST_FLAGS
    if (strcasecmp(str, "never") == 0 ) {
        *use_fast = false;
    } else if (strcasecmp(str, "try") == 0 || strcasecmp(str, "demand") == 0) {
        *use_fast = true;
    } else {
        sss_log(SSS_LOG_ALERT, "Unsupported value [%s] for option krb5_use_fast,"
                               "please use never, try, or demand.\n", str);
        return EINVAL;
    }

    return EOK;
#else
    sss_log(SSS_LOG_ALERT, "This build of sssd does not support FAST. "
                           "Please remove option krb5_use_fast.\n");
    return EINVAL;
#endif
}

krb5_error_code KRB5_CALLCONV sss_krb5_get_init_creds_opt_set_fast_ccache_name(
                                                   krb5_context context,
                                                   krb5_get_init_creds_opt *opt,
                                                   const char *fast_ccache_name)
{
#ifdef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_FAST_CCACHE_NAME
    return krb5_get_init_creds_opt_set_fast_ccache_name(context, opt,
                                                        fast_ccache_name);
#else
    DEBUG(SSSDBG_FUNC_DATA,
          "krb5_get_init_creds_opt_set_fast_ccache_name not available.\n");
    return 0;
#endif
}

krb5_error_code KRB5_CALLCONV sss_krb5_get_init_creds_opt_set_fast_flags(
                                                   krb5_context context,
                                                   krb5_get_init_creds_opt *opt,
                                                   krb5_flags flags)
{
#ifdef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_FAST_FLAGS
    return krb5_get_init_creds_opt_set_fast_flags(context, opt, flags);
#else
    DEBUG(SSSDBG_FUNC_DATA,
          "krb5_get_init_creds_opt_set_fast_flags not available.\n");
    return 0;
#endif
}


#ifndef HAVE_KRB5_UNPARSE_NAME_FLAGS
#ifndef REALM_SEP
#define REALM_SEP       '@'
#endif
#ifndef COMPONENT_SEP
#define COMPONENT_SEP   '/'
#endif

static int
sss_krb5_copy_component_quoting(char *dest, const krb5_data *src, int flags)
{
    int j;
    const char *cp = src->data;
    char *q = dest;
    int length = src->length;

    if (flags & KRB5_PRINCIPAL_UNPARSE_DISPLAY) {
        memcpy(dest, src->data, src->length);
        return src->length;
    }

    for (j=0; j < length; j++,cp++) {
        int no_realm = (flags & KRB5_PRINCIPAL_UNPARSE_NO_REALM) &&
            !(flags & KRB5_PRINCIPAL_UNPARSE_SHORT);

        switch (*cp) {
        case REALM_SEP:
            if (no_realm) {
                *q++ = *cp;
                break;
            }
        case COMPONENT_SEP:
        case '\\':
            *q++ = '\\';
            *q++ = *cp;
            break;
        case '\t':
            *q++ = '\\';
            *q++ = 't';
            break;
        case '\n':
            *q++ = '\\';
            *q++ = 'n';
            break;
        case '\b':
            *q++ = '\\';
            *q++ = 'b';
            break;
        case '\0':
            *q++ = '\\';
            *q++ = '0';
            break;
        default:
            *q++ = *cp;
        }
    }
    return q - dest;
}

static int
sss_krb5_component_length_quoted(const krb5_data *src, int flags)
{
    const char *cp = src->data;
    int length = src->length;
    int j;
    int size = length;

    if ((flags & KRB5_PRINCIPAL_UNPARSE_DISPLAY) == 0) {
        int no_realm = (flags & KRB5_PRINCIPAL_UNPARSE_NO_REALM) &&
            !(flags & KRB5_PRINCIPAL_UNPARSE_SHORT);

        for (j = 0; j < length; j++,cp++)
            if ((!no_realm && *cp == REALM_SEP) ||
                *cp == COMPONENT_SEP ||
                *cp == '\0' || *cp == '\\' || *cp == '\t' ||
                *cp == '\n' || *cp == '\b')
                size++;
    }

    return size;
}

#endif /* HAVE_KRB5_UNPARSE_NAME_FLAGS */


krb5_error_code
sss_krb5_parse_name_flags(krb5_context context, const char *name, int flags,
                          krb5_principal *principal)
{
#ifdef HAVE_KRB5_PARSE_NAME_FLAGS
    return krb5_parse_name_flags(context, name, flags, principal);
#else
    if (flags != 0) {
        DEBUG(SSSDBG_MINOR_FAILURE, "krb5_parse_name_flags not available on " \
                                     "this plattform, names are parsed " \
                                     "without flags. Some features like " \
                                     "enterprise principals might not work " \
                                     "as expected.\n");
    }

    return krb5_parse_name(context, name, principal);
#endif
}

krb5_error_code
sss_krb5_unparse_name_flags(krb5_context context, krb5_const_principal principal,
                        int flags, char **name)
{
#ifdef HAVE_KRB5_UNPARSE_NAME_FLAGS
    return krb5_unparse_name_flags(context, principal, flags, name);
#else
    char *cp, *q;
    int i;
    int length;
    krb5_int32 nelem;
    unsigned int totalsize = 0;
    char *default_realm = NULL;
    krb5_error_code ret = 0;

    if (name != NULL)
        *name = NULL;

    if (!principal || !name)
        return KRB5_PARSE_MALFORMED;

    if (flags & KRB5_PRINCIPAL_UNPARSE_SHORT) {
        /* omit realm if local realm */
        krb5_principal_data p;

        ret = krb5_get_default_realm(context, &default_realm);
        if (ret != 0)
            goto cleanup;

        krb5_princ_realm(context, &p)->length = strlen(default_realm);
        krb5_princ_realm(context, &p)->data = default_realm;

        if (krb5_realm_compare(context, &p, principal))
            flags |= KRB5_PRINCIPAL_UNPARSE_NO_REALM;
    }

    if ((flags & KRB5_PRINCIPAL_UNPARSE_NO_REALM) == 0) {
        totalsize += sss_krb5_component_length_quoted(krb5_princ_realm(context,
                                                              principal),
                                             flags);
        totalsize++;
    }

    nelem = krb5_princ_size(context, principal);
    for (i = 0; i < (int) nelem; i++) {
        cp = krb5_princ_component(context, principal, i)->data;
        totalsize += sss_krb5_component_length_quoted(krb5_princ_component(context, principal, i), flags);
        totalsize++;
    }
    if (nelem == 0)
        totalsize++;

    *name = malloc(totalsize);

    if (!*name) {
        ret = ENOMEM;
        goto cleanup;
    }

    q = *name;

    for (i = 0; i < (int) nelem; i++) {
        cp = krb5_princ_component(context, principal, i)->data;
        length = krb5_princ_component(context, principal, i)->length;
        q += sss_krb5_copy_component_quoting(q,
                                    krb5_princ_component(context,
                                                         principal,
                                                         i),
                                    flags);
        *q++ = COMPONENT_SEP;
    }

    if (i > 0)
        q--;
    if ((flags & KRB5_PRINCIPAL_UNPARSE_NO_REALM) == 0) {
        *q++ = REALM_SEP;
        q += sss_krb5_copy_component_quoting(q, krb5_princ_realm(context, principal), flags);
    }
    *q++ = '\0';

cleanup:
    free(default_realm);

    return ret;
#endif /* HAVE_KRB5_UNPARSE_NAME_FLAGS */
}

void sss_krb5_get_init_creds_opt_set_canonicalize(krb5_get_init_creds_opt *opts,
                                                  int canonicalize)
{
    /* FIXME: The extra check for HAVE_KRB5_TICKET_TIMES is a workaround due to Heimdal
     * defining krb5_get_init_creds_opt_set_canonicalize() with a different set of
     * arguments. We should use a better configure check in the future.
     */
#if defined(HAVE_KRB5_GET_INIT_CREDS_OPT_SET_CANONICALIZE) && defined(HAVE_KRB5_TICKET_TIMES)
    krb5_get_init_creds_opt_set_canonicalize(opts, canonicalize);
#else
    DEBUG(SSSDBG_OP_FAILURE, "Kerberos principal canonicalization is not available!\n");
#endif
}

#ifdef HAVE_KRB5_PRINCIPAL_GET_REALM
void sss_krb5_princ_realm(krb5_context context, krb5_const_principal princ,
                          const char **realm, int *len)
{
    const char *realm_str = krb5_principal_get_realm(context, princ);

    if (realm_str != NULL) {
        *realm = realm_str;
        *len = strlen(realm_str);
    } else {
        *realm = NULL;
        *len = 0;
    }
}
#else
void sss_krb5_princ_realm(krb5_context context, krb5_const_principal princ,
                          const char **realm, int *len)
{
    const krb5_data *data;

    data = krb5_princ_realm(context, princ);
    if (data) {
        *realm = data->data;
        *len = data->length;
    } else {
        *realm = NULL;
        *len = 0;
    }
}
#endif

#ifdef HAVE_KRB5_FREE_KEYTAB_ENTRY_CONTENTS
krb5_error_code
sss_krb5_free_keytab_entry_contents(krb5_context context,
                                    krb5_keytab_entry *entry)
{
    return krb5_free_keytab_entry_contents(context, entry);
}
#else
krb5_error_code
sss_krb5_free_keytab_entry_contents(krb5_context context,
                                    krb5_keytab_entry *entry)
{
    return krb5_kt_free_entry(context, entry);
}
#endif


#ifdef HAVE_KRB5_SET_TRACE_CALLBACK

#ifndef HAVE_KRB5_TRACE_INFO
/* krb5-1.10 had struct krb5_trace_info, 1.11 has type named krb5_trace_info */
typedef struct krb5_trace_info krb5_trace_info;
#endif  /* HAVE_KRB5_TRACE_INFO */

static void
sss_child_krb5_trace_cb(krb5_context context,
                        const krb5_trace_info *info, void *data)
{
    if (info == NULL) {
        /* Null info means destroy the callback data. */
        return;
    }

    DEBUG(SSSDBG_TRACE_ALL, "%s\n", info->message);
}

errno_t
sss_child_set_krb5_tracing(krb5_context ctx)
{
    return krb5_set_trace_callback(ctx, sss_child_krb5_trace_cb, NULL);
}
#else /* HAVE_KRB5_SET_TRACE_CALLBACK */
errno_t
sss_child_set_krb5_tracing(krb5_context ctx)
{
    DEBUG(SSSDBG_CONF_SETTINGS, "krb5 tracing is not available\n");
    return 0;
}
#endif /* HAVE_KRB5_SET_TRACE_CALLBACK */

krb5_error_code sss_krb5_find_authdata(krb5_context context,
                                       krb5_authdata *const *ticket_authdata,
                                       krb5_authdata *const *ap_req_authdata,
                                       krb5_authdatatype ad_type,
                                       krb5_authdata ***results)
{
#ifdef HAVE_KRB5_FIND_AUTHDATA
    return krb5_find_authdata(context, ticket_authdata, ap_req_authdata,
                              ad_type, results);
#else
    return ENOTSUP;
#endif
}

krb5_error_code sss_extract_pac(krb5_context ctx,
                                krb5_ccache ccache,
                                krb5_principal server_principal,
                                krb5_principal client_principal,
                                krb5_keytab keytab,
                                krb5_authdata ***_pac_authdata)
{
#ifdef HAVE_PAC_RESPONDER
    krb5_error_code kerr;
    krb5_creds mcred;
    krb5_creds cred;
    krb5_authdata **pac_authdata = NULL;
    krb5_pac pac = NULL;
    int ret;
    krb5_ticket *ticket = NULL;
    krb5_keytab_entry entry;

    memset(&entry, 0, sizeof(entry));
    memset(&mcred, 0, sizeof(mcred));
    memset(&cred, 0, sizeof(mcred));

    mcred.server = server_principal;
    mcred.client = client_principal;

    kerr = krb5_cc_retrieve_cred(ctx, ccache, 0, &mcred, &cred);
    if (kerr != 0) {
        DEBUG(SSSDBG_OP_FAILURE, "krb5_cc_retrieve_cred failed.\n");
        goto done;
    }

    kerr = krb5_decode_ticket(&cred.ticket, &ticket);
    if (kerr != 0) {
        DEBUG(SSSDBG_OP_FAILURE, "krb5_decode_ticket failed.\n");
        goto done;
    }

    kerr = krb5_server_decrypt_ticket_keytab(ctx, keytab, ticket);
    if (kerr != 0) {
        DEBUG(SSSDBG_OP_FAILURE, "krb5_server_decrypt_ticket_keytab failed.\n");
        goto done;
    }

    kerr = sss_krb5_find_authdata(ctx,
                                  ticket->enc_part2->authorization_data, NULL,
                                  KRB5_AUTHDATA_WIN2K_PAC, &pac_authdata);
    if (kerr != 0) {
        DEBUG(SSSDBG_OP_FAILURE, "krb5_find_authdata failed.\n");
        goto done;
    }

    if (pac_authdata == NULL || pac_authdata[0] == NULL) {
        DEBUG(SSSDBG_OP_FAILURE, "No PAC authdata available.\n");
        kerr = ENOENT;
        goto done;
    }

    if (pac_authdata[1] != NULL) {
        DEBUG(SSSDBG_OP_FAILURE, "More than one PAC autdata found.\n");
        kerr = EINVAL;
        goto done;
    }

    kerr = krb5_pac_parse(ctx, pac_authdata[0]->contents,
                          pac_authdata[0]->length, &pac);
    if (kerr != 0) {
        DEBUG(SSSDBG_OP_FAILURE, "krb5_pac_parse failed.\n");
        goto done;
    }

    kerr = krb5_kt_get_entry(ctx, keytab, ticket->server,
                             ticket->enc_part.kvno, ticket->enc_part.enctype,
                             &entry);
    if (kerr != 0) {
        DEBUG(SSSDBG_OP_FAILURE, "krb5_kt_get_entry failed.\n");
        goto done;
    }

    kerr = krb5_pac_verify(ctx, pac, 0, NULL, &entry.key, NULL);
    if (kerr != 0) {
        DEBUG(SSSDBG_OP_FAILURE, "krb5_pac_verify failed.\n");
        goto done;
    }

    ret = unsetenv("_SSS_LOOPS");
    if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Failed to unset _SSS_LOOPS, "
                  "sss_pac_make_request will most certainly fail.\n");
    }

    *_pac_authdata = pac_authdata;
    kerr = 0;

done:
    if (kerr != 0) {
        krb5_free_authdata(ctx, pac_authdata);
    }
    if (entry.magic != 0) {
        krb5_free_keytab_entry_contents(ctx, &entry);
    }
    krb5_pac_free(ctx, pac);
    if (ticket != NULL) {
        krb5_free_ticket(ctx, ticket);
    }

    krb5_free_cred_contents(ctx, &cred);
    return kerr;
#else
    return ENOTSUP;
#endif
}

char * sss_get_ccache_name_for_principal(TALLOC_CTX *mem_ctx,
                                         krb5_context ctx,
                                         krb5_principal principal,
                                         const char *location)
{
#ifdef HAVE_KRB5_CC_COLLECTION
    krb5_error_code kerr;
    krb5_ccache tmp_cc = NULL;
    char *tmp_ccname = NULL;
    char *ret_ccname = NULL;

    DEBUG(SSSDBG_TRACE_ALL,
          "Location: [%s]\n", location);

    kerr = krb5_cc_set_default_name(ctx, location);
    if (kerr != 0) {
        KRB5_DEBUG(SSSDBG_MINOR_FAILURE, ctx, kerr);
        return NULL;
    }

    kerr = krb5_cc_cache_match(ctx, principal, &tmp_cc);
    if (kerr != 0) {
        const char *err_msg = sss_krb5_get_error_message(ctx, kerr);
        DEBUG(SSSDBG_TRACE_INTERNAL,
              "krb5_cc_cache_match failed: [%d][%s]\n", kerr, err_msg);
        sss_krb5_free_error_message(ctx, err_msg);
        return NULL;
    }

    kerr = krb5_cc_get_full_name(ctx, tmp_cc, &tmp_ccname);
    if (kerr != 0) {
        KRB5_DEBUG(SSSDBG_MINOR_FAILURE, ctx, kerr);
        goto done;
    }

    DEBUG(SSSDBG_TRACE_ALL,
          "tmp_ccname: [%s]\n", tmp_ccname);

    ret_ccname = talloc_strdup(mem_ctx, tmp_ccname);
    if (ret_ccname == NULL) {
        DEBUG(SSSDBG_OP_FAILURE, "talloc_strdup failed (ENOMEM).\n");
    }

done:
    if (tmp_cc != NULL) {
        kerr = krb5_cc_close(ctx, tmp_cc);
        if (kerr != 0) {
            KRB5_DEBUG(SSSDBG_MINOR_FAILURE, ctx, kerr);
        }
    }
    krb5_free_string(ctx, tmp_ccname);

    return ret_ccname;
#else
    return NULL;
#endif /* HAVE_KRB5_CC_COLLECTION */
}

krb5_error_code sss_krb5_kt_have_content(krb5_context context,
                                         krb5_keytab keytab)
{
#ifdef HAVE_KRB5_KT_HAVE_CONTENT
    return krb5_kt_have_content(context, keytab);
#else
    krb5_keytab_entry entry;
    krb5_kt_cursor cursor;
    krb5_error_code kerr;
    krb5_error_code kerr_end;

    kerr = krb5_kt_start_seq_get(context, keytab, &cursor);
    if (kerr != 0) {
        DEBUG(SSSDBG_OP_FAILURE,
              "krb5_kt_start_seq_get failed, assuming no entries.\n");
        return KRB5_KT_NOTFOUND;
    }

    kerr = krb5_kt_next_entry(context, keytab, &entry, &cursor);
    kerr_end = krb5_kt_end_seq_get(context, keytab, &cursor);
    if (kerr != 0) {
        DEBUG(SSSDBG_OP_FAILURE,
              "krb5_kt_next_entry failed, assuming no entries.\n");
        return KRB5_KT_NOTFOUND;
    }
    kerr = krb5_free_keytab_entry_contents(context, &entry);

    if (kerr_end != 0) {
        DEBUG(SSSDBG_TRACE_FUNC,
              "krb5_kt_end_seq_get failed, ignored.\n");
    }
    if (kerr != 0) {
        DEBUG(SSSDBG_TRACE_FUNC,
              "krb5_free_keytab_entry_contents failed, ignored.\n");
    }

    return 0;
#endif
}

#define KDC_PROXY_INDICATOR "https://"
#define KDC_PROXY_INDICATOR_LEN (sizeof(KDC_PROXY_INDICATOR) - 1)

bool sss_krb5_realm_has_proxy(const char *realm)
{
    krb5_context context = NULL;
    krb5_error_code kerr;
    struct _profile_t *profile = NULL;
    const char  *profile_path[4] = {"realms", NULL, "kdc", NULL};
    char **list = NULL;
    bool res = false;
    size_t c;

    if (realm == NULL) {
        return false;
    }

    kerr = sss_krb5_init_context(&context);
    if (kerr != 0) {
        DEBUG(SSSDBG_OP_FAILURE, "sss_krb5_init_context failed.\n");
        return false;
    }

    kerr = krb5_get_profile(context, &profile);
    if (kerr != 0) {
        DEBUG(SSSDBG_OP_FAILURE, "krb5_get_profile failed.\n");
        goto done;
    }

    profile_path[1] = realm;

    kerr = profile_get_values(profile, profile_path, &list);
    if (kerr == PROF_NO_RELATION || kerr == PROF_NO_SECTION) {
        goto done;
    } else if (kerr != 0) {
        DEBUG(SSSDBG_OP_FAILURE, "profile_get_values failed.\n");
        goto done;
    }

    for (c = 0; list[c] != NULL; c++) {
        if (strncasecmp(KDC_PROXY_INDICATOR, list[c],
                        KDC_PROXY_INDICATOR_LEN) == 0) {
            DEBUG(SSSDBG_TRACE_ALL,
                  "Found KDC Proxy indicator [%s] in [%s].\n",
                  KDC_PROXY_INDICATOR, list[c]);
            res = true;
            break;
        }
    }

done:
    profile_free_list(list);
    profile_release(profile);
    krb5_free_context(context);

    return res;
}

static errno_t iobuf_read_uint32be(struct sss_iobuf *iobuf,
                                   uint32_t *_val)
{
    uint32_t beval;
    errno_t ret;

    ret = sss_iobuf_read_uint32(iobuf, &beval);
    if (ret != EOK) {
        return ret;
    }

    *_val = be32toh(beval);
    return EOK;
}

static errno_t iobuf_write_uint32be(struct sss_iobuf *iobuf,
                                    uint32_t val)
{
    uint32_t beval;

    beval = htobe32(val);
    return sss_iobuf_write_uint32(iobuf, beval);
}

static errno_t iobuf_get_len_bytes(TALLOC_CTX *mem_ctx,
                                   struct sss_iobuf *iobuf,
                                   uint32_t *_nbytes,
                                   uint8_t **_bytes)
{
    errno_t ret;
    uint32_t nbytes;
    uint8_t *bytes = NULL;

    ret = iobuf_read_uint32be(iobuf, &nbytes);
    if (ret != EOK) {
        return ret;
    }

    bytes = talloc_zero_size(mem_ctx, nbytes);
    if (bytes == NULL) {
        return ENOMEM;
    }

    ret = sss_iobuf_read_len(iobuf, nbytes, bytes);
    if (ret != EOK) {
        talloc_free(bytes);
        return ret;
    }

    *_bytes = bytes;
    *_nbytes = nbytes;
    return EOK;
}

static errno_t get_krb5_data(TALLOC_CTX *mem_ctx,
                             struct sss_iobuf *iobuf,
                             krb5_data *k5data)
{
    errno_t ret;
    uint32_t nbytes;
    uint8_t *bytes = NULL;

    ret = iobuf_get_len_bytes(mem_ctx, iobuf,  &nbytes, &bytes);
    if (ret != EOK) {
        talloc_free(bytes);
        return ret;
    }

    k5data->data = (char *) bytes; /* FIXME - the cast is ugly */
    k5data->length = nbytes;
    return EOK;
}

static errno_t set_krb5_data(struct sss_iobuf *iobuf,
                             krb5_data *k5data)
{
    errno_t ret;

    ret = iobuf_write_uint32be(iobuf, k5data->length);
    if (ret != EOK) {
        return ret;
    }

    if (k5data->length > 0) {
        ret = sss_iobuf_write_len(iobuf,
                                  (uint8_t *) k5data->data,
                                  k5data->length);
        if (ret != EOK) {
            return ret;
        }
    }

    return EOK;
}

/* FIXME - it would be nice if Kerberos exported these APIs.. */
krb5_error_code sss_krb5_unmarshal_princ(TALLOC_CTX *mem_ctx,
                                         struct sss_iobuf *iobuf,
                                         krb5_principal *_princ)
{
    krb5_principal princ = NULL;
    krb5_error_code ret;
    uint32_t ncomps;

    if (iobuf == NULL || _princ == NULL) {
        return EINVAL;
    }

    princ = talloc_zero(mem_ctx, struct krb5_principal_data);
    if (princ == NULL) {
        return ENOMEM;
    }

    princ->magic = KV5M_PRINCIPAL;

    ret = iobuf_read_uint32be(iobuf, (uint32_t *) &princ->type);
    if (ret != EOK) {
        goto fail;
    }

    ret = iobuf_read_uint32be(iobuf, &ncomps);
    if (ret != EOK) {
        goto fail;
    }

    if (ncomps > sss_iobuf_get_capacity(iobuf)) {
        /* Sanity check to avoid large allocations */
        ret = EINVAL;
        goto fail;
    }

    if (ncomps != 0) {
        princ->data = talloc_zero_array(princ, krb5_data, ncomps);
        if (princ->data == NULL) {
            ret = ENOMEM;
            goto fail;
        }

        princ->length = ncomps;
    }

    ret = get_krb5_data(princ, iobuf, &princ->realm);
    if (ret != EOK) {
        goto fail;
    }

    for (size_t i = 0; i < ncomps; i++) {
        ret = get_krb5_data(princ->data, iobuf, &princ->data[i]);
        if (ret != EOK) {
            goto fail;
        }
    }

    *_princ = princ;
    return 0;

fail:
    talloc_free(princ);
    return ret;
}

krb5_error_code sss_krb5_marshal_princ(krb5_principal princ,
                                       struct sss_iobuf *iobuf)
{
    krb5_error_code ret;

    if (iobuf == NULL || princ == NULL) {
        return EINVAL;
    }

    ret = iobuf_write_uint32be(iobuf, princ->type);
    if (ret != EOK) {
        return ret;
    }

    ret = iobuf_write_uint32be(iobuf, princ->length);
    if (ret != EOK) {
        return ret;
    }

    ret = set_krb5_data(iobuf, &princ->realm);
    if (ret != EOK) {
        return ret;
    }

    for (int i = 0; i < princ->length; i++) {
        ret = set_krb5_data(iobuf, &princ->data[i]);
        if (ret != EOK) {
            return ret;
        }
    }
    return EOK;
}

krb5_error_code sss_krb5_init_context(krb5_context *context)
{
    krb5_error_code kerr;
    const char *msg;

    kerr = krb5_init_context(context);
    if (kerr != 0) {
        /* It is safe to call (sss_)krb5_get_error_message() with NULL as first
         * argument. */
        msg = sss_krb5_get_error_message(NULL, kerr);
        DEBUG(SSSDBG_FATAL_FAILURE,
              "Failed to init Kerberos context [%s]\n", msg);
        sss_log(SSS_LOG_CRIT, "Failed to init Kerberos context [%s]\n", msg);
        sss_krb5_free_error_message(NULL, msg);
    }

    return kerr;
}
