/*
   GSS-PROXY

   Copyright (C) 2011 Red Hat, Inc.
   Copyright (C) 2011 Simo Sorce <simo.sorce@redhat.com>

   Permission is hereby granted, free of charge, to any person obtaining a
   copy of this software and associated documentation files (the "Software"),
   to deal in the Software without restriction, including without limitation
   the rights to use, copy, modify, merge, publish, distribute, sublicense,
   and/or sell copies of the Software, and to permit persons to whom the
   Software is furnished to do so, subject to the following conditions:

   The above copyright notice and this permission notice shall be included in
   all copies or substantial portions of the Software.

   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
   THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
   DEALINGS IN THE SOFTWARE.
*/

#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;
    int i;

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

    for (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 bool gpm_equal_oids(gss_const_OID a, gss_const_OID b)
{
    int ret;

    if (a->length == b->length) {
        ret = memcmp(a->elements, b->elements, a->length);
        if (ret == 0) {
            return true;
        }
    }

    return false;
}

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;
    int i;

    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 (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 (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 (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 (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;
}

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;
    int i;

    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 (i = 0; i < global_mechs.info_len; i++) {
        if (!gpm_equal_oids(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 name_type = GSS_C_NO_OID;
    int present;
    int i;

    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_min = gp_conv_gssx_to_oid_alloc(&input_name->name_type, &name_type);
    if (ret_min) {
        ret_maj = GSS_S_FAILURE;
        goto done;
    }

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

    for (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:
    gss_release_oid(&discard, &name_type);
    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;
    int i;

    if (!minor_status) {
        return GSS_S_CALL_INACCESSIBLE_WRITE;
    }
    if (!mech_attrs || !known_mech_attrs) {
        *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 (i = 0; i < global_mechs.info_len; i++) {
        if (!gpm_equal_oids(global_mechs.info[i].mech, mech)) {
            continue;
        }
        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;
        }
        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;
    }

    *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;
    int i;

    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 (i = 0; i < global_mechs.info_len; i++) {
        if (!gpm_equal_oids(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;
    int i;

    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 (i = 0; i < global_mechs.desc_len; i++) {
        if (!gpm_equal_oids(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;
    int i, j;

    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 (i = 0; i < global_mechs.info_len; i++) {
        if (desired_mech_attrs != GSS_C_NO_OID_SET) {
            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) {
            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) {
            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;
}
