/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
/* lib/krb5/ccache/ccselect.c - krb5_cc_select API and module loader */
/*
 * Copyright (C) 2011 by the Massachusetts Institute of Technology.
 * All rights reserved.
 *
 * Export of this software from the United States of America may
 *   require a specific license from the United States Government.
 *   It is the responsibility of any person or organization contemplating
 *   export to obtain such a license before exporting.
 *
 * WITHIN THAT CONSTRAINT, permission to use, copy, modify, and
 * distribute this software and its documentation for any purpose and
 * without fee is hereby granted, provided that the above copyright
 * notice appear in all copies and that both that copyright notice and
 * this permission notice appear in supporting documentation, and that
 * the name of M.I.T. not be used in advertising or publicity pertaining
 * to distribution of the software without specific, written prior
 * permission.  Furthermore if you modify this software you must label
 * your software as modified software and not distribute it in such a
 * fashion that it might be confused with the original M.I.T. software.
 * M.I.T. makes no representations about the suitability of
 * this software for any purpose.  It is provided "as is" without express
 * or implied warranty.
 */

#include "k5-int.h"
#include "cc-int.h"
#include <krb5/ccselect_plugin.h>
#include "../krb/int-proto.h"

struct ccselect_module_handle {
    struct krb5_ccselect_vtable_st vt;
    krb5_ccselect_moddata data;
    int priority;
};

static void
free_handles(krb5_context context, struct ccselect_module_handle **handles)
{
    struct ccselect_module_handle *h, **hp;

    if (handles == NULL)
        return;
    for (hp = handles; *hp != NULL; hp++) {
        h = *hp;
        if (h->vt.fini)
            h->vt.fini(context, h->data);
        free(h);
    }
    free(handles);
}

static krb5_error_code
load_modules(krb5_context context)
{
    krb5_error_code ret;
    struct ccselect_module_handle **list = NULL, *handle;
    krb5_plugin_initvt_fn *modules = NULL, *mod;
    size_t count;

#ifndef _WIN32
    ret = k5_plugin_register(context, PLUGIN_INTERFACE_CCSELECT, "k5identity",
                             ccselect_k5identity_initvt);
    if (ret != 0)
        goto cleanup;
#endif

    ret = k5_plugin_register(context, PLUGIN_INTERFACE_CCSELECT, "realm",
                             ccselect_realm_initvt);
    if (ret != 0)
        goto cleanup;

    ret = k5_plugin_register(context, PLUGIN_INTERFACE_CCSELECT, "hostname",
                             ccselect_hostname_initvt);
    if (ret != 0)
        goto cleanup;

    ret = k5_plugin_load_all(context, PLUGIN_INTERFACE_CCSELECT, &modules);
    if (ret != 0)
        goto cleanup;

    /* Allocate a large enough list of handles. */
    for (count = 0; modules[count] != NULL; count++);
    list = k5calloc(count + 1, sizeof(*list), &ret);
    if (list == NULL)
        goto cleanup;

    /* Initialize each module, ignoring ones that fail. */
    count = 0;
    for (mod = modules; *mod != NULL; mod++) {
        handle = k5alloc(sizeof(*handle), &ret);
        if (handle == NULL)
            goto cleanup;
        ret = (*mod)(context, 1, 1, (krb5_plugin_vtable)&handle->vt);
        if (ret != 0) {         /* Failed vtable init is non-fatal. */
            TRACE_CCSELECT_VTINIT_FAIL(context, ret);
            free(handle);
            continue;
        }
        handle->data = NULL;
        ret = handle->vt.init(context, &handle->data, &handle->priority);
        if (ret != 0) {         /* Failed initialization is non-fatal. */
            TRACE_CCSELECT_INIT_FAIL(context, handle->vt.name, ret);
            free(handle);
            continue;
        }
        list[count++] = handle;
        list[count] = NULL;
    }
    list[count] = NULL;

    ret = 0;
    context->ccselect_handles = list;
    list = NULL;

cleanup:
    k5_plugin_free_modules(context, modules);
    free_handles(context, list);
    return ret;
}

static krb5_error_code
choose(krb5_context context, struct ccselect_module_handle *h,
       krb5_principal server, krb5_ccache *cache_out,
       krb5_principal *princ_out)
{
    return h->vt.choose(context, h->data, server, cache_out, princ_out);
}

krb5_error_code KRB5_CALLCONV
krb5_cc_select(krb5_context context, krb5_principal server,
               krb5_ccache *cache_out, krb5_principal *princ_out)
{
    krb5_error_code ret;
    int priority;
    struct ccselect_module_handle **hp, *h;
    krb5_ccache cache;
    krb5_principal princ;
    krb5_principal srvcp = NULL;
    char **fbrealms = NULL;

    *cache_out = NULL;
    *princ_out = NULL;

    if (context->ccselect_handles == NULL) {
        ret = load_modules(context);
        if (ret)
            goto cleanup;
    }

    /* Try to use the fallback host realm for the server if there is no
     * authoritative realm. */
    if (krb5_is_referral_realm(&server->realm) &&
        server->type == KRB5_NT_SRV_HST && server->length == 2) {
        ret = krb5_get_fallback_host_realm(context, &server->data[1],
                                           &fbrealms);
        if (ret)
            goto cleanup;

        /* Make a copy with the first fallback realm. */
        ret = krb5_copy_principal(context, server, &srvcp);
        if (ret)
            goto cleanup;
        ret = krb5_set_principal_realm(context, srvcp, fbrealms[0]);
        if (ret)
            goto cleanup;

        server = srvcp;
    }

    /* Consult authoritative modules first, then heuristic ones. */
    for (priority = KRB5_CCSELECT_PRIORITY_AUTHORITATIVE;
         priority >= KRB5_CCSELECT_PRIORITY_HEURISTIC; priority--) {
        for (hp = context->ccselect_handles; *hp != NULL; hp++) {
            h = *hp;
            if (h->priority != priority)
                continue;
            ret = choose(context, h, server, &cache, &princ);
            if (ret == 0) {
                TRACE_CCSELECT_MODCHOICE(context, h->vt.name, server, cache,
                                         princ);
                *cache_out = cache;
                *princ_out = princ;
                goto cleanup;
            } else if (ret == KRB5_CC_NOTFOUND) {
                TRACE_CCSELECT_MODNOTFOUND(context, h->vt.name, server, princ);
                *princ_out = princ;
                goto cleanup;
            } else if (ret != KRB5_PLUGIN_NO_HANDLE) {
                TRACE_CCSELECT_MODFAIL(context, h->vt.name, ret, server);
                goto cleanup;
            }
        }
    }

    TRACE_CCSELECT_NOTFOUND(context, server);
    ret = KRB5_CC_NOTFOUND;

cleanup:
    krb5_free_principal(context, srvcp);
    krb5_free_host_realm(context, fbrealms);
    return ret;
}

void
k5_ccselect_free_context(krb5_context context)
{
    free_handles(context, context->ccselect_handles);
    context->ccselect_handles = NULL;
}
