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

#include "gss_plugin.h"

static OM_uint32 init_ctx_local(OM_uint32 *minor_status,
                                struct gpp_cred_handle *cred_handle,
                                struct gpp_context_handle *ctx_handle,
                                struct gpp_name_handle *name,
                                gss_OID mech_type,
                                OM_uint32 req_flags,
                                OM_uint32 time_req,
                                gss_channel_bindings_t input_cb,
                                gss_buffer_t input_token,
                                gss_OID *actual_mech_type,
                                gss_buffer_t output_token,
                                OM_uint32 *ret_flags,
                                OM_uint32 *time_rec)
{
    OM_uint32 maj, min;

    if (name->remote && !name->local) {
        maj = gpp_name_to_local(&min, name->remote,
                                mech_type, &name->local);
        if (maj) {
            goto done;
        }
    }

    maj = gss_init_sec_context(&min,
                               cred_handle->local,
                               &ctx_handle->local,
                               name->local,
                               gpp_special_mech(mech_type),
                               req_flags,
                               time_req,
                               input_cb,
                               input_token,
                               actual_mech_type,
                               output_token,
                               ret_flags,
                               time_rec);

done:
    *minor_status = min;
    return maj;
}

OM_uint32 gssi_init_sec_context(OM_uint32 *minor_status,
                                gss_cred_id_t claimant_cred_handle,
                                gss_ctx_id_t *context_handle,
                                gss_name_t target_name,
                                gss_OID mech_type,
                                OM_uint32 req_flags,
                                OM_uint32 time_req,
                                gss_channel_bindings_t input_cb,
                                gss_buffer_t input_token,
                                gss_OID *actual_mech_type,
                                gss_buffer_t output_token,
                                OM_uint32 *ret_flags,
                                OM_uint32 *time_rec)
{
    enum gpp_behavior behavior = GPP_UNINITIALIZED;
    struct gpp_context_handle *ctx_handle = NULL;
    struct gpp_cred_handle *cred_handle = NULL;
    gssx_cred *out_cred = NULL;
    struct gpp_name_handle *name;
    OM_uint32 tmaj, tmin;
    OM_uint32 maj, min;

    GSSI_TRACE();

    *minor_status = 0;

    if (target_name == GSS_C_NO_NAME) {
        return GSS_S_CALL_INACCESSIBLE_READ;
    }

    if (mech_type == GSS_C_NO_OID || gpp_is_special_oid(mech_type)) {
        return GSS_S_BAD_MECH;
    }

    tmaj = GSS_S_COMPLETE;
    tmin = 0;

    if (*context_handle) {
        ctx_handle = (struct gpp_context_handle *)*context_handle;
        if (ctx_handle->local) {
            /* ok this means a previous call decided to use the local mech,
             * so let's just re-enter the mechglue here and keep at it */
            behavior = GPP_LOCAL_ONLY;
        }
    } else {
        ctx_handle = calloc(1, sizeof(struct gpp_context_handle));
        if (!ctx_handle) {
            maj = GSS_S_FAILURE;
            min = ENOMEM;
            goto done;
        }
    }

    if (claimant_cred_handle != GSS_C_NO_CREDENTIAL) {
        cred_handle = (struct gpp_cred_handle *)claimant_cred_handle;
        if (cred_handle->local) {
            /* ok this means a previous call decided to short circuit to the
             * local mech, so let's just re-enter the mechglue here, as we
             * have no way to export creds yet. */
            behavior = GPP_LOCAL_ONLY;
        } else if (behavior == GPP_LOCAL_ONLY) {
            maj = GSS_S_DEFECTIVE_CREDENTIAL;
            min = 0;
            goto done;
        }
    } else {
        maj = gpp_cred_handle_init(&min, true, NULL, &cred_handle);
        if (maj) {
            goto done;
        }
    }

    name = (struct gpp_name_handle *)target_name;
    if (behavior == GPP_UNINITIALIZED) {
        behavior = gpp_get_behavior();
    }

    /* See if we should try local first */
    if (behavior == GPP_LOCAL_ONLY || behavior == GPP_LOCAL_FIRST) {

        maj = init_ctx_local(&min, cred_handle, ctx_handle, name,
                              mech_type, req_flags, time_req, input_cb,
                              input_token, actual_mech_type, output_token,
                              ret_flags, time_rec);

        if (maj == GSS_S_COMPLETE || maj == GSS_S_CONTINUE_NEEDED ||
            behavior == GPP_LOCAL_ONLY) {
            goto done;
        }

        /* not successful, save actual local error if remote fallback fails */
        tmaj = maj;
        tmin = min;
    }

    /* Then try with remote */
    if (behavior != GPP_LOCAL_ONLY) {
        if (name->local && !name->remote) {
            maj = gpp_local_to_name(&min, name->local, &name->remote);
            if (maj) {
                goto done;
            }
        }

        if (!cred_handle->remote) {
            /* we ignore failures here */
            (void)gppint_get_def_creds(&min, GPP_REMOTE_ONLY, NULL,
                                       GSS_C_INITIATE, &cred_handle);
        }

        maj = gpm_init_sec_context(&min,
                                   cred_handle->remote,
                                   &ctx_handle->remote,
                                   name->remote,
                                   mech_type,
                                   req_flags,
                                   time_req,
                                   input_cb,
                                   input_token,
                                   actual_mech_type,
                                   output_token,
                                   ret_flags,
                                   time_rec,
                                   &out_cred);
        if (maj == GSS_S_COMPLETE || maj == GSS_S_CONTINUE_NEEDED) {
            if (out_cred) {
                if (cred_handle->remote) {
                    xdr_free((xdrproc_t)xdr_gssx_cred,
                             (char *)cred_handle->remote);
                    free(cred_handle->remote);
                }
                cred_handle->remote = out_cred;
                out_cred = NULL;
                /* failuire is not fatal */
                (void)gpp_store_remote_creds(&tmin,
                                             cred_handle->default_creds,
                                             &cred_handle->store,
                                             cred_handle->remote);
            }
            goto done;
        }

        if (behavior == GPP_REMOTE_FIRST) {
            /* So remote failed, but we can fallback to local, try that */
            maj = init_ctx_local(&min, cred_handle, ctx_handle, name,
                                 mech_type, req_flags, time_req, input_cb,
                                 input_token, actual_mech_type, output_token,
                                 ret_flags, time_rec);
        }
    }

done:
    if (maj != GSS_S_COMPLETE &&
        maj != GSS_S_CONTINUE_NEEDED &&
        tmaj != GSS_S_COMPLETE) {
        maj = tmaj;
        min = tmin;
    }
    if (maj != GSS_S_COMPLETE && maj != GSS_S_CONTINUE_NEEDED) {
        if (ctx_handle &&
            ctx_handle->local == GSS_C_NO_CONTEXT &&
            ctx_handle->remote == NULL) {
            free(ctx_handle);
            ctx_handle = NULL;
        }
        *minor_status = gpp_map_error(min);
    }
    /* always replace the provided context handle to avoid
     * dangling pointers when a context has been passed in */
    *context_handle = (gss_ctx_id_t)ctx_handle;

    if (claimant_cred_handle == GSS_C_NO_CREDENTIAL) {
        (void)gpp_cred_handle_free(&min, cred_handle);
    }
    return maj;
}
