Blob Blame History Raw
/* Copyright (C) 2011 the GSS-PROXY contributors, see COPYING for license */

#include "config.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <pwd.h>
#include "gp_proxy.h"
#include "gp_config.h"
#include "gp_selinux.h"

#include <gssapi/gssapi.h>

#include <ini_configobj.h>

struct gp_flag_def {
    const char *name;
    uint32_t value;
};

struct gp_flag_def flag_names[] = {
    { "DELEGATE", GSS_C_DELEG_FLAG },
    { "MUTUAL_AUTH", GSS_C_MUTUAL_FLAG },
    { "REPLAY_DETECT", GSS_C_REPLAY_FLAG },
    { "SEQUENCE", GSS_C_SEQUENCE_FLAG },
    { "CONFIDENTIALITY", GSS_C_CONF_FLAG },
    { "INTEGRITIY", GSS_C_INTEG_FLAG },
    { "ANONYMOUS", GSS_C_ANON_FLAG },
    { NULL, 0 }
};

#define DEFAULT_FILTERED_FLAGS GSS_C_DELEG_FLAG
#define DEFAULT_ENFORCED_FLAGS 0

static void free_str_array(const char ***a, int *count)
{
    const char **array;
    int i;

    if (!a) {
        return;
    }
    array = *a;

    if (count) {
        for (i = 0; i < *count; i++) {
            safefree(array[i]);
        }
    } else {
        for (i = 0; array[i]; i++) {
            safefree(array[i]);
        }
    }
    safefree(*a);
}

void free_cred_store_elements(gss_key_value_set_desc *cs)
{
    if (!cs->elements) return;

    for (unsigned i = 0; i < cs->count; i++) {
        safefree(cs->elements[i].key);
        safefree(cs->elements[i].value);
    }
    safefree(cs->elements);
    cs->count = 0;
}

static void gp_service_free(struct gp_service *svc)
{
    free(svc->name);
    if (svc->mechs & GP_CRED_KRB5) {
        free(svc->krb5.principal);
        free_cred_store_elements(&svc->krb5.store);
        gp_free_creds_handle(&svc->krb5.creds_handle);
    }
    free(svc->socket);
    free(svc->program);
    SELINUX_context_free(svc->selinux_ctx);
    memset(svc, 0, sizeof(struct gp_service));
}

static int setup_krb5_creds_handle(struct gp_service *svc)
{
    uint32_t ret_maj, ret_min;
    const char *keytab = NULL;

    for (unsigned i = 0; i < svc->krb5.store.count; i++) {
        if (strcmp(svc->krb5.store.elements[i].key, "keytab") == 0) {
            keytab = svc->krb5.store.elements[i].value;
            break;
        }
    }

    ret_maj = gp_init_creds_handle(&ret_min, svc->name, keytab,
                                   &svc->krb5.creds_handle);
    if (ret_maj) {
        return ret_min;
    }

    return 0;
}

static int get_krb5_mech_cfg(struct gp_service *svc,
                             struct gp_ini_context *ctx,
                             const char *secname)
{
    struct { const char *a; const char *b; } deprecated_vals[] = {
        {"krb5_keytab", "keytab" },
        {"krb5_ccache", "ccache" },
        {"krb5_client_keytab", "client_keytab" }
    };
    const char *value;
    const char **strings = NULL;
    int count = 0;
    int i;
    int ret;

    ret = gp_config_get_string(ctx, secname, "krb5_principal", &value);
    if (ret == 0) {
        svc->krb5.principal = strdup(value);
        if (!svc->krb5.principal) {
            return ENOMEM;
        }
    } else if (ret != ENOENT) {
        return ret;
    }

    /* check for deprecated options */
    for (i = 0; i < 3; i++) {
        ret = gp_config_get_string(ctx, secname, deprecated_vals[i].a, &value);
        if (ret == 0) {
            GPERROR("\"%s = %s\" is deprecated, "
                    "please use \"cred_store = %s:%s\"\n",
                    deprecated_vals[i].a, value,
                    deprecated_vals[i].b, value);
            return EINVAL;
        } else if (ret != ENOENT) {
            return ret;
        }
    }

    /* instead look for the cred_store parameter */
    ret = gp_config_get_string_array(ctx, secname, "cred_store",
                                     &count, &strings);
    if (ret == 0) {
        const char *p;
        ssize_t len;
        char *key;

        svc->krb5.store.elements =
            calloc(count, sizeof(gss_key_value_element_desc));
        if (!svc->krb5.store.elements) {
            ret = ENOMEM;
            goto done;
        }
        svc->krb5.store.count = count;

        for (int c = 0; c < count; c++) {
            p = strchr(strings[c], ':');
            if (!p) {
                GPERROR("Invalid cred_store value, no ':' separator found in"
                        " [%s].\n", strings[c]);
                ret = EINVAL;
                goto done;
            }
            len = asprintf(&key, "%.*s", (int)(p - strings[c]), strings[c]);
            if (len == -1) {
                ret = ENOMEM;
                goto done;
            }
            svc->krb5.store.elements[c].key = key;
            svc->krb5.store.elements[c].value = strdup(p + 1);
            if (!svc->krb5.store.elements[c].value) {
                ret = ENOMEM;
                goto done;
            }
        }

    } else if (ret == ENOENT) {
        /* when not there we ignore */
        ret = 0;
    }

    if (ret == 0) {
        ret = setup_krb5_creds_handle(svc);
    }

done:
    free_str_array(&strings, &count);
    return ret;
}

static int parse_flags(const char *value, uint32_t *storage)
{
    char *handle;
    char *token;
    char *str;
    bool add;
    unsigned long int conv;
    uint32_t flagval;
    int i;

    str = strdup(value);
    if (!str) {
        return ENOMEM;
    }

    for (token = strtok_r(str, ", ", &handle);
         token != NULL;
         token = strtok_r(NULL, ", ", &handle)) {
        switch (token[0]) {
        case '+':
            add = true;
            break;
        case '-':
            add = false;
            break;
        default:
            GPERROR("Ignoring flag [%s], missing +/- qualifier.\n", token);
            continue;
        }
        token++;
        for (i = 0; flag_names[i].name != NULL; i++) {
            if (strcasecmp(token, flag_names[i].name) == 0) {
                flagval = flag_names[i].value;
                break;
            }
        }
        if (flag_names[i].name == NULL) {
            conv = strtoul(token, &handle, 0);
            if (conv == 0 || conv == ULONG_MAX || *handle != '\0') {
                GPERROR("Ignoring flag [%s], unrecognized value.\n", token);
                continue;
            }
            flagval = conv;
        }
        GPDEBUG("%s Flag %s (%u).\n", add?"Add":"Remove", token, flagval);
        if (add) *storage |= flagval;
        else *storage &= ~flagval;
    }
    safefree(str);

    return 0;
}

static int check_services(const struct gp_config *cfg)
{
    int i, j;
    struct gp_service *isvc, *jsvc;
    const char *isock, *jsock;
    int ret = 0;

    /* [gssproxy] section does not get placed in svcs */
    for (i = 0; i < cfg->num_svcs; i++) {
        isvc = cfg->svcs[i];
        isock = isvc->socket;
        if (!isock) {
            isock = GP_SOCKET_NAME;
        }

        if (isvc->program) {
            if (isvc->program[0] != '/') {
                ret = 1;
                GPERROR("Program paths must be absolute!\n");
            } else if (strchr(isvc->program, '|')) {
                ret = 1;
                GPERROR("The character '|' is invalid in program paths!\n");
            }
        }

        for (j = 0; j < i; j++) {
            jsvc = cfg->svcs[j];
            jsock = jsvc->socket;
            if (!jsock) {
                jsock = GP_SOCKET_NAME;
            }

            if (!gp_same(isock, jsock) ||
                !gp_same(isvc->program, jsvc->program) ||
                !gp_selinux_ctx_equal(isvc->selinux_ctx, jsvc->selinux_ctx)) {
                continue;
            }

            if (jsvc->any_uid) {
                ret = 1;
                GPERROR("%s sets allow_any_uid with the same socket, "
                        "selinux_context, and program as %s!\n",
                        jsvc->name, isvc->name);
            } else if (jsvc->euid == isvc->euid) {
                ret = 1;
                GPERROR("socket, selinux_context, euid, and program for "
                        "%s and %s should not match!\n",
                        isvc->name, jsvc->name);
            }
        }
    }

    return ret;
}

static int load_services(struct gp_config *cfg, struct gp_ini_context *ctx)
{
    int num_sec;
    char *secname = NULL;
    const char *value;
    char *vcopy;
    char *token;
    char *handle;
    int valnum;
    int ret;
    int i, n;

    num_sec = gp_config_get_nsec(ctx);

    /* allocate enough space for num_sec services,
     * we won't waste too much space by overallocating */
    cfg->svcs = calloc(num_sec, sizeof(struct gp_service *));
    if (!cfg->svcs) {
        ret = ENOMEM;
        goto done;
    }

    for (i = 0; i < num_sec; i++) {
        secname = gp_config_get_secname(ctx, i);

        ret = strncmp(secname, "service/", 8);
        if (ret == 0) {
            n = cfg->num_svcs;
            cfg->svcs[n] = calloc(1, sizeof(struct gp_service));
            if (!cfg->svcs[n]) {
                ret = ENOMEM;
                goto done;
            }
            cfg->num_svcs++;

            /* by default allow both */
            cfg->svcs[n]->cred_usage = GSS_C_BOTH;

            cfg->svcs[n]->name = strdup(secname + 8);
            if (!cfg->svcs[n]->name) {
                ret = ENOMEM;
                goto done;
            }

            /* euid can be a string or an int */
            ret = gp_config_get_int(ctx, secname, "euid", &valnum);
            if (ret != 0) {
                ret = gp_config_get_string(ctx, secname, "euid", &value);
                if (ret == 0) {
                    struct passwd *eu_passwd; /* static; do not free */

                    errno = 0; /* needs to be 0; otherwise it won't be set */
                    eu_passwd = getpwnam(value);
                    if (!eu_passwd) {
                        ret = errno;
                        if (ret == 0) { /* not that it gets set anyway... */
                            ret = ENOENT;
                        }
                    } else {
                        valnum = eu_passwd->pw_uid;
                    }
                }
                if (ret != 0) {
                    /* if euid is missing or there is an error retrieving it
                     * return an error and end. This is a fatal condition. */
                    if (ret == ENOENT) {
                        GPERROR("Option 'euid' is missing from [%s].\n", secname);
                        ret = EINVAL;
                    }
                    gp_service_free(cfg->svcs[n]);
                    cfg->num_svcs--;
                    safefree(secname);
                    goto done;
                }
            }
            cfg->svcs[n]->euid = valnum;

            ret = gp_config_get_string(ctx, secname, "allow_any_uid", &value);
            if (ret == 0) {
                if (gp_boolean_is_true(value)) {
                    cfg->svcs[n]->any_uid = true;
                }
            }

            ret = gp_config_get_string(ctx, secname,
                                       "allow_protocol_transition", &value);
            if (ret == 0) {
                if (gp_boolean_is_true(value)) {
                    cfg->svcs[n]->allow_proto_trans = true;
                }
            }

            ret = gp_config_get_string(ctx, secname,
                                       "allow_constrained_delegation", &value);
            if (ret == 0) {
                if (gp_boolean_is_true(value)) {
                    cfg->svcs[n]->allow_const_deleg = true;
                }
            }

            ret = gp_config_get_string(ctx, secname,
                                       "allow_client_ccache_sync", &value);
            if (ret == 0) {
                if (gp_boolean_is_true(value)) {
                    cfg->svcs[n]->allow_cc_sync = true;
                }
            }

            ret = gp_config_get_string(ctx, secname, "trusted", &value);
            if (ret == 0) {
                if (gp_boolean_is_true(value)) {
                    cfg->svcs[n]->trusted = true;
                }
            }

            ret = gp_config_get_string(ctx, secname, "kernel_nfsd", &value);
            if (ret == 0) {
                if (gp_boolean_is_true(value)) {
                    cfg->svcs[n]->kernel_nfsd = true;
                }
            }

            ret = gp_config_get_string(ctx, secname, "impersonate", &value);
            if (ret == 0) {
                if (gp_boolean_is_true(value)) {
                    cfg->svcs[n]->impersonate = true;
                }
            }

            ret = gp_config_get_string(ctx, secname, "socket", &value);
            if (ret == 0) {
                cfg->svcs[n]->socket = strdup(value);
                if (!cfg->svcs[n]->socket) {
                    ret = ENOMEM;
                    goto done;
                }
            }

            ret = gp_config_get_string(ctx, secname, "mechs", &value);
            if (ret != 0) {
                /* if mechs is missing or there is an error retrieving it
                 * return an error and end. This is a fatal condition. */
                if (ret == ENOENT) {
                    GPERROR("Option 'mechs' is missing from [%s].\n", secname);
                    ret = EINVAL;
                }
                gp_service_free(cfg->svcs[n]);
                cfg->num_svcs--;
                safefree(secname);
                goto done;
            }

            vcopy = strdup(value);
            if (!vcopy) {
                ret = ENOMEM;
                goto done;
            }
            token = strtok_r(vcopy, ", ", &handle);
            do {

                ret = strcmp(value, "krb5");
                if (ret == 0) {
                    ret = get_krb5_mech_cfg(cfg->svcs[n], ctx, secname);
                    if (ret == 0) {
                        cfg->svcs[n]->mechs |= GP_CRED_KRB5;
                    } else {
                        GPERROR("Failed to read krb5 config for %s.\n",
                                secname);
                        safefree(vcopy);
                        return ret;
                    }

                } else {
                    GPERROR("Unknown mech: %s in [%s], ignoring.\n",
                            token, secname);
                }

                token = strtok_r(NULL, ", ", &handle);
            } while (token != NULL);
            safefree(vcopy);

            if (cfg->svcs[n]->mechs == 0) {
                GPDEBUG("No mechs found for [%s], ignoring.\n", secname);
                gp_service_free(cfg->svcs[n]);
                cfg->num_svcs--;
                safefree(secname);
                continue;
            }

            ret = gp_config_get_string(ctx, secname,
                                       "selinux_context", &value);
            if (ret == 0) {
                GPDEBUG(
                    "selinux_ctx is deprecated; use euid/socket instead.\n");
                cfg->svcs[n]->selinux_ctx = SELINUX_context_new(value);
                if (!cfg->svcs[n]->selinux_ctx) {
                    ret = EINVAL;
                    goto done;
                }
            }

            ret = gp_config_get_string(ctx, secname, "cred_usage", &value);
            if (ret == 0) {
                if (strcasecmp(value, "initiate") == 0) {
                    cfg->svcs[n]->cred_usage = GSS_C_INITIATE;
                } else if (strcasecmp(value, "accept") == 0) {
                    cfg->svcs[n]->cred_usage = GSS_C_ACCEPT;
                } else if (strcasecmp(value, "both") == 0) {
                    cfg->svcs[n]->cred_usage = GSS_C_BOTH;
                } else {
                    GPDEBUG("Invalid value '%s' for cred_usage in [%s].\n",
                            value, secname);
                    ret = EINVAL;
                    goto done;
                }
            }

            cfg->svcs[n]->filter_flags = DEFAULT_FILTERED_FLAGS;
            ret = gp_config_get_string(ctx, secname, "filter_flags", &value);
            if (ret == 0) {
                parse_flags(value, &cfg->svcs[n]->filter_flags);
            }

            cfg->svcs[n]->enforce_flags = DEFAULT_ENFORCED_FLAGS;
            ret = gp_config_get_string(ctx, secname, "enforce_flags", &value);
            if (ret == 0) {
                ret = parse_flags(value, &cfg->svcs[n]->enforce_flags);
                if (ret) goto done;
            }

            ret = gp_config_get_string(ctx, secname, "program", &value);
            if (ret == 0) {
                cfg->svcs[n]->program = strdup(value);
                if (!cfg->svcs[n]->program) {
                    ret = ENOMEM;
                    goto done;
                }
            }
        }
        safefree(secname);
    }

    if (cfg->num_svcs == 0) {
        GPERROR("No service sections configured!\n");
        return ENOENT;
    }

    ret = check_services(cfg);

done:
    safefree(secname);
    return ret;
}

static int gp_init_ini_context(const char *config_file,
                               const char *config_dir,
                               struct gp_ini_context **ctxp)
{
    struct gp_ini_context *ctx;
    int ret;

    if (!ctxp) {
        return EINVAL;
    }

    ctx = calloc(1, sizeof(struct gp_ini_context));
    if (!ctx) {
        return ENOENT;
    }

    ret = gp_config_init(config_file, config_dir, ctx);

    if (ret) {
        free(ctx);
    } else {
        *ctxp = ctx;
    }
    return ret;
}

int load_config(struct gp_config *cfg)
{
    struct gp_ini_context *ctx;
    const char *tmpstr;
    int tmp_dbg_lvl = 0;
    int tmpint = 0;
    int ret;

    ret = gp_init_ini_context(cfg->config_file, cfg->config_dir, &ctx);
    if (ret) {
        return ret;
    }

    ret = gp_config_get_string(ctx, "gssproxy", "debug", &tmpstr);
    if (ret == 0) {
        if (gp_boolean_is_true(tmpstr)) {
            if (tmp_dbg_lvl == 0) {
                tmp_dbg_lvl = 1;
            }
        }
    } else if (ret != ENOENT) {
        goto done;
    }

    ret = gp_config_get_int(ctx, "gssproxy", "debug_level", &tmpint);
    if (ret == 0) {
        tmp_dbg_lvl = tmpint;
    } else if (ret != ENOENT) {
        goto done;
    }

    ret = gp_config_get_string(ctx, "gssproxy", "run_as_user", &tmpstr);
    if (ret == 0) {
        cfg->proxy_user = strdup(tmpstr);
        if (!cfg->proxy_user) {
            ret = ENOMEM;
            goto done;
        }
    } else if (ret != ENOENT) {
        goto done;
    }

    ret = gp_config_get_int(ctx, "gssproxy", "worker threads",
                            &cfg->num_workers);
    if (ret != 0 && ret != ENOENT) {
        goto done;
    }

    ret = load_services(cfg, ctx);

done:
    if (ret != 0) {
        GPERROR("Error reading configuration %d: %s", ret, gp_strerror(ret));
    }
    gp_debug_toggle(tmp_dbg_lvl);
    gp_config_close(ctx);
    safefree(ctx);
    return ret;
}

struct gp_config *read_config(char *config_file, char *config_dir,
                              char *socket_name, int opt_daemonize)
{
    const char *socket = GP_SOCKET_NAME;
    const char *dir = NULL;
    struct gp_config *cfg;
    int ret;

    cfg = calloc(1, sizeof(struct gp_config));
    if (!cfg) {
        return NULL;
    }

    if (config_file) {
        cfg->config_file = strdup(config_file);
        if (!cfg->config_file) {
            ret = ENOMEM;
            goto done;
        }
    } else {
        ret = asprintf(&cfg->config_file, "%s/gssproxy.conf", PUBCONF_PATH);
        if (ret == -1) {
            goto done;
        }
    }

    if (config_dir) {
        dir = config_dir;
    } else if (!config_file) {
        dir = PUBCONF_PATH;
    }

    if (dir) {
        cfg->config_dir = strdup(dir);
        if (!cfg->config_dir) {
            ret = ENOMEM;
            goto done;
        }
    }

    if (socket_name) socket = socket_name;

    cfg->socket_name = strdup(socket);
    if (cfg->socket_name == NULL) {
        ret = ENOMEM;
        goto done;
    }

    switch (opt_daemonize) {
    case 0:
        /* daemonize by default */
    case 1:
        cfg->daemonize = true;
        break;
    case 2:
        cfg->daemonize = false;
        break;
    }

    ret = load_config(cfg);
    if (ret) {
        GPDEBUG("Config file(s) not found!\n");
    }

done:
    if (ret) {
        /* recursively frees cfg */
        free_config(&cfg);
        return NULL;
    }

    return cfg;
}

struct gp_creds_handle *gp_service_get_creds_handle(struct gp_service *svc)
{
    return svc->krb5.creds_handle;
}

void free_config(struct gp_config **cfg)
{
    struct gp_config *config = *cfg;

    if (!config) {
        return;
    }

    free(config->config_file);
    free(config->config_dir);
    free(config->socket_name);
    free(config->proxy_user);

    for (int i = 0; i < config->num_svcs; i++) {
        gp_service_free(config->svcs[i]);
        safefree(config->svcs[i]);
    }

    free(config->svcs);
    free(config);
    *cfg = NULL;
}

static int gp_config_from_file(const char *config_file,
                               struct ini_cfgobj *ini_config,
                               const uint32_t collision_flags)
{
    struct ini_cfgfile *file_ctx = NULL;
    int ret;

    ret = ini_config_file_open(config_file,
                               0, /* metadata_flags, FIXME */
                               &file_ctx);
    if (ret) {
        GPERROR("Failed to open config file: %d (%s)\n",
                ret, gp_strerror(ret));
        ini_config_destroy(ini_config);
        return ret;
    }

    ret = ini_config_parse(file_ctx,
                           INI_STOP_ON_ANY, /* error_level */
                           collision_flags,
                           INI_PARSE_NOWRAP, /* parse_flags */
                           ini_config);
    if (ret) {
        char **errors = NULL;
        /* we had a parsing failure */
        GPERROR("Failed to parse config file: %d (%s)\n",
                ret, gp_strerror(ret));
        if (ini_config_error_count(ini_config)) {
            ini_config_get_errors(ini_config, &errors);
            if (errors) {
                ini_config_print_errors(stderr, errors);
                ini_config_free_errors(errors);
            }
        }
        ini_config_file_destroy(file_ctx);
        ini_config_destroy(ini_config);
        return ret;
    }

    ini_config_file_destroy(file_ctx);
    return 0;
}

static int gp_config_from_dir(const char *config_dir,
                              struct ini_cfgobj **ini_config,
                              const uint32_t collision_flags)
{
    struct ini_cfgobj *result_cfg = NULL;
    struct ref_array *error_list = NULL;
    int ret;

    const char *patterns[] = {
        /* match only files starting with "##-" and ending in ".conf" */
        "^[0-9]\\{2\\}-.\\{1,\\}\\.conf$",
        NULL,
    };

    const char *sections[] = {
        /* match either "gssproxy" or sections that start with "service/" */
        "^gssproxy$",
        "^service/.*$",
        NULL,
    };

    /* Permission check failures silently skip the file, so they are not
     * useful to us. */
    ret = ini_config_augment(*ini_config,
                             config_dir,
                             patterns,
                             sections,
                             NULL, /* check_perm */
                             INI_STOP_ON_ANY, /* error_level */
                             collision_flags,
                             INI_PARSE_NOWRAP,
                             /* allow sections with the same name in
                              * different files, but log warnings */
                             INI_MS_DETECT | INI_MS_PRESERVE,
                             &result_cfg,
                             &error_list,
                             NULL);
    if (error_list) {
        uint32_t len;
        len = ref_array_len(error_list);
        for (uint32_t i = 0; i < len; i++) {
            /* libini has an unfixable bug where error strings are (char **) */
            GPAUDIT("Error when reading config directory: %s\n",
                    *(char **)ref_array_get(error_list, i, NULL));
        }
        ref_array_destroy(error_list);
    }

    if (ret && ret != EEXIST) {
        GPERROR("Error when reading config directory number: %d\n", ret);

        ref_array_destroy(error_list);
        return ret;
    }

    /* if we read no new files, result_cfg will be NULL */
    if (result_cfg) {
        ini_config_destroy(*ini_config);
        *ini_config = result_cfg;
    }

    return 0;
}

int gp_config_init(const char *config_file, const char *config_dir,
                   struct gp_ini_context *ctx)
{
    struct ini_cfgobj *ini_config = NULL;
    int ret;

    /* Within a single file, merge all collisions */
    const uint32_t collision_flags =
      INI_MS_MERGE | INI_MV1S_ALLOW | INI_MV2S_ALLOW;

    if (!ctx) {
        return EINVAL;
    }

    ret = ini_config_create(&ini_config);
    if (ret) {
        return ENOENT;
    }

    if (config_file) {
        ret = gp_config_from_file(config_file, ini_config, collision_flags);
        if (ret) {
            GPDEBUG("Error when trying to read config file %s.\n",
                    config_file);
            return ret;
        }
    }
    if (config_dir) {
        ret = gp_config_from_dir(config_dir, &ini_config, collision_flags);
        if (ret) {
            GPDEBUG("Error when trying to read config directory %s.\n",
                    config_dir);
            return ret;
        }
    }

    ctx->private_data = ini_config;

    return 0;
}

int gp_config_get_string(struct gp_ini_context *ctx,
                         const char *secname,
                         const char *keyname,
                         const char **value)
{
    struct ini_cfgobj *ini_config = (struct ini_cfgobj *)ctx->private_data;
    struct value_obj *vo = NULL;
    int ret;
    const char *val;

    if (!value) {
        return -1;
    }

    *value = NULL;

    ret = ini_get_config_valueobj(secname,
                                  keyname,
                                  ini_config,
                                  INI_GET_FIRST_VALUE,
                                  &vo);
    if (ret) {
        return ret;
    }
    if (!vo) {
        return ENOENT;
    }

    val = ini_get_const_string_config_value(vo, &ret);
    if (ret) {
        return ret;
    }

    *value = val;

    return 0;
}

int gp_config_get_string_array(struct gp_ini_context *ctx,
                               const char *secname,
                               const char *keyname,
                               int *num_values,
                               const char ***values)
{
    struct ini_cfgobj *ini_config = (struct ini_cfgobj *)ctx->private_data;
    struct value_obj *vo = NULL;
    const char *value;
    int ret;
    int i, count = 0;
    const char **array = NULL;
    const char **t_array;

    if (!values || !num_values) {
        return EINVAL;
    }

    *num_values = 0;
    *values = NULL;

    ret = ini_get_config_valueobj(secname,
                                  keyname,
                                  ini_config,
                                  INI_GET_FIRST_VALUE,
                                  &vo);
    if (ret) {
        return ret;
    }
    if (!vo) {
        return ENOENT;
    }

    value = ini_get_const_string_config_value(vo, &ret);
    if (ret) {
        return ret;
    }

    array = calloc(1, sizeof(char *));
    if (array == NULL) {
        ret = ENOMEM;
        goto done;
    }

    array[count] = strdup(value);
    if (array[count] == NULL) {
        ret = ENOMEM;
        goto done;
    }

    count++;

    do {
        ret = ini_get_config_valueobj(secname,
                                      keyname,
                                      ini_config,
                                      INI_GET_NEXT_VALUE,
                                      &vo);
        if (ret) {
            goto done;
        }
        if (!vo) {
            break;
        }

        value = ini_get_const_string_config_value(vo, &ret);
        if (ret) {
            goto done;
        }

        t_array = realloc(array, (count+1) * sizeof(char *));
        if (t_array == NULL) {
            ret = ENOMEM;
            goto done;
        }
        array = t_array;

        array[count] = strdup(value);
        if (array[count] == NULL) {
            ret = ENOMEM;
            goto done;
        }

        count++;

    } while (1);

    *num_values = count;
    *values = array;

    ret = 0;

done:
    if (ret && array) {
        for (i = 0; i < count; i++) {
            safefree(array[i]);
        }
        safefree(array);
    }
    return ret;
}

int gp_config_get_int(struct gp_ini_context *ctx,
                      const char *secname,
                      const char *keyname,
                      int *value)
{
    struct ini_cfgobj *ini_config = (struct ini_cfgobj *)ctx->private_data;
    struct value_obj *vo = NULL;
    int ret;
    int val;

    if (!value) {
        return EINVAL;
    }

    *value = -1;

    ret = ini_get_config_valueobj(secname,
                                  keyname,
                                  ini_config,
                                  INI_GET_FIRST_VALUE,
                                  &vo);

    if (ret) {
        return ret;
    }
    if (!vo) {
        return ENOENT;
    }

    val = ini_get_int_config_value(vo,
                                   0, /* strict */
                                   0, /* default */
                                   &ret);
    if (ret) {
        return ret;
    }

    *value = val;

    return 0;
}

int gp_config_get_nsec(struct gp_ini_context *ctx)
{
    struct ini_cfgobj *ini_config = (struct ini_cfgobj *)ctx->private_data;
    char **list = NULL;
    int count;
    int error;

    list = ini_get_section_list(ini_config, &count, &error);
    if (error) {
        return 0;
    }

    ini_free_section_list(list);

    return count;
}

char *gp_config_get_secname(struct gp_ini_context *ctx,
                            int i)
{
    struct ini_cfgobj *ini_config = (struct ini_cfgobj *)ctx->private_data;
    char **list = NULL;
    int count;
    int error;
    char *secname;

    list = ini_get_section_list(ini_config, &count, &error);
    if (error) {
        return NULL;
    }

    if (i >= count) {
        return NULL;
    }

    secname = strdup(list[i]);
    ini_free_section_list(list);
    if (!secname) {
        return NULL;
    }

    return secname;
}

int gp_config_close(struct gp_ini_context *ctx)
{
    struct ini_cfgobj *ini_config = NULL;

    if (!ctx) {
        return 0;
    }

    ini_config = (struct ini_cfgobj *)ctx->private_data;

    ini_config_destroy(ini_config);

    return 0;
}