/*
 *  OpenVPN -- An application to securely tunnel IP networks
 *             over a single TCP/UDP port, with support for SSL/TLS-based
 *             session authentication and key exchange,
 *             packet encryption, packet authentication, and
 *             packet compression.
 *
 *  Copyright (C) 2002-2022 OpenVPN Inc <sales@openvpn.net>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2
 *  as published by the Free Software Foundation.
 *
 *  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, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#elif defined(_MSC_VER)
#include "config-msvc.h"
#endif
#ifdef HAVE_CONFIG_VERSION_H
#include "config-version.h"
#endif

#include "syshead.h"

#ifdef ENABLE_PLUGIN

#ifdef HAVE_DLFCN_H
#include <dlfcn.h>
#endif

#include "buffer.h"
#include "error.h"
#include "misc.h"
#include "plugin.h"
#include "ssl_backend.h"
#include "base64.h"
#include "win32.h"
#include "memdbg.h"

#define PLUGIN_SYMBOL_REQUIRED (1<<0)

/* used only for program aborts */
static struct plugin_common *static_plugin_common = NULL; /* GLOBAL */

static void
plugin_show_string_array(int msglevel, const char *name, const char *array[])
{
    int i;
    for (i = 0; array[i]; ++i)
    {
        if (env_safe_to_print(array[i]))
        {
            msg(msglevel, "%s[%d] = '%s'", name, i, array[i]);
        }
    }
}

static void
plugin_show_args_env(int msglevel, const char *argv[], const char *envp[])
{
    if (check_debug_level(msglevel))
    {
        plugin_show_string_array(msglevel, "ARGV", argv);
        plugin_show_string_array(msglevel, "ENVP", envp);
    }
}

static const char *
plugin_type_name(const int type)
{
    switch (type)
    {
        case OPENVPN_PLUGIN_UP:
            return "PLUGIN_UP";

        case OPENVPN_PLUGIN_DOWN:
            return "PLUGIN_DOWN";

        case OPENVPN_PLUGIN_ROUTE_UP:
            return "PLUGIN_ROUTE_UP";

        case OPENVPN_PLUGIN_IPCHANGE:
            return "PLUGIN_IPCHANGE";

        case OPENVPN_PLUGIN_TLS_VERIFY:
            return "PLUGIN_TLS_VERIFY";

        case OPENVPN_PLUGIN_AUTH_USER_PASS_VERIFY:
            return "PLUGIN_AUTH_USER_PASS_VERIFY";

        case OPENVPN_PLUGIN_CLIENT_CONNECT:
            return "PLUGIN_CLIENT_CONNECT";

        case OPENVPN_PLUGIN_CLIENT_CONNECT_V2:
            return "PLUGIN_CLIENT_CONNECT";

        case OPENVPN_PLUGIN_CLIENT_CONNECT_DEFER:
            return "PLUGIN_CLIENT_CONNECT_DEFER";

        case OPENVPN_PLUGIN_CLIENT_CONNECT_DEFER_V2:
            return "PLUGIN_CLIENT_CONNECT_DEFER_V2";

        case OPENVPN_PLUGIN_CLIENT_DISCONNECT:
            return "PLUGIN_CLIENT_DISCONNECT";

        case OPENVPN_PLUGIN_LEARN_ADDRESS:
            return "PLUGIN_LEARN_ADDRESS";

        case OPENVPN_PLUGIN_TLS_FINAL:
            return "PLUGIN_TLS_FINAL";

        case OPENVPN_PLUGIN_ENABLE_PF:
            return "PLUGIN_ENABLE_PF";

        case OPENVPN_PLUGIN_ROUTE_PREDOWN:
            return "PLUGIN_ROUTE_PREDOWN";

        default:
            return "PLUGIN_???";
    }
}

static const char *
plugin_mask_string(const unsigned int type_mask, struct gc_arena *gc)
{
    struct buffer out = alloc_buf_gc(256, gc);
    bool first = true;
    int i;

    for (i = 0; i < OPENVPN_PLUGIN_N; ++i)
    {
        if (OPENVPN_PLUGIN_MASK(i) & type_mask)
        {
            if (!first)
            {
                buf_printf(&out, "|");
            }
            buf_printf(&out, "%s", plugin_type_name(i));
            first = false;
        }
    }
    return BSTR(&out);
}

static inline unsigned int
plugin_supported_types(void)
{
    return ((1<<OPENVPN_PLUGIN_N)-1);
}

struct plugin_option_list *
plugin_option_list_new(struct gc_arena *gc)
{
    struct plugin_option_list *ret;
    ALLOC_OBJ_CLEAR_GC(ret, struct plugin_option_list, gc);
    return ret;
}

bool
plugin_option_list_add(struct plugin_option_list *list, char **p,
                       struct gc_arena *gc)
{
    if (list->n < MAX_PLUGINS)
    {
        struct plugin_option *o = &list->plugins[list->n++];
        o->argv = make_extended_arg_array(p, false, gc);
        if (o->argv[0])
        {
            o->so_pathname = o->argv[0];
        }
        return true;
    }
    else
    {
        return false;
    }
}

#ifndef ENABLE_SMALL
void
plugin_option_list_print(const struct plugin_option_list *list, int msglevel)
{
    int i;
    struct gc_arena gc = gc_new();

    for (i = 0; i < list->n; ++i)
    {
        const struct plugin_option *o = &list->plugins[i];
        msg(msglevel, "  plugin[%d] %s '%s'", i, o->so_pathname, print_argv(o->argv, &gc, PA_BRACKET));
    }

    gc_free(&gc);
}
#endif

#ifndef _WIN32

static void
libdl_resolve_symbol(void *handle, void **dest, const char *symbol, const char *plugin_name, const unsigned int flags)
{
    *dest = dlsym(handle, symbol);
    if ((flags & PLUGIN_SYMBOL_REQUIRED) && !*dest)
    {
        msg(M_FATAL, "PLUGIN: could not find required symbol '%s' in plugin shared object %s: %s", symbol, plugin_name, dlerror());
    }
}

#else  /* ifndef _WIN32 */

static void
dll_resolve_symbol(HMODULE module, void **dest, const char *symbol, const char *plugin_name, const unsigned int flags)
{
    *dest = GetProcAddress(module, symbol);
    if ((flags & PLUGIN_SYMBOL_REQUIRED) && !*dest)
    {
        msg(M_FATAL, "PLUGIN: could not find required symbol '%s' in plugin DLL %s", symbol, plugin_name);
    }
}

#endif /* ifndef _WIN32 */

static void
plugin_init_item(struct plugin *p, const struct plugin_option *o)
{
    struct gc_arena gc = gc_new();
    bool rel = false;

    p->so_pathname = o->so_pathname;
    p->plugin_type_mask = plugin_supported_types();

#ifndef _WIN32

    p->handle = NULL;

    /* If the plug-in filename is not an absolute path,
     * or beginning with '.', it should use the PLUGIN_LIBDIR
     * as the base directory for loading the plug-in.
     *
     * This means the following scenarios are loaded from these places:
     *    --plugin fancyplug.so              -> $PLUGIN_LIBDIR/fancyplug.so
     *    --plugin my/fancyplug.so           -> $PLUGIN_LIBDIR/my/fancyplug.so
     *    --plugin ./fancyplug.so            -> $CWD/fancyplug.so
     *    --plugin /usr/lib/my/fancyplug.so  -> /usr/lib/my/fancyplug.so
     *
     * Please note that $CWD means the directory OpenVPN is either started from
     * or the directory OpenVPN have changed into using --cd before --plugin
     * was parsed.
     *
     */
    if (!platform_absolute_pathname(p->so_pathname)
        && p->so_pathname[0] != '.')
    {
        char full[PATH_MAX];

        openvpn_snprintf(full, sizeof(full), "%s/%s", PLUGIN_LIBDIR, p->so_pathname);
        p->handle = dlopen(full, RTLD_NOW);
    }
    else
    {
        rel = !platform_absolute_pathname(p->so_pathname);
        p->handle = dlopen(p->so_pathname, RTLD_NOW);
    }
    if (!p->handle)
    {
        msg(M_ERR, "PLUGIN_INIT: could not load plugin shared object %s: %s", p->so_pathname, dlerror());
    }

#define PLUGIN_SYM(var, name, flags) libdl_resolve_symbol(p->handle, (void *)&p->var, name, p->so_pathname, flags)

#else  /* ifndef _WIN32 */

    WCHAR *wpath = wide_string(p->so_pathname, &gc);
    WCHAR normalized_plugin_path[MAX_PATH] = {0};
    /* Normalize the plugin path, converting any relative paths to absolute paths. */
    if (!GetFullPathNameW(wpath, MAX_PATH, normalized_plugin_path, NULL))
    {
        msg(M_ERR, "PLUGIN_INIT: could not load plugin DLL: %ls. Failed to normalize plugin path.", wpath);
    }

    if (!plugin_in_trusted_dir(normalized_plugin_path))
    {
        msg(M_FATAL, "PLUGIN_INIT: could not load plugin DLL: %ls. The DLL is not in a trusted directory.", normalized_plugin_path);
    }

    p->module = LoadLibraryW(normalized_plugin_path);
    if (!p->module)
    {
        msg(M_ERR, "PLUGIN_INIT: could not load plugin DLL: %ls", normalized_plugin_path);
    }

#define PLUGIN_SYM(var, name, flags) dll_resolve_symbol(p->module, (void *)&p->var, name, p->so_pathname, flags)

#endif /* ifndef _WIN32 */

    PLUGIN_SYM(open1, "openvpn_plugin_open_v1", 0);
    PLUGIN_SYM(open2, "openvpn_plugin_open_v2", 0);
    PLUGIN_SYM(open3, "openvpn_plugin_open_v3", 0);
    PLUGIN_SYM(func1, "openvpn_plugin_func_v1", 0);
    PLUGIN_SYM(func2, "openvpn_plugin_func_v2", 0);
    PLUGIN_SYM(func3, "openvpn_plugin_func_v3", 0);
    PLUGIN_SYM(close, "openvpn_plugin_close_v1", PLUGIN_SYMBOL_REQUIRED);
    PLUGIN_SYM(abort, "openvpn_plugin_abort_v1", 0);
    PLUGIN_SYM(client_constructor, "openvpn_plugin_client_constructor_v1", 0);
    PLUGIN_SYM(client_destructor, "openvpn_plugin_client_destructor_v1", 0);
    PLUGIN_SYM(min_version_required, "openvpn_plugin_min_version_required_v1", 0);
    PLUGIN_SYM(initialization_point, "openvpn_plugin_select_initialization_point_v1", 0);

    if (!p->open1 && !p->open2 && !p->open3)
    {
        msg(M_FATAL, "PLUGIN: symbol openvpn_plugin_open_vX is undefined in plugin: %s", p->so_pathname);
    }

    if (!p->func1 && !p->func2 && !p->func3)
    {
        msg(M_FATAL, "PLUGIN: symbol openvpn_plugin_func_vX is undefined in plugin: %s", p->so_pathname);
    }

    /*
     * Verify that we are sufficiently up-to-date to handle the plugin
     */
    if (p->min_version_required)
    {
        const int plugin_needs_version = (*p->min_version_required)();
        if (plugin_needs_version > OPENVPN_PLUGIN_VERSION)
        {
            msg(M_FATAL, "PLUGIN_INIT: plugin needs interface version %d, but this version of OpenVPN only supports version %d: %s",
                plugin_needs_version,
                OPENVPN_PLUGIN_VERSION,
                p->so_pathname);
        }
    }

    if (p->initialization_point)
    {
        p->requested_initialization_point = (*p->initialization_point)();
    }
    else
    {
        p->requested_initialization_point = OPENVPN_PLUGIN_INIT_PRE_DAEMON;
    }

    if (rel)
    {
        msg(M_WARN, "WARNING: plugin '%s' specified by a relative pathname -- using an absolute pathname would be more secure", p->so_pathname);
    }

    p->initialized = true;

    gc_free(&gc);
}

static void
plugin_vlog(openvpn_plugin_log_flags_t flags, const char *name, const char *format, va_list arglist)
{
    unsigned int msg_flags = 0;

    if (!format)
    {
        return;
    }

    if (!name || name[0] == '\0')
    {
        msg(D_PLUGIN_DEBUG, "PLUGIN: suppressed log message from plugin with unknown name");
        return;
    }

    if (flags & PLOG_ERR)
    {
        msg_flags = M_INFO | M_NONFATAL;
    }
    else if (flags & PLOG_WARN)
    {
        msg_flags = M_INFO | M_WARN;
    }
    else if (flags & PLOG_NOTE)
    {
        msg_flags = M_INFO;
    }
    else if (flags & PLOG_DEBUG)
    {
        msg_flags = D_PLUGIN_DEBUG;
    }

    if (flags & PLOG_ERRNO)
    {
        msg_flags |= M_ERRNO;
    }
    if (flags & PLOG_NOMUTE)
    {
        msg_flags |= M_NOMUTE;
    }

    if (msg_test(msg_flags))
    {
        struct gc_arena gc;
        char *msg_fmt;

        /* Never add instance prefix; not thread safe */
        msg_flags |= M_NOIPREFIX;

        gc_init(&gc);
        msg_fmt = gc_malloc(ERR_BUF_SIZE, false, &gc);
        openvpn_snprintf(msg_fmt, ERR_BUF_SIZE, "PLUGIN %s: %s", name, format);
        x_msg_va(msg_flags, msg_fmt, arglist);

        gc_free(&gc);
    }
}

static void
plugin_log(openvpn_plugin_log_flags_t flags, const char *name, const char *format, ...)
{
    va_list arglist;
    va_start(arglist, format);
    plugin_vlog(flags, name, format, arglist);
    va_end(arglist);
}

static struct openvpn_plugin_callbacks callbacks = {
    plugin_log,
    plugin_vlog,
    secure_memzero,         /* plugin_secure_memzero */
    openvpn_base64_encode,  /* plugin_base64_encode */
    openvpn_base64_decode,  /* plugin_base64_decode */
};


/* Provide a wrapper macro for a version patch level string to plug-ins.
 * This is located here purely to not make the code too messy with #ifndef
 * inside a struct declaration
 */
#ifndef CONFIGURE_GIT_REVISION
#define _OPENVPN_PATCH_LEVEL OPENVPN_VERSION_PATCH
#else
#define _OPENVPN_PATCH_LEVEL "git:" CONFIGURE_GIT_REVISION CONFIGURE_GIT_FLAGS
#endif

static void
plugin_open_item(struct plugin *p,
                 const struct plugin_option *o,
                 struct openvpn_plugin_string_list **retlist,
                 const char **envp,
                 const int init_point)
{
    ASSERT(p->initialized);

    /* clear return list */
    if (retlist)
    {
        *retlist = NULL;
    }

    if (!p->plugin_handle && init_point == p->requested_initialization_point)
    {
        struct gc_arena gc = gc_new();

        dmsg(D_PLUGIN_DEBUG, "PLUGIN_INIT: PRE");
        plugin_show_args_env(D_PLUGIN_DEBUG, o->argv, envp);

        /*
         * Call the plugin initialization
         */
        if (p->open3)
        {
            struct openvpn_plugin_args_open_in args = { p->plugin_type_mask,
                                                        (const char **const) o->argv,
                                                        (const char **const) envp,
                                                        &callbacks,
                                                        SSLAPI,
                                                        PACKAGE_VERSION,
                                                        OPENVPN_VERSION_MAJOR,
                                                        OPENVPN_VERSION_MINOR,
                                                        _OPENVPN_PATCH_LEVEL};
            struct openvpn_plugin_args_open_return retargs;

            CLEAR(retargs);
            retargs.return_list = retlist;
            if ((*p->open3)(OPENVPN_PLUGINv3_STRUCTVER, &args, &retargs) == OPENVPN_PLUGIN_FUNC_SUCCESS)
            {
                p->plugin_type_mask = retargs.type_mask;
                p->plugin_handle = retargs.handle;
            }
            else
            {
                p->plugin_handle = NULL;
            }
        }
        else if (p->open2)
        {
            p->plugin_handle = (*p->open2)(&p->plugin_type_mask, o->argv, envp, retlist);
        }
        else if (p->open1)
        {
            p->plugin_handle = (*p->open1)(&p->plugin_type_mask, o->argv, envp);
        }
        else
        {
            ASSERT(0);
        }

        msg(D_PLUGIN, "PLUGIN_INIT: POST %s '%s' intercepted=%s %s",
            p->so_pathname,
            print_argv(o->argv, &gc, PA_BRACKET),
            plugin_mask_string(p->plugin_type_mask, &gc),
            (retlist && *retlist) ? "[RETLIST]" : "");

        if ((p->plugin_type_mask | plugin_supported_types()) != plugin_supported_types())
        {
            msg(M_FATAL, "PLUGIN_INIT: plugin %s expressed interest in unsupported plugin types: [want=0x%08x, have=0x%08x]",
                p->so_pathname,
                p->plugin_type_mask,
                plugin_supported_types());
        }

        if (p->plugin_handle == NULL)
        {
            msg(M_FATAL, "PLUGIN_INIT: plugin initialization function failed: %s",
                p->so_pathname);
        }

        gc_free(&gc);
    }
}

static int
plugin_call_item(const struct plugin *p,
                 void *per_client_context,
                 const int type,
                 const struct argv *av,
                 struct openvpn_plugin_string_list **retlist,
                 const char **envp,
                 int certdepth,
                 openvpn_x509_cert_t *current_cert
                 )
{
    int status = OPENVPN_PLUGIN_FUNC_SUCCESS;

    /* clear return list */
    if (retlist)
    {
        *retlist = NULL;
    }

    if (p->plugin_handle && (p->plugin_type_mask & OPENVPN_PLUGIN_MASK(type)))
    {
        struct gc_arena gc = gc_new();
        struct argv a = argv_insert_head(av, p->so_pathname);

        dmsg(D_PLUGIN_DEBUG, "PLUGIN_CALL: PRE type=%s", plugin_type_name(type));
        plugin_show_args_env(D_PLUGIN_DEBUG, (const char **)a.argv, envp);

        /*
         * Call the plugin work function
         */
        if (p->func3)
        {
            struct openvpn_plugin_args_func_in args = { type,
                                                        (const char **const) a.argv,
                                                        (const char **const) envp,
                                                        p->plugin_handle,
                                                        per_client_context,
                                                        (current_cert ? certdepth : -1),
                                                        current_cert };

            struct openvpn_plugin_args_func_return retargs;

            CLEAR(retargs);
            retargs.return_list = retlist;
            status = (*p->func3)(OPENVPN_PLUGINv3_STRUCTVER, &args, &retargs);
        }
        else if (p->func2)
        {
            status = (*p->func2)(p->plugin_handle, type, (const char **)a.argv, envp, per_client_context, retlist);
        }
        else if (p->func1)
        {
            status = (*p->func1)(p->plugin_handle, type, (const char **)a.argv, envp);
        }
        else
        {
            ASSERT(0);
        }

        msg(D_PLUGIN, "PLUGIN_CALL: POST %s/%s status=%d",
            p->so_pathname,
            plugin_type_name(type),
            status);

        if (status == OPENVPN_PLUGIN_FUNC_ERROR)
        {
            msg(M_WARN, "PLUGIN_CALL: plugin function %s failed with status %d: %s",
                plugin_type_name(type),
                status,
                p->so_pathname);
        }

        argv_free(&a);
        gc_free(&gc);
    }
    return status;
}

static void
plugin_close_item(struct plugin *p)
{
    if (p->initialized)
    {
        msg(D_PLUGIN, "PLUGIN_CLOSE: %s", p->so_pathname);

        /*
         * Call the plugin close function
         */
        if (p->plugin_handle)
        {
            (*p->close)(p->plugin_handle);
        }

#ifndef _WIN32
        if (dlclose(p->handle))
        {
            msg(M_WARN, "PLUGIN_CLOSE: dlclose() failed on plugin: %s", p->so_pathname);
        }
#elif defined(_WIN32)
        if (!FreeLibrary(p->module))
        {
            msg(M_WARN, "PLUGIN_CLOSE: FreeLibrary() failed on plugin: %s", p->so_pathname);
        }
#endif

        p->initialized = false;
    }
}

static void
plugin_abort_item(const struct plugin *p)
{
    /*
     * Call the plugin abort function
     */
    if (p->abort)
    {
        (*p->abort)(p->plugin_handle);
    }
}

static void
plugin_per_client_init(const struct plugin_common *pc,
                       struct plugin_per_client *cli,
                       const int init_point)
{
    const int n = pc->n;
    int i;

    for (i = 0; i < n; ++i)
    {
        const struct plugin *p = &pc->plugins[i];
        if (p->plugin_handle
            && (init_point < 0 || init_point == p->requested_initialization_point)
            && p->client_constructor)
        {
            cli->per_client_context[i] = (*p->client_constructor)(p->plugin_handle);
        }
    }
}

static void
plugin_per_client_destroy(const struct plugin_common *pc, struct plugin_per_client *cli)
{
    const int n = pc->n;
    int i;

    for (i = 0; i < n; ++i)
    {
        const struct plugin *p = &pc->plugins[i];
        void *cc = cli->per_client_context[i];

        if (p->client_destructor && cc)
        {
            (*p->client_destructor)(p->plugin_handle, cc);
        }
    }
    CLEAR(*cli);
}

struct plugin_list *
plugin_list_inherit(const struct plugin_list *src)
{
    struct plugin_list *pl;
    ALLOC_OBJ_CLEAR(pl, struct plugin_list);
    pl->common = src->common;
    ASSERT(pl->common);
    plugin_per_client_init(pl->common, &pl->per_client, -1);
    return pl;
}

static struct plugin_common *
plugin_common_init(const struct plugin_option_list *list)
{
    int i;
    struct plugin_common *pc;

    ALLOC_OBJ_CLEAR(pc, struct plugin_common);

    for (i = 0; i < list->n; ++i)
    {
        plugin_init_item(&pc->plugins[i],
                         &list->plugins[i]);
        pc->n = i + 1;
    }

    static_plugin_common = pc;
    return pc;
}

static void
plugin_common_open(struct plugin_common *pc,
                   const struct plugin_option_list *list,
                   struct plugin_return *pr,
                   const struct env_set *es,
                   const int init_point)
{
    struct gc_arena gc = gc_new();
    int i;
    const char **envp;

    envp = make_env_array(es, false, &gc);

    if (pr)
    {
        plugin_return_init(pr);
    }

    for (i = 0; i < pc->n; ++i)
    {
        plugin_open_item(&pc->plugins[i],
                         &list->plugins[i],
                         pr ? &pr->list[i] : NULL,
                         envp,
                         init_point);
    }

    if (pr)
    {
        pr->n = i;
    }

    gc_free(&gc);
}

static void
plugin_common_close(struct plugin_common *pc)
{
    static_plugin_common = NULL;
    if (pc)
    {
        int i;

        for (i = 0; i < pc->n; ++i)
        {
            plugin_close_item(&pc->plugins[i]);
        }
        free(pc);
    }
}

struct plugin_list *
plugin_list_init(const struct plugin_option_list *list)
{
    struct plugin_list *pl;
    ALLOC_OBJ_CLEAR(pl, struct plugin_list);
    pl->common = plugin_common_init(list);
    pl->common_owned = true;
    return pl;
}

void
plugin_list_open(struct plugin_list *pl,
                 const struct plugin_option_list *list,
                 struct plugin_return *pr,
                 const struct env_set *es,
                 const int init_point)
{
    plugin_common_open(pl->common, list, pr, es, init_point);
    plugin_per_client_init(pl->common, &pl->per_client, init_point);
}

int
plugin_call_ssl(const struct plugin_list *pl,
                const int type,
                const struct argv *av,
                struct plugin_return *pr,
                struct env_set *es,
                int certdepth,
                openvpn_x509_cert_t *current_cert
                )
{
    if (pr)
    {
        plugin_return_init(pr);
    }

    if (plugin_defined(pl, type))
    {
        struct gc_arena gc = gc_new();
        int i;
        const char **envp;
        const int n = plugin_n(pl);
        bool success = false;
        bool error = false;
        bool deferred_auth_done = false;

        setenv_del(es, "script_type");
        envp = make_env_array(es, false, &gc);

        for (i = 0; i < n; ++i)
        {
            const int status = plugin_call_item(&pl->common->plugins[i],
                                                pl->per_client.per_client_context[i],
                                                type,
                                                av,
                                                pr ? &pr->list[i] : NULL,
                                                envp,
                                                certdepth,
                                                current_cert
                                                );
            switch (status)
            {
                case OPENVPN_PLUGIN_FUNC_SUCCESS:
                    success = true;
                    break;

                case OPENVPN_PLUGIN_FUNC_DEFERRED:
                    if ((type == OPENVPN_PLUGIN_AUTH_USER_PASS_VERIFY)
                        && deferred_auth_done)
                    {
                        /*
                         * Do not allow deferred auth if a deferred auth has
                         * already been started.  This should allow a single
                         * deferred auth call to happen, with one or more
                         * auth calls with an instant authentication result.
                         *
                         * The plug-in API is not designed for multiple
                         * deferred authentications to happen, as the
                         * auth_control_file file will be shared across all
                         * the plug-ins.
                         *
                         * Since this is considered a critical configuration
                         * error, we bail out and exit the OpenVPN process.
                         */
                        error = true;
                        msg(M_FATAL,
                            "Exiting due to multiple authentication plug-ins "
                            "performing deferred authentication.  Only one "
                            "authentication plug-in doing deferred auth is "
                            "allowed.  Ignoring the result and stopping now, "
                            "the current authentication result is not to be "
                            "trusted.");
                        break;
                    }
                    deferred_auth_done = true;
                    break;

                default:
                    error = true;
                    break;
            }
        }

        if (pr)
        {
            pr->n = i;
        }

        gc_free(&gc);

        if (type == OPENVPN_PLUGIN_ENABLE_PF && success)
        {
            return OPENVPN_PLUGIN_FUNC_SUCCESS;
        }
        else if (error)
        {
            return OPENVPN_PLUGIN_FUNC_ERROR;
        }
        else if (deferred_auth_done)
        {
            return OPENVPN_PLUGIN_FUNC_DEFERRED;
        }
    }

    return OPENVPN_PLUGIN_FUNC_SUCCESS;
}

void
plugin_list_close(struct plugin_list *pl)
{
    if (pl)
    {
        if (pl->common)
        {
            plugin_per_client_destroy(pl->common, &pl->per_client);

            if (pl->common_owned)
            {
                plugin_common_close(pl->common);
            }
        }

        free(pl);
    }
}

void
plugin_abort(void)
{
    struct plugin_common *pc = static_plugin_common;
    static_plugin_common = NULL;
    if (pc)
    {
        int i;

        for (i = 0; i < pc->n; ++i)
        {
            plugin_abort_item(&pc->plugins[i]);
        }
    }
}

bool
plugin_defined(const struct plugin_list *pl, const int type)
{
    bool ret = false;

    if (pl)
    {
        const struct plugin_common *pc = pl->common;

        if (pc)
        {
            int i;
            const unsigned int mask = OPENVPN_PLUGIN_MASK(type);
            for (i = 0; i < pc->n; ++i)
            {
                if (pc->plugins[i].plugin_type_mask & mask)
                {
                    ret = true;
                    break;
                }
            }
        }
    }
    return ret;
}

/*
 * Plugin return functions
 */

static void
openvpn_plugin_string_list_item_free(struct openvpn_plugin_string_list *l)
{
    if (l)
    {
        free(l->name);
        string_clear(l->value);
        free(l->value);
        free(l);
    }
}

static void
openvpn_plugin_string_list_free(struct openvpn_plugin_string_list *l)
{
    struct openvpn_plugin_string_list *next;
    while (l)
    {
        next = l->next;
        openvpn_plugin_string_list_item_free(l);
        l = next;
    }
}

static struct openvpn_plugin_string_list *
openvpn_plugin_string_list_find(struct openvpn_plugin_string_list *l, const char *name)
{
    while (l)
    {
        if (!strcmp(l->name, name))
        {
            return l;
        }
        l = l->next;
    }
    return NULL;
}

void
plugin_return_get_column(const struct plugin_return *src,
                         struct plugin_return *dest,
                         const char *colname)
{
    int i;

    dest->n = 0;
    for (i = 0; i < src->n; ++i)
    {
        dest->list[i] = openvpn_plugin_string_list_find(src->list[i], colname);
    }
    dest->n = i;
}

void
plugin_return_free(struct plugin_return *pr)
{
    int i;
    for (i = 0; i < pr->n; ++i)
    {
        openvpn_plugin_string_list_free(pr->list[i]);
    }
    pr->n = 0;
}

#ifdef ENABLE_DEBUG
void
plugin_return_print(const int msglevel, const char *prefix, const struct plugin_return *pr)
{
    int i;
    msg(msglevel, "PLUGIN_RETURN_PRINT %s", prefix);
    for (i = 0; i < pr->n; ++i)
    {
        struct openvpn_plugin_string_list *l = pr->list[i];
        int count = 0;

        msg(msglevel, "PLUGIN #%d (%s)", i, prefix);
        while (l)
        {
            msg(msglevel, "[%d] '%s' -> '%s'\n",
                ++count,
                l->name,
                l->value);
            l = l->next;
        }
    }
}
#endif /* ifdef ENABLE_DEBUG */

#else  /* ifdef ENABLE_PLUGIN */
static void
dummy(void)
{
}
#endif /* ENABLE_PLUGIN */
