/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
/* lib/krb5/krb/plugin.c - Plugin framework functions */
/*
 * Copyright (C) 2010 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"

/*
 * A plugin_mapping structure maps a module name to a built-in or dynamic
 * module.  modname is always present; the other three fields can be in four
 * different states:
 *
 * - If dyn_path and dyn_handle are null but module is set, the mapping is to a
 *   built-in module.
 * - If dyn_path is set but dyn_handle and module are null, the mapping is to a
 *   dynamic module which hasn't been loaded yet.
 * - If all three fields are set, the mapping is to a dynamic module which has
 *   been loaded and is ready to use.
 * - If all three fields are null, the mapping is to a dynamic module which
 *   failed to load and should be ignored.
 */
struct plugin_mapping {
    char *modname;
    char *dyn_path;
    struct plugin_file_handle *dyn_handle;
    krb5_plugin_initvt_fn module;
};

const char *interface_names[] = {
    "pwqual",
    "kadm5_hook",
    "clpreauth",
    "kdcpreauth",
    "ccselect",
    "localauth",
    "hostrealm",
    "audit",
    "tls"
};

/* Return the context's interface structure for id, or NULL if invalid. */
static inline struct plugin_interface *
get_interface(krb5_context context, int id)
{
    if (context == NULL || id < 0 || id >= PLUGIN_NUM_INTERFACES)
        return NULL;
    return &context->plugins[id];
}

/* Release the memory associated with the mapping list entry map. */
static void
free_plugin_mapping(struct plugin_mapping *map)
{
    if (map == NULL)
        return;
    free(map->modname);
    free(map->dyn_path);
    if (map->dyn_handle != NULL)
        krb5int_close_plugin(map->dyn_handle);
    free(map);
}

static void
free_mapping_list(struct plugin_mapping **list)
{
    struct plugin_mapping **mp;

    for (mp = list; mp != NULL && *mp != NULL; mp++)
        free_plugin_mapping(*mp);
    free(list);
}

/* Construct a plugin mapping object.  path may be NULL (for a built-in
 * module), or may be relative to the plugin base directory. */
static krb5_error_code
make_plugin_mapping(krb5_context context, const char *name, size_t namelen,
                    const char *path, krb5_plugin_initvt_fn module,
                    struct plugin_mapping **map_out)
{
    krb5_error_code ret;
    struct plugin_mapping *map = NULL;

    /* Create the mapping entry. */
    map = k5alloc(sizeof(*map), &ret);
    if (map == NULL)
        return ret;

    map->modname = k5memdup0(name, namelen, &ret);
    if (map->modname == NULL)
        goto oom;
    if (path != NULL) {
        if (k5_path_join(context->plugin_base_dir, path, &map->dyn_path))
            goto oom;
    }
    map->module = module;
    *map_out = map;
    return 0;

oom:
    free_plugin_mapping(map);
    return ENOMEM;
}

/*
 * Register a mapping from modname to either dyn_path (for an auto-registered
 * dynamic module) or to module (for a builtin module).  dyn_path may be
 * relative to the plugin base directory.
 */
static krb5_error_code
register_module(krb5_context context, struct plugin_interface *interface,
                const char *modname, const char *dyn_path,
                krb5_plugin_initvt_fn module)
{
    struct plugin_mapping **list;
    size_t count;

    /* Allocate list space for another element and a terminator. */
    list = interface->modules;
    for (count = 0; list != NULL && list[count] != NULL; count++);
    list = realloc(interface->modules, (count + 2) * sizeof(*list));
    if (list == NULL)
        return ENOMEM;
    list[count] = list[count + 1] = NULL;
    interface->modules = list;

    /* Create a new mapping structure and add it to the list. */
    return make_plugin_mapping(context, modname, strlen(modname), dyn_path,
                               module, &list[count]);
}

/* Parse a profile module string of the form "modname:modpath" into a mapping
 * entry. */
static krb5_error_code
parse_modstr(krb5_context context, const char *modstr,
             struct plugin_mapping **map_out)
{
    const char *sep;

    *map_out = NULL;

    sep = strchr(modstr, ':');
    if (sep == NULL) {
        k5_setmsg(context, KRB5_PLUGIN_BAD_MODULE_SPEC,
                  _("Invalid module specifier %s"), modstr);
        return KRB5_PLUGIN_BAD_MODULE_SPEC;
    }

    return make_plugin_mapping(context, modstr, sep - modstr, sep + 1, NULL,
                               map_out);
}

/* Return true if value is found in list. */
static krb5_boolean
find_in_list(char **list, const char *value)
{
    for (; *list != NULL; list++) {
        if (strcmp(*list, value) == 0)
            return TRUE;
    }
    return FALSE;
}

/* Get the list of values for the profile variable varname in the section for
 * interface id, or NULL if no values are set. */
static krb5_error_code
get_profile_var(krb5_context context, int id, const char *varname, char ***out)
{
    krb5_error_code ret;
    const char *path[4];

    *out = NULL;
    path[0] = KRB5_CONF_PLUGINS;
    path[1] = interface_names[id];
    path[2] = varname;
    path[3] = NULL;
    ret = profile_get_values(context->profile, path, out);
    return (ret == PROF_NO_RELATION) ? 0 : ret;
}

/* Expand *list_inout to contain the mappings from modstrs, followed by the
 * existing built-in module mappings. */
static krb5_error_code
make_full_list(krb5_context context, char **modstrs,
               struct plugin_mapping ***list_inout)
{
    krb5_error_code ret = 0;
    size_t count, pos, i, j;
    struct plugin_mapping **list, **mp;
    char **mod;

    /* Allocate space for all of the modules plus a null terminator. */
    for (count = 0; modstrs[count] != NULL; count++);
    for (mp = *list_inout; mp != NULL && *mp != NULL; mp++, count++);
    list = calloc(count + 1, sizeof(*list));
    if (list == NULL)
        return ENOMEM;

    /* Parse each profile module entry and store it in the list. */
    for (mod = modstrs, pos = 0; *mod != NULL; mod++, pos++) {
        ret = parse_modstr(context, *mod, &list[pos]);
        if (ret != 0) {
            free_mapping_list(list);
            return ret;
        }
    }

    /* Cannibalize the old list of built-in modules. */
    for (mp = *list_inout; mp != NULL && *mp != NULL; mp++, pos++)
        list[pos] = *mp;
    assert(pos == count);

    /* Filter out duplicates, preferring earlier entries to later ones. */
    for (i = 0, pos = 0; i < count; i++) {
        for (j = 0; j < pos; j++) {
            if (strcmp(list[i]->modname, list[j]->modname) == 0) {
                free_plugin_mapping(list[i]);
                break;
            }
        }
        if (j == pos)
            list[pos++] = list[i];
    }
    list[pos] = NULL;

    free(*list_inout);
    *list_inout = list;
    return 0;
}

/* Remove any entries from list which match values in disabled. */
static void
remove_disabled_modules(struct plugin_mapping **list, char **disable)
{
    struct plugin_mapping **in, **out;

    out = list;
    for (in = list; *in != NULL; in++) {
        if (find_in_list(disable, (*in)->modname))
            free_plugin_mapping(*in);
        else
            *out++ = *in;
    }
    *out = NULL;
}

/* Modify list to include only the entries matching strings in enable, in
 * the order they are listed there. */
static void
filter_enabled_modules(struct plugin_mapping **list, char **enable)
{
    size_t count, i, pos = 0;
    struct plugin_mapping *tmp;

    /* Count the number of existing entries. */
    for (count = 0; list[count] != NULL; count++);

    /* For each string in enable, look for a matching module. */
    for (; *enable != NULL; enable++) {
        for (i = pos; i < count; i++) {
            if (strcmp(list[i]->modname, *enable) == 0) {
                /* Swap the matching module into the next result position. */
                tmp = list[pos];
                list[pos++] = list[i];
                list[i] = tmp;
                break;
            }
        }
    }

    /* Free all mappings which didn't match and terminate the list. */
    for (i = pos; i < count; i++)
        free_plugin_mapping(list[i]);
    list[pos] = NULL;
}

/* Ensure that a plugin interface is configured.  id must be valid. */
static krb5_error_code
configure_interface(krb5_context context, int id)
{
    krb5_error_code ret;
    struct plugin_interface *interface = &context->plugins[id];
    char **modstrs = NULL, **enable = NULL, **disable = NULL;

    if (interface->configured)
        return 0;

    /* Detect consistency errors when plugin interfaces are added. */
    assert(sizeof(interface_names) / sizeof(*interface_names) ==
           PLUGIN_NUM_INTERFACES);

    /* Get profile variables for this interface. */
    ret = get_profile_var(context, id, KRB5_CONF_MODULE, &modstrs);
    if (ret)
        goto cleanup;
    ret = get_profile_var(context, id, KRB5_CONF_DISABLE, &disable);
    if (ret)
        goto cleanup;
    ret = get_profile_var(context, id, KRB5_CONF_ENABLE_ONLY, &enable);
    if (ret)
        goto cleanup;

    /* Create the full list of dynamic and built-in modules. */
    if (modstrs != NULL) {
        ret = make_full_list(context, modstrs, &interface->modules);
        if (ret)
            goto cleanup;
    }

    /* Remove disabled modules. */
    if (disable != NULL)
        remove_disabled_modules(interface->modules, disable);

    /* Filter and re-order the list according to enable-modules. */
    if (enable != NULL)
        filter_enabled_modules(interface->modules, enable);

cleanup:
    profile_free_list(modstrs);
    profile_free_list(enable);
    profile_free_list(disable);
    return ret;
}

/* If map is for a dynamic module which hasn't been loaded yet, attempt to load
 * it.  Only try to load a module once. */
static void
load_if_needed(krb5_context context, struct plugin_mapping *map,
               const char *iname)
{
    char *symname = NULL;
    struct plugin_file_handle *handle = NULL;
    void (*initvt_fn)();

    if (map->module != NULL || map->dyn_path == NULL)
        return;
    if (asprintf(&symname, "%s_%s_initvt", iname, map->modname) < 0)
        return;
    if (krb5int_open_plugin(map->dyn_path, &handle, &context->err))
        goto err;
    if (krb5int_get_plugin_func(handle, symname, &initvt_fn, &context->err))
        goto err;
    free(symname);
    map->dyn_handle = handle;
    map->module = (krb5_plugin_initvt_fn)initvt_fn;
    return;

err:
    /* Clean up, and also null out map->dyn_path so we don't try again. */
    if (handle != NULL)
        krb5int_close_plugin(handle);
    free(symname);
    free(map->dyn_path);
    map->dyn_path = NULL;
}

krb5_error_code
k5_plugin_load(krb5_context context, int interface_id, const char *modname,
               krb5_plugin_initvt_fn *module)
{
    krb5_error_code ret;
    struct plugin_interface *interface = get_interface(context, interface_id);
    struct plugin_mapping **mp, *map;

    if (interface == NULL)
        return EINVAL;
    ret = configure_interface(context, interface_id);
    if (ret != 0)
        return ret;
    for (mp = interface->modules; mp != NULL && *mp != NULL; mp++) {
        map = *mp;
        if (strcmp(map->modname, modname) == 0) {
            load_if_needed(context, map, interface_names[interface_id]);
            if (map->module != NULL) {
                *module = map->module;
                return 0;
            }
            break;
        }
    }
    k5_setmsg(context, KRB5_PLUGIN_NAME_NOTFOUND,
              _("Could not find %s plugin module named '%s'"),
              interface_names[interface_id], modname);
    return KRB5_PLUGIN_NAME_NOTFOUND;
}

krb5_error_code
k5_plugin_load_all(krb5_context context, int interface_id,
                   krb5_plugin_initvt_fn **modules)
{
    krb5_error_code ret;
    struct plugin_interface *interface = get_interface(context, interface_id);
    struct plugin_mapping **mp, *map;
    krb5_plugin_initvt_fn *list;
    size_t count;

    if (interface == NULL)
        return EINVAL;
    ret = configure_interface(context, interface_id);
    if (ret != 0)
        return ret;

    /* Count the modules and allocate a list to hold them. */
    mp = interface->modules;
    for (count = 0; mp != NULL && mp[count] != NULL; count++);
    list = calloc(count + 1, sizeof(*list));
    if (list == NULL)
        return ENOMEM;

    /* Place each module's initvt function into list. */
    count = 0;
    for (mp = interface->modules; mp != NULL && *mp != NULL; mp++) {
        map = *mp;
        load_if_needed(context, map, interface_names[interface_id]);
        if (map->module != NULL)
            list[count++] = map->module;
    }

    *modules = list;
    return 0;
}

void
k5_plugin_free_modules(krb5_context context, krb5_plugin_initvt_fn *modules)
{
    free(modules);
}

krb5_error_code
k5_plugin_register(krb5_context context, int interface_id, const char *modname,
                   krb5_plugin_initvt_fn module)
{
    struct plugin_interface *interface = get_interface(context, interface_id);

    if (interface == NULL)
        return EINVAL;

    /* Disallow registering plugins after load.  We may need to reconsider
     * this, but it simplifies the design. */
    if (interface->configured)
        return EINVAL;

    return register_module(context, interface, modname, NULL, module);
}

krb5_error_code
k5_plugin_register_dyn(krb5_context context, int interface_id,
                       const char *modname, const char *modsubdir)
{
    krb5_error_code ret;
    struct plugin_interface *interface = get_interface(context, interface_id);
    char *path;

    /* Disallow registering plugins after load. */
    if (interface == NULL || interface->configured)
        return EINVAL;

    if (asprintf(&path, "%s/%s%s", modsubdir, modname, PLUGIN_EXT) < 0)
        return ENOMEM;
    ret = register_module(context, interface, modname, path, NULL);
    free(path);
    return ret;
}

void
k5_plugin_free_context(krb5_context context)
{
    int i;

    for (i = 0; i < PLUGIN_NUM_INTERFACES; i++)
        free_mapping_list(context->plugins[i].modules);
    memset(context->plugins, 0, sizeof(context->plugins));
}
