/*
 * FreeIPA 2FA companion daemon
 *
 * Authors: Sumit Bose <sbose@redhat.com>
 *
 * Copyright (C) 2021 Red Hat
 * see file 'COPYING' for use and warranty information
 *
 * This program is free software you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * 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, see <http://www.gnu.org/licenses/>.
 */

/*
 * This file reaches out to a third-party IdP to handle an OAuth2
 * authentication request (stdio.c/query.c) if the user is configured
 * accordingly. The result is placed in the stdout queue (stdio.c).
 */

#include <krb5/krb5.h>
#include <stdbool.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/random.h>

#include "internal.h"

#define OIDC_CHILD_PATH "/usr/libexec/sssd/oidc_child"

struct child_ctx {
    int read_from_child;
    int write_to_child;
    verto_ev *read_ev;
    verto_ev *write_ev;
    verto_ev *child_ev;
    struct otpd_queue_item *item;
    struct otpd_queue_item *saved_item;
    enum oauth2_state oauth2_state;
};

static int set_fd_nonblocking(int fd)
{
    int flags;
    int ret;

    flags = fcntl(fd, F_GETFL, 0);
    if (flags == -1) {
        ret = errno;
        return ret;
    }

    if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
        ret = errno;
        return ret;
    }

    return 0;
}

static void free_child_ctx(verto_ctx *vctx, verto_ev *ev)
{
    (void)vctx; /* Unused */
    struct child_ctx *child_ctx;

    child_ctx = verto_get_private(ev);

    free(child_ctx);
}

static void oauth2_on_child_exit(verto_ctx *vctx, verto_ev *ev)
{
    (void)vctx; /* Unused */
    verto_proc_status st;

    st = verto_get_proc_status(ev);

    /* The krad req might not be available at this stage anymore, so
     * otpd_log_err() is used. */
    otpd_log_err(0, "Child finished with status [%d].", WEXITSTATUS(st));
}

static void oauth2_on_child_writable(verto_ctx *vctx, verto_ev *ev)
{
    (void)vctx; /* Unused */
    ssize_t io;
    struct child_ctx *child_ctx;

    child_ctx = verto_get_private(ev);
    if (child_ctx == NULL) {
        otpd_log_err(EINVAL, "Lost child context");
        verto_del(ev);
        return;
    }

    if (child_ctx->oauth2_state == OAUTH2_GET_DEVICE_CODE) {
        /* no input needed */
        verto_del(ev);
        return;
    }


    io = write(verto_get_fd(ev),
               child_ctx->saved_item->oauth2.device_code_reply,
               strlen(child_ctx->saved_item->oauth2.device_code_reply));
    otpd_queue_item_free(child_ctx->saved_item);

    if (io < 0) {
        switch (errno) {
#if defined(EWOULDBLOCK) && (!defined(EAGAIN) || EAGAIN - EWOULDBLOCK != 0)
        case EWOULDBLOCK:
#endif
#if defined(EAGAIN)
        case EAGAIN:
#endif
        case ENOBUFS:
        case EINTR:
            /* In this case, we just need to try again. */
            return;
        default:
            /* Unrecoverable. */
            break;
        }
        otpd_log_err(errno, "Failed to send to child");
    }

    verto_del(ev);
}

#define min(a,b) ((a) > (b) ? (b) : (a))
static int add_krad_attr_to_set(struct child_ctx *child_ctx,
                                krad_attrset *attrset,
                                krb5_data *datap,
                                krad_attr attr, const char *message)
{
    krb5_data state = {0};
    char *p = datap->data;
    unsigned int len = datap->length;
    int ret = 0;

    do {
        state.data = p;
        state.length = min(MAX_ATTRSIZE - 5, len);
        p += state.length;

        ret = krad_attrset_add(attrset, attr, &(state));
        if (ret != 0) {
            otpd_log_req(child_ctx->item->req, message);
            break;
        }
        len -= state.length;
    } while (len > 0);

    return ret;
}

/* Most attributes have limited length (MAX_ATTRSIZE). In order to accept longer
 * values, we will concatenate all the attribute values to single krb5_data. */
static int get_krad_attr_from_packet(const krad_packet *rres,
                                     krad_attr attr, krb5_data *_data)
{
    const krb5_data *rmsg;
    krb5_data data = {0};
    unsigned int memindex;
    unsigned int i;

    i = 0;
    do {
        rmsg = krad_packet_get_attr(rres, attr, i);
        if (rmsg != NULL) {
            data.length += rmsg->length;
        }
        i++;
    } while (rmsg != NULL);

    if (data.length == 0) {
        return ENOENT;
    }

    data.data = malloc(data.length);
    if (data.data == NULL) {
        return ENOMEM;
    }

    i = 0;
    memindex = 0;
    do {
        rmsg = krad_packet_get_attr(rres, attr, i);
        if (rmsg != NULL) {
            memcpy(&data.data[memindex], rmsg->data, rmsg->length);
            memindex += rmsg->length;
        }
        i++;
    } while (rmsg != NULL);

    if (memindex != data.length) {
        free(data.data);
        return ERANGE;
    }

    *_data = data;

    return 0;
}

/* oidc_child will return two lines.
 * The first is a JSON formatted string containing the device code and other
 * data needed to get the access token in the second round. This will be
 * returned to the caller as Radius Proxy-State so that the caller will send
 * it back in the next round.
 * The second line is the string expected by the krb5 oauth2 pre-auth plugin
 * and will be send to the caller as Radius Reply-Message.
 */
static int handle_device_code_reply(struct child_ctx *child_ctx,
                                    const char *dc_reply, char *rad_reply)
{
    krad_attrset *attrset = NULL;
    int ret;
    krb5_data data = { 0 };
    struct otpd_queue_item *state_item;

    ret = otpd_queue_item_new(NULL, &state_item);
    if (ret != 0) {
        otpd_log_req(child_ctx->item->req, "Failed to allocate state item");
        goto done;
    }

    state_item->oauth2.device_code_reply = strdup(dc_reply);
    if (state_item->oauth2.device_code_reply == NULL) {
        otpd_log_req(child_ctx->item->req, "Failed to copy device code reply.");
        goto done;
    }

    ret = krad_attrset_new(ctx.kctx, &attrset);
    if (ret != 0) {
        otpd_log_req(child_ctx->item->req,
                     "Failed to create radius attribute set");
        goto done;
    }

    state_item->oauth2.state.magic = 0;

    state_item->oauth2.state.data = strdup(dc_reply);
    if (state_item->oauth2.state.data == NULL) {
        otpd_log_req(child_ctx->item->req,
                     "Failed to copy device code reply to krad.");
        goto done;
    }
    state_item->oauth2.state.length = strlen(dc_reply);

    ret = add_krad_attr_to_set(child_ctx, attrset, &(state_item->oauth2.state),
                               krad_attr_name2num("Proxy-State"),
                               "Failed to serialize state to attribute set");
    if (ret != 0) {
        goto done;
    }

    data.magic = 0;
    data.data = rad_reply;
    data.length = strlen(rad_reply);
    ret = add_krad_attr_to_set(child_ctx, attrset, &data,
                               krad_attr_name2num("Reply-Message"),
                               "Failed to serialize reply to attribute set");
    if (ret != 0) {
        goto done;
    }

    ret = krad_packet_new_response(ctx.kctx, SECRET,
                                   krad_code_name2num("Access-Challenge"),
                                   attrset,
                                   child_ctx->item->req, &child_ctx->item->rsp);
    if (ret != 0) {
        otpd_log_err(ret, "Failed to create radius response");
        child_ctx->item->rsp = NULL;
    }

    otpd_queue_push(&ctx.oauth2_state.states, state_item);

    ret = 0;
done:
    krad_attrset_free(attrset);
    if (ret != 0) {
        if (state_item != NULL) {
            free(state_item->oauth2.state.data);
            free(state_item->oauth2.device_code_reply);
            free(state_item);
        }
    }

    return ret;
}

static int check_access_token_reply(struct child_ctx *child_ctx,
                                    const char *buf, size_t len)
{
    int ret;

    if (strlen(child_ctx->item->user.ipaidpSub) != len
            || memcmp(child_ctx->item->user.ipaidpSub, buf, len) != 0) {
        return EPERM;
    }

    ret = krad_packet_new_response(ctx.kctx, SECRET,
                                   krad_code_name2num("Access-Accept"), NULL,
                                   child_ctx->item->req, &child_ctx->item->rsp);
    if (ret != 0) {
        otpd_log_err(ret, "Failed to create radius response");
        child_ctx->item->rsp = NULL;
    }

    return ret;
}

static void oauth2_on_child_readable(verto_ctx *vctx, verto_ev *ev)
{
    static char buf[10240];
    ssize_t io = 0;
    struct child_ctx *child_ctx = NULL;
    int ret;
    char *rad_reply;
    char *end;

    (void) vctx; /* Unused */

    child_ctx = (struct child_ctx *) verto_get_private(ev);
    if (child_ctx == NULL) {
        otpd_log_err(EINVAL, "Lost child context");
        verto_del(ev);
        return;
    }
    /* Make sure ctx.stdio.responses will at least return an error */
    child_ctx->item->rsp = NULL;
    child_ctx->item->sent = 0;

    io = read(verto_get_fd(ev), buf, 10240);
    if (io < 0) {
        otpd_log_err(errno, "Failed to read from child");
        goto done;
    }

    if (io >= 0) {
        buf[io] = '\0';
        otpd_log_req(child_ctx->item->req, "Received: [%s]", buf);
    }

    verto_del(ev);

    if (child_ctx->oauth2_state == OAUTH2_GET_DEVICE_CODE) {
        /* expect 2 lines of output. First the orginal JSON string return by
         * the IdP from the devicecode request which will be used as input to
         * the child process in the second run. Second the JSON string returned
         * in the radius reply. */

        rad_reply = memchr(buf, '\n', io);
        if (rad_reply != NULL) {
            *rad_reply = '\0';
            rad_reply++;
            end = memchr(rad_reply, '\n', io - (rad_reply - 1 - buf));
            if (end == NULL) {
                otpd_log_req(child_ctx->item->req, "Missing second new-line.");
                goto done;
            }
            *end = '\0';

            ret = handle_device_code_reply(child_ctx, buf, rad_reply);
            if (ret != 0) {
                otpd_log_req(child_ctx->item->req,
                             "Failed to handle device code reply.");
            }
        }
    } else if (child_ctx->oauth2_state == OAUTH2_GET_ACCESS_TOKEN) {
        ret = check_access_token_reply(child_ctx, buf, (size_t) io);
        if (ret != 0) {
                otpd_log_req(child_ctx->item->req,
                             "Failed to check access token reply.");
        }
    } else {
        /* error */
        otpd_log_req(child_ctx->item->req, "Unexpected state [%d].",
                                           child_ctx->oauth2_state);
    }

done:
    otpd_queue_push(&ctx.stdio.responses, child_ctx->item);
    verto_set_flags(ctx.stdio.writer, VERTO_EV_FLAG_PERSIST |
                                      VERTO_EV_FLAG_IO_ERROR |
                                      VERTO_EV_FLAG_IO_READ |
                                      VERTO_EV_FLAG_IO_WRITE);
}

static const char *oauth2_state_to_str(enum oauth2_state oauth2_state)
{
    switch (oauth2_state) {
    case OAUTH2_NO:
        return "OAuth2 not available";
        break;
    case OAUTH2_GET_ISSUER:
        return "Get issuer from LDAP";
        break;
    case OAUTH2_GET_DEVICE_CODE:
        return "Get device code";
        break;
    case OAUTH2_GET_ACCESS_TOKEN:
        return "Get access token";
        break;
    default:
        return "Unknown OAuth2 state";
    }
}

int oauth2(struct otpd_queue_item **item, enum oauth2_state oauth2_state)
{
    int ret;
    pid_t child_pid;
    int pipefd_to_child[2] = { -1, -1};
    int pipefd_from_child[2] = { -1, -1};
    struct child_ctx *child_ctx;
    /* Up to 50 arguments to the helper supported. The amount of arguments
     * is controlled inside this function. Right now max used is below 20 */
    char *args[50] = {NULL};
    size_t args_idx = 0;
    krb5_data data_state = {0};
    struct otpd_queue_item *saved_item = NULL;

    if (oauth2_state != OAUTH2_GET_DEVICE_CODE
                && oauth2_state != OAUTH2_GET_ACCESS_TOKEN) {
        otpd_log_req((*item)->req, "Unexpected OAuth2 state [%d][%s]",
                     oauth2_state, oauth2_state_to_str(oauth2_state));
        return EINVAL;
    }

    if (oauth2_state == OAUTH2_GET_ACCESS_TOKEN) {
        ret = get_krad_attr_from_packet((*item)->req,
                                               krad_attr_name2num("Proxy-State"),
                                               &data_state);
        if ((ret != 0) || (data_state.length == 0)) {
            otpd_log_req((*item)->req, "Missing Radius Proxy-State attribute");
            return EINVAL;
        }

        saved_item = calloc(sizeof(struct otpd_queue_item), 1);
        if (saved_item == NULL) {
            otpd_log_req((*item)->req, "No matching saved state found");
            return EINVAL;
        }
        saved_item->oauth2.device_code_reply = strndup(data_state.data,
                                                       data_state.length);
        if (saved_item->oauth2.device_code_reply == NULL) {
            otpd_log_req((*item)->req, "Failed to copy device code reply");
            return EINVAL;
        }
        krb5_free_data_contents(NULL, &data_state);
    }

    child_ctx = calloc(sizeof(struct child_ctx), 1);
    if (child_ctx == NULL) {
        ret = ENOMEM;
        goto done;
    }
    child_ctx->item = (*item);
    child_ctx->saved_item = saved_item;
    child_ctx->oauth2_state = oauth2_state;

    otpd_log_req((*item)->req, "oauth2 start: %s",
                               oauth2_state_to_str(oauth2_state));

    args[args_idx++] = OIDC_CHILD_PATH;

    if (oauth2_state == OAUTH2_GET_DEVICE_CODE) {
        args[args_idx++] = "--get-device-code";
    } else {
        args[args_idx++] = "--get-access-token";
    }

    if ((*item)->idp.ipaidpIssuerURL != NULL) {
        args[args_idx++] = "--issuer-url";
        args[args_idx++] = (*item)->idp.ipaidpIssuerURL;
    } else {
        args[args_idx++] = "--device-auth-endpoint";
        args[args_idx++] = (*item)->idp.ipaidpDevAuthEndpoint;

        args[args_idx++] = "--token-endpoint";
        args[args_idx++] = (*item)->idp.ipaidpTokenEndpoint;

        args[args_idx++] = "--userinfo-endpoint";
        args[args_idx++] = (*item)->idp.ipaidpUserInfoEndpoint;

        if ((*item)->idp.ipaidpKeysEndpoint) {
            args[args_idx++] = "--jwks-uri";
            args[args_idx++] = (*item)->idp.ipaidpKeysEndpoint;
        }

    }

    args[args_idx++] = "--client-id";
    args[args_idx++] = (*item)->idp.ipaidpClientID;

    if ((*item)->idp.ipaidpClientSecret) {
        args[args_idx++] = "--client-secret";
        args[args_idx++] = (*item)->idp.ipaidpClientSecret;
    }

    if ((*item)->idp.ipaidpScope) {
        args[args_idx++] = "--scope";
        args[args_idx++] = (*item)->idp.ipaidpScope;
    }

    if ((*item)->idp.ipaidpSub) {
        args[args_idx++] = "--user-identifier-attribute";
        args[args_idx++] = (*item)->idp.ipaidpSub;
    }

    if ((*item)->idp.ipaidpDebugLevelStr != NULL) {
        args[args_idx++] = "--debug-level";
        args[args_idx++] = (*item)->idp.ipaidpDebugLevelStr;
    }

    if ((*item)->idp.ipaidpDebugCurl) {
        args[args_idx++] = "--libcurl-debug";
    }

#if 0
    for (int i; args[i]; i++) {
        otpd_log_req((*item)->req, "oidc_child exec: %s", args[i]);
    }
#endif

    ret = pipe(pipefd_from_child);
    if (ret == -1) {
        ret = errno;
        goto done;
    }
    ret = pipe(pipefd_to_child);
    if (ret == -1) {
        ret = errno;
        goto done;
    }

    child_pid = fork();

    if (child_pid == 0) { /* child */
        close(pipefd_to_child[1]);
        ret = dup2(pipefd_to_child[0], STDIN_FILENO);
        if (ret == -1) {
            exit(EXIT_FAILURE);
        }

        close(pipefd_from_child[0]);
        ret = dup2(pipefd_from_child[1], STDOUT_FILENO);
        if (ret == -1) {
            exit(EXIT_FAILURE);
        }

        execv(OIDC_CHILD_PATH, args);
        exit(EXIT_FAILURE);
    } else if (child_pid > 0) { /* parent */
        close(pipefd_to_child[0]);
        set_fd_nonblocking(pipefd_to_child[1]);
        child_ctx->write_to_child = pipefd_to_child[1];

        close(pipefd_from_child[1]);
        set_fd_nonblocking(pipefd_from_child[0]);
        child_ctx->read_from_child = pipefd_from_child[0];

        child_ctx->write_ev = verto_add_io(ctx.vctx, VERTO_EV_FLAG_PERSIST |
                                                     VERTO_EV_FLAG_IO_CLOSE_FD |
                                                     VERTO_EV_FLAG_IO_ERROR |
                                                     VERTO_EV_FLAG_IO_WRITE,
                                                     oauth2_on_child_writable,
                                                     child_ctx->write_to_child);
        if (child_ctx->write_ev == NULL) {
            ret = ENOMEM;
            otpd_log_err(ret, "Unable to initialize oauth2 writer event");
            goto done;
        }
        verto_set_private(child_ctx->write_ev, child_ctx, NULL);

        child_ctx->read_ev = verto_add_io(ctx.vctx, VERTO_EV_FLAG_PERSIST |
                                                    VERTO_EV_FLAG_IO_CLOSE_FD |
                                                    VERTO_EV_FLAG_IO_ERROR |
                                                    VERTO_EV_FLAG_IO_READ,
                                                    oauth2_on_child_readable,
                                                    child_ctx->read_from_child);
        if (child_ctx->read_ev == NULL) {
            ret = ENOMEM;
            otpd_log_err(ret, "Unable to initialize oauth2 writer event");
            goto done;
        }
        verto_set_private(child_ctx->read_ev, child_ctx, NULL);

        child_ctx->child_ev = verto_add_child(ctx.vctx, VERTO_EV_FLAG_NONE,
                                              oauth2_on_child_exit, child_pid);
        verto_set_private(child_ctx->child_ev, child_ctx, free_child_ctx);

    } else { /* error */
        ret = errno;
        otpd_log_err(ret, "Failed to fork oidc_child");
        goto done;
    }

    ret = 0;
done:
    if (ret == 0) {
        *item = NULL;
    }

    return ret;
}
