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

#include "gssapi_gpm.h"
#include <pthread.h>

struct gpm_mech_info {
    gss_OID mech;
    gss_OID_set name_types;
    gss_OID_set mech_attrs;
    gss_OID_set known_mech_attrs;
    gss_OID_set cred_options;
    gss_OID_set sec_ctx_options;
    gss_buffer_t saslname_sasl_mech_name;
    gss_buffer_t saslname_mech_name;
    gss_buffer_t saslname_mech_desc;
};

struct gpm_mech_attr {
    gss_OID attr;
    gss_buffer_t name;
    gss_buffer_t short_desc;
    gss_buffer_t long_desc;
};

struct gpm_mechs {
    bool initialized;

    gss_OID_set mech_set;

    size_t info_len;
    struct gpm_mech_info *info;

    size_t desc_len;
    struct gpm_mech_attr *desc;
};

pthread_mutex_t global_mechs_lock = PTHREAD_MUTEX_INITIALIZER;
pthread_once_t indicate_mechs_once = PTHREAD_ONCE_INIT;
struct gpm_mechs global_mechs = {
    .initialized = false,
    .mech_set = GSS_C_NO_OID_SET,
    .info_len = 0,
    .info = NULL,
    .desc_len = 0,
    .desc = NULL,
};

static uint32_t gpm_copy_gss_OID_set(uint32_t *minor_status,
                                     gss_OID_set oldset, gss_OID_set *newset)
{
    gss_OID_set n;
    uint32_t ret_maj;
    uint32_t ret_min;

    ret_maj = gss_create_empty_oid_set(&ret_min, &n);
    if (ret_maj) {
        *minor_status = ret_min;
        return ret_maj;
    }

    for (size_t i = 0; i < oldset->count; i++) {
        ret_maj = gss_add_oid_set_member(&ret_min, &oldset->elements[i], &n);
        if (ret_maj) {
            *minor_status = ret_min;
            gss_release_oid_set(&ret_min, &n);
            return ret_maj;
        }
    }

    *newset = n;
    *minor_status = 0;
    return GSS_S_COMPLETE;
}

static uint32_t gpm_copy_gss_buffer(uint32_t *minor_status,
                                    gss_buffer_t oldbuf,
                                    gss_buffer_t newbuf)
{
    if (!oldbuf || oldbuf->length == 0) {
        newbuf->value = NULL;
        newbuf->length = 0;
        *minor_status = 0;
        return GSS_S_COMPLETE;
    }

    newbuf->value = malloc(oldbuf->length);
    if (!newbuf->value) {
        *minor_status = ENOMEM;
        return GSS_S_FAILURE;
    }
    memcpy(newbuf->value, oldbuf->value, oldbuf->length);
    newbuf->length = oldbuf->length;

    *minor_status = 0;
    return GSS_S_COMPLETE;
}

static void gpmint_indicate_mechs(void)
{
    union gp_rpc_arg uarg;
    union gp_rpc_res ures;
    gssx_arg_indicate_mechs *arg = &uarg.indicate_mechs;
    gssx_res_indicate_mechs *res = &ures.indicate_mechs;
    struct gpm_mech_info *gi;
    struct gpm_mech_attr *ga;
    gssx_mech_info *mi;
    gssx_mech_attr *ma;
    uint32_t discard;
    uint32_t ret_min;
    uint32_t ret_maj = 0;
    int ret = 0;

    memset(arg, 0, sizeof(gssx_arg_indicate_mechs));
    memset(res, 0, sizeof(gssx_res_indicate_mechs));

    /* ignore call_ctx for now */

    /* execute proxy request */
    ret = gpm_make_call(GSSX_INDICATE_MECHS, &uarg, &ures);
    if (ret) {
        goto done;
    }

    if (res->status.major_status) {
        gpm_save_status(&res->status);
        ret_min = res->status.minor_status;
        ret_maj = res->status.major_status;
        ret = 0;
        goto done;
    }

    ret_maj = gss_create_empty_oid_set(&ret_min, &global_mechs.mech_set);
    if (ret_maj) {
        goto done;
    }

    global_mechs.info = calloc(res->mechs.mechs_len,
                               sizeof(struct gpm_mech_info));
    if (!global_mechs.info) {
        ret_maj = GSS_S_FAILURE;
        ret_min = ENOMEM;
        goto done;
    }

    for (unsigned i = 0; i < res->mechs.mechs_len; i++) {
        mi = &res->mechs.mechs_val[i];
        gi = &global_mechs.info[i];

        ret = gp_conv_gssx_to_oid_alloc(&mi->mech,
                                        &gi->mech);
        if (ret) {
            goto done;
        }
        ret_maj = gss_add_oid_set_member(&ret_min, gi->mech,
                                         &global_mechs.mech_set);
        if (ret_maj) {
            goto done;
        }

        ret = gp_conv_gssx_to_oid_set(&mi->name_types,
                                      &gi->name_types);
        if (ret) {
            goto done;
        }
        ret = gp_conv_gssx_to_oid_set(&mi->mech_attrs,
                                      &gi->mech_attrs);
        if (ret) {
            goto done;
        }
        ret = gp_conv_gssx_to_oid_set(&mi->known_mech_attrs,
                                      &gi->known_mech_attrs);
        if (ret) {
            goto done;
        }
        ret = gp_conv_gssx_to_oid_set(&mi->cred_options,
                                      &gi->cred_options);
        if (ret) {
            goto done;
        }
        ret = gp_conv_gssx_to_oid_set(&mi->sec_ctx_options,
                                      &gi->sec_ctx_options);
        if (ret) {
            goto done;
        }
        ret = gp_conv_gssx_to_buffer_alloc(&mi->saslname_sasl_mech_name,
                                           &gi->saslname_sasl_mech_name);
        if (ret) {
            goto done;
        }
        ret = gp_conv_gssx_to_buffer_alloc(&mi->saslname_mech_name,
                                           &gi->saslname_mech_name);
        if (ret) {
            goto done;
        }
        ret = gp_conv_gssx_to_buffer_alloc(&mi->saslname_mech_desc,
                                           &gi->saslname_mech_desc);
        if (ret) {
            goto done;
        }
    }
    global_mechs.info_len = res->mechs.mechs_len;

    global_mechs.desc = calloc(res->mech_attr_descs.mech_attr_descs_len,
                               sizeof(struct gpm_mech_attr));
    if (!global_mechs.desc) {
        goto done;
    }

    for (unsigned i = 0; i < res->mech_attr_descs.mech_attr_descs_len; i++) {
        ma = &res->mech_attr_descs.mech_attr_descs_val[i];
        ga = &global_mechs.desc[i];

        ret = gp_conv_gssx_to_oid_alloc(&ma->attr, &ga->attr);
        if (ret) {
            goto done;
        }
        ret = gp_conv_gssx_to_buffer_alloc(&ma->name, &ga->name);
        if (ret) {
            goto done;
        }
        ret = gp_conv_gssx_to_buffer_alloc(&ma->short_desc, &ga->short_desc);
        if (ret) {
            goto done;
        }
        ret = gp_conv_gssx_to_buffer_alloc(&ma->long_desc, &ga->long_desc);
        if (ret) {
            goto done;
        }
    }
    global_mechs.desc_len = res->mech_attr_descs.mech_attr_descs_len;

    global_mechs.initialized = true;

done:
    if (ret || ret_maj) {
        for (unsigned i = 0; i < global_mechs.desc_len; i++) {
            ga = &global_mechs.desc[i];
            gss_release_oid(&discard, &ga->attr);
            gss_release_buffer(&discard, ga->name);
            gss_release_buffer(&discard, ga->short_desc);
            gss_release_buffer(&discard, ga->long_desc);
        }
        free(global_mechs.desc);
        global_mechs.desc = NULL;
        for (unsigned i = 0; i < global_mechs.info_len; i++) {
            gi = &global_mechs.info[i];
            gss_release_oid(&discard, &gi->mech);
            gss_release_oid_set(&discard, &gi->name_types);
            gss_release_oid_set(&discard, &gi->mech_attrs);
            gss_release_oid_set(&discard, &gi->known_mech_attrs);
            gss_release_oid_set(&discard, &gi->cred_options);
            gss_release_oid_set(&discard, &gi->sec_ctx_options);
            gss_release_buffer(&discard, gi->saslname_sasl_mech_name);
            gss_release_buffer(&discard, gi->saslname_mech_name);
            gss_release_buffer(&discard, gi->saslname_mech_desc);
        }
        free(global_mechs.info);
        global_mechs.info = NULL;
        gss_release_oid_set(&discard, &global_mechs.mech_set);
    }
    gpm_free_xdrs(GSSX_INDICATE_MECHS, &uarg, &ures);
}

static int gpmint_init_global_mechs(void)
{
    pthread_once(&indicate_mechs_once, gpmint_indicate_mechs);

    if (!global_mechs.initialized) {
        /* this is quite a corner case. It means the pthread_once() call
         * failed for some reason. In this case we need to use a mutex */

        pthread_mutex_lock(&global_mechs_lock);
        /* need to recheck once we acquired the lock, to avoid redoing
         * if we were stuck after another thread that already did it */
        if (!global_mechs.initialized) {
            gpmint_indicate_mechs();
        }
        pthread_mutex_unlock(&global_mechs_lock);

        if (!global_mechs.initialized) {
            /* if still it is not initialized, give up */
            return EIO;
        }
    }

    return 0;
}

/* GSSAPI requires some APIs to return "static" mechs that callers do not need
 * to free. So match a radom mech and return from our global "static" array */
int gpm_mech_to_static(gss_OID mech_type, gss_OID *mech_static)
{
    int ret;

    ret = gpmint_init_global_mechs();
    if (ret) {
        return ret;
    }

    *mech_static = GSS_C_NO_OID;
    for (size_t i = 0; i < global_mechs.mech_set->count; i++) {
        if (gss_oid_equal(&global_mechs.mech_set->elements[i], mech_type)) {
            *mech_static = &global_mechs.mech_set->elements[i];
            return 0;
        }
    }
    /* TODO: potentially in future add the mech to the list if missing */
    return ENOENT;
}

bool gpm_mech_is_static(gss_OID mech_type)
{
    if (global_mechs.mech_set) {
        for (size_t i = 0; i < global_mechs.mech_set->count; i++) {
            if (&global_mechs.mech_set->elements[i] == mech_type) {
                return true;
            }
        }
    }
    return false;
}

OM_uint32 gpm_indicate_mechs(OM_uint32 *minor_status, gss_OID_set *mech_set)
{
    uint32_t ret_min;
    uint32_t ret_maj;
    int ret;

    if (!minor_status) {
        return GSS_S_CALL_INACCESSIBLE_WRITE;
    }
    if (!mech_set) {
        *minor_status = 0;
        return GSS_S_CALL_INACCESSIBLE_WRITE;
    }

    ret= gpmint_init_global_mechs();
    if (ret) {
        *minor_status = ret;
        return GSS_S_FAILURE;
    }

    ret_maj = gpm_copy_gss_OID_set(&ret_min,
                                   global_mechs.mech_set,
                                   mech_set);
    *minor_status = ret_min;
    return ret_maj;
}

OM_uint32 gpm_inquire_names_for_mech(OM_uint32 *minor_status,
                                     gss_OID mech_type,
                                     gss_OID_set *mech_names)
{
    uint32_t ret_min;
    uint32_t ret_maj;

    if (!minor_status) {
        return GSS_S_CALL_INACCESSIBLE_WRITE;
    }
    if (!mech_names) {
        *minor_status = 0;
        return GSS_S_CALL_INACCESSIBLE_WRITE;
    }

    ret_min = gpmint_init_global_mechs();
    if (ret_min) {
        *minor_status = ret_min;
        return GSS_S_FAILURE;
    }

    for (unsigned i = 0; i < global_mechs.info_len; i++) {
        if (!gss_oid_equal(global_mechs.info[i].mech, mech_type)) {
            continue;
        }
        ret_maj = gpm_copy_gss_OID_set(&ret_min,
                                       global_mechs.info[i].name_types,
                                       mech_names);
        *minor_status = ret_min;
        return ret_maj;
    }

    *minor_status = 0;
    return GSS_S_BAD_MECH;
}

OM_uint32 gpm_inquire_mechs_for_name(OM_uint32 *minor_status,
                                     gssx_name *input_name,
                                     gss_OID_set *mech_types)
{
    uint32_t ret_min;
    uint32_t ret_maj;
    uint32_t discard;
    gss_OID_desc name_type;
    int present;

    if (!minor_status) {
        return GSS_S_CALL_INACCESSIBLE_WRITE;
    }
    if (!input_name || !mech_types) {
        *minor_status = 0;
        return GSS_S_CALL_INACCESSIBLE_WRITE;
    }

    ret_min = gpmint_init_global_mechs();
    if (ret_min) {
        *minor_status = ret_min;
        return GSS_S_FAILURE;
    }

    ret_maj = gss_create_empty_oid_set(&ret_min, mech_types);
    if (ret_maj) {
        goto done;
    }

    gp_conv_gssx_to_oid(&input_name->name_type, &name_type);
    for (unsigned i = 0; i < global_mechs.info_len; i++) {
        ret_maj = gss_test_oid_set_member(&ret_min, &name_type,
                                          global_mechs.info[i].name_types,
                                          &present);
        if (ret_maj) {
            /* skip on error */
            continue;
        }
        if (present) {
            ret_maj = gss_add_oid_set_member(&ret_min,
                                             global_mechs.info[i].mech,
                                             mech_types);
        }
        if (ret_maj) {
            goto done;
        }
    }

done:
    if (ret_maj) {
        gss_release_oid_set(&discard, mech_types);
        *minor_status = ret_min;
        return ret_maj;
    }
    *minor_status = 0;
    return GSS_S_COMPLETE;
}

OM_uint32 gpm_inquire_attrs_for_mech(OM_uint32 *minor_status,
                                     gss_OID mech,
                                     gss_OID_set *mech_attrs,
                                     gss_OID_set *known_mech_attrs)
{
    uint32_t ret_min;
    uint32_t ret_maj;
    uint32_t discard;

    if (!minor_status) {
        return GSS_S_CALL_INACCESSIBLE_WRITE;
    }

    ret_min = gpmint_init_global_mechs();
    if (ret_min) {
        *minor_status = ret_min;
        return GSS_S_FAILURE;
    }

    for (unsigned i = 0; i < global_mechs.info_len; i++) {
        if (!gss_oid_equal(global_mechs.info[i].mech, mech)) {
            continue;
        }

        if (mech_attrs != NULL) {
            ret_maj = gpm_copy_gss_OID_set(&ret_min,
                                           global_mechs.info[i].mech_attrs,
                                           mech_attrs);
            if (ret_maj) {
                *minor_status = ret_min;
                return ret_maj;
            }
        }

        if (known_mech_attrs != NULL) {
            ret_maj = gpm_copy_gss_OID_set(&ret_min,
                                           global_mechs.info[i].known_mech_attrs,
                                           known_mech_attrs);
            if (ret_maj) {
                gss_release_oid_set(&discard, known_mech_attrs);
            }
            *minor_status = ret_min;
            return ret_maj;
        }

        /* all requested attributes copied successfully */
        *minor_status = 0;
        return GSS_S_COMPLETE;
    }

    *minor_status = 0;
    return GSS_S_BAD_MECH;
}

OM_uint32 gpm_inquire_saslname_for_mech(OM_uint32 *minor_status,
                                        const gss_OID desired_mech,
                                        gss_buffer_t sasl_mech_name,
                                        gss_buffer_t mech_name,
                                        gss_buffer_t mech_description)
{
    uint32_t ret_min;
    uint32_t ret_maj;
    uint32_t discard;

    if (!minor_status) {
        return GSS_S_CALL_INACCESSIBLE_WRITE;
    }
    if (!sasl_mech_name || !mech_name || !mech_description) {
        *minor_status = 0;
        return GSS_S_CALL_INACCESSIBLE_WRITE;
    }

    ret_min = gpmint_init_global_mechs();
    if (ret_min) {
        *minor_status = ret_min;
        return GSS_S_FAILURE;
    }

    for (unsigned i = 0; i < global_mechs.info_len; i++) {
        if (!gss_oid_equal(global_mechs.info[i].mech, desired_mech)) {
            continue;
        }
        ret_maj = gpm_copy_gss_buffer(&ret_min,
                                global_mechs.info[i].saslname_sasl_mech_name,
                                sasl_mech_name);
        if (ret_maj) {
            *minor_status = ret_min;
            return ret_maj;
        }
        ret_maj = gpm_copy_gss_buffer(&ret_min,
                                global_mechs.info[i].saslname_mech_name,
                                mech_name);
        if (ret_maj) {
            gss_release_buffer(&discard, sasl_mech_name);
            *minor_status = ret_min;
            return ret_maj;
        }
        ret_maj = gpm_copy_gss_buffer(&ret_min,
                                global_mechs.info[i].saslname_mech_desc,
                                mech_description);
        if (ret_maj) {
            gss_release_buffer(&discard, sasl_mech_name);
            gss_release_buffer(&discard, mech_name);
        }
        *minor_status = ret_min;
        return ret_maj;
    }

    *minor_status = 0;
    return GSS_S_BAD_MECH;
}

OM_uint32 gpm_display_mech_attr(OM_uint32 *minor_status,
                                gss_const_OID mech_attr,
                                gss_buffer_t name,
                                gss_buffer_t short_desc,
                                gss_buffer_t long_desc)
{
    uint32_t ret_min;
    uint32_t ret_maj;
    uint32_t discard;

    if (!minor_status) {
        return GSS_S_CALL_INACCESSIBLE_WRITE;
    }
    if (!name || !short_desc || !long_desc) {
        *minor_status = 0;
        return GSS_S_CALL_INACCESSIBLE_WRITE;
    }

    ret_min = gpmint_init_global_mechs();
    if (ret_min) {
        *minor_status = ret_min;
        return GSS_S_FAILURE;
    }

    for (unsigned i = 0; i < global_mechs.desc_len; i++) {
        if (!gss_oid_equal(global_mechs.desc[i].attr, mech_attr)) {
            continue;
        }
        ret_maj = gpm_copy_gss_buffer(&ret_min,
                                      global_mechs.desc[i].name,
                                      name);
        if (ret_maj) {
            *minor_status = ret_min;
            return ret_maj;
        }
        ret_maj = gpm_copy_gss_buffer(&ret_min,
                                      global_mechs.desc[i].short_desc,
                                      short_desc);
        if (ret_maj) {
            gss_release_buffer(&discard, name);
            *minor_status = ret_min;
            return ret_maj;
        }
        ret_maj = gpm_copy_gss_buffer(&ret_min,
                                      global_mechs.desc[i].long_desc,
                                      long_desc);
        if (ret_maj) {
            gss_release_buffer(&discard, name);
            gss_release_buffer(&discard, short_desc);
        }
        *minor_status = ret_min;
        return ret_maj;
    }

    *minor_status = 0;
    return GSS_S_BAD_MECH;
}

OM_uint32 gpm_indicate_mechs_by_attrs(OM_uint32 *minor_status,
                                      gss_const_OID_set desired_mech_attrs,
                                      gss_const_OID_set except_mech_attrs,
                                      gss_const_OID_set critical_mech_attrs,
                                      gss_OID_set *mechs)
{
    uint32_t ret_min;
    uint32_t ret_maj;
    uint32_t discard;
    int present;

    if (!minor_status) {
        return GSS_S_CALL_INACCESSIBLE_WRITE;
    }
    if (!mechs) {
        *minor_status = 0;
        return GSS_S_CALL_INACCESSIBLE_WRITE;
    }

    ret_min = gpmint_init_global_mechs();
    if (ret_min) {
        *minor_status = ret_min;
        return GSS_S_FAILURE;
    }

    ret_maj = gss_create_empty_oid_set(&ret_min, mechs);
    if (ret_maj) {
        *minor_status = ret_min;
        return ret_maj;
    }

    for (unsigned i = 0; i < global_mechs.info_len; i++) {
        if (desired_mech_attrs != GSS_C_NO_OID_SET) {
            unsigned j;
            for (j = 0; j < desired_mech_attrs->count; j++) {
                ret_maj = gss_test_oid_set_member(&ret_min,
                                            &desired_mech_attrs->elements[j],
                                            global_mechs.info[i].mech_attrs,
                                            &present);
                if (ret_maj) {
                    /* skip in case of errors */
                    break;
                }
                if (!present) {
                    break;
                }
            }
            /* if not desired skip */
            if (j != desired_mech_attrs->count) {
                continue;
            }
        }
        if (except_mech_attrs != GSS_C_NO_OID_SET) {
            unsigned j;
            for (j = 0; j < except_mech_attrs->count; j++) {
                ret_maj = gss_test_oid_set_member(&ret_min,
                                            &except_mech_attrs->elements[j],
                                            global_mechs.info[i].mech_attrs,
                                            &present);
                if (ret_maj) {
                    /* continue in case of errors */
                    continue;
                }
                if (present) {
                    break;
                }
            }
            /* if excepted skip */
            if (j == except_mech_attrs->count) {
                continue;
            }
        }
        if (critical_mech_attrs != GSS_C_NO_OID_SET) {
            unsigned j;
            for (j = 0; j < critical_mech_attrs->count; j++) {
                ret_maj = gss_test_oid_set_member(&ret_min,
                                    &critical_mech_attrs->elements[j],
                                    global_mechs.info[i].known_mech_attrs,
                                    &present);
                if (ret_maj) {
                    /* skip in case of errors */
                    break;
                }
                if (!present) {
                    break;
                }
            }
            /* if not known skip */
            if (j != critical_mech_attrs->count) {
                continue;
            }
        }

        /* passes all tests, add to list */
        ret_maj = gss_add_oid_set_member(&ret_min,
                                         global_mechs.info[i].mech, mechs);
        if (ret_maj) {
            goto done;
        }
    }

done:
    if (ret_maj) {
        gss_release_oid_set(&discard, mechs);
        *minor_status = ret_min;
        return ret_maj;
    }
    *minor_status = 0;
    return GSS_S_COMPLETE;
}
