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

#include "gssapi_gpm.h"
#include <sys/types.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdlib.h>
#include <time.h>
#include <pthread.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <sys/random.h>
#include <sys/timerfd.h>

#define FRAGMENT_BIT (1 << 31)

#define RESPONSE_TIMEOUT 15
#define SAFETY_TIMEOUT RESPONSE_TIMEOUT * 10 * 1000
#define MAX_TIMEOUT_RETRY 3

struct gpm_ctx {
    pthread_mutex_t lock;
    int fd;

    /* these are only meaningful if fd != -1 */
    pid_t pid;
    uid_t uid;
    gid_t gid;

    int next_xid;

    int epollfd;
    int timerfd;
};

/* a single global struct is not particularly efficient,
 * but will do for now */
struct gpm_ctx gpm_global_ctx;

pthread_once_t gpm_init_once_control = PTHREAD_ONCE_INIT;

static void gpm_init_once(void)
{
    pthread_mutexattr_t attr;
    char *buf = (char *)&gpm_global_ctx.next_xid;
    size_t len = sizeof(gpm_global_ctx.next_xid);
    size_t ret = 0;

    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);

    pthread_mutex_init(&gpm_global_ctx.lock, &attr);

    gpm_global_ctx.fd = -1;
    gpm_global_ctx.epollfd = -1;
    gpm_global_ctx.timerfd = -1;

    while(ret < len) {
        ret += getrandom(buf + ret, len - ret, 0);
    }

    pthread_mutexattr_destroy(&attr);

    gpm_display_status_init_once();
}

static int get_pipe_name(char *name)
{
    const char *socket;
    int ret;

    socket = gp_getenv("GSSPROXY_SOCKET");
    if (!socket) {
        socket = GP_SOCKET_NAME;
    }

    ret = snprintf(name, PATH_MAX, "%s", socket);
    if (ret < 0 || ret >= PATH_MAX) {
        return ENAMETOOLONG;
    }

    return 0;
}

static int gpm_open_socket(struct gpm_ctx *gpmctx)
{
    struct sockaddr_un addr = {0};
    char name[PATH_MAX];
    int ret;
    int fd = -1;

    ret = get_pipe_name(name);
    if (ret) {
        return ret;
    }

    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, name, sizeof(addr.sun_path)-1);
    addr.sun_path[sizeof(addr.sun_path)-1] = '\0';

    fd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK, 0);
    if (fd == -1) {
        ret = errno;
        goto done;
    }

    ret = connect(fd, (struct sockaddr *)&addr, sizeof(addr));
    if (ret == -1) {
        ret = errno;
    }

done:
    if (ret) {
        if (fd != -1) {
            close(fd);
            fd = -1;
        }
    }
    gpmctx->fd = fd;
    gpmctx->pid = getpid();
    gpmctx->uid = geteuid();
    gpmctx->gid = getegid();
    return ret;
}

static void gpm_close_socket(struct gpm_ctx *gpmctx)
{
    /* In theory, we should retry close() on EINTR, but the fd will be invalid
     * after close() has been called, so closing again may cause a race with
     * another thread that just happend to open an unrelated file descriptor.
     * So until POSIX finally amends language around close() and at least the
     * Linux kernel changes its behavior, it is better to risk a leak than
     * closing an unrelated file descriptor. */
    close(gpmctx->fd);

    gpmctx->fd = -1;
}

static void gpm_timer_close(struct gpm_ctx *gpmctx)
{
    if (gpmctx->timerfd < 0) {
        return;
    }

    close(gpmctx->timerfd);
    gpmctx->timerfd = -1;
}

static int gpm_timer_setup(struct gpm_ctx *gpmctx, int timeout_seconds)
{
    int ret;
    struct itimerspec its;

    if (gpmctx->timerfd >= 0) {
        gpm_timer_close(gpmctx);
    }

    gpmctx->timerfd = timerfd_create(CLOCK_MONOTONIC,
                                     TFD_NONBLOCK | TFD_CLOEXEC);
    if (gpmctx->timerfd < 0) {
        return errno;
    }

    its.it_interval.tv_sec = timeout_seconds;
    its.it_interval.tv_nsec = 0;
    its.it_value.tv_sec = timeout_seconds;
    its.it_value.tv_nsec = 0;

    ret = timerfd_settime(gpmctx->timerfd, 0, &its, NULL);
    if (ret) {
        ret = errno;
        gpm_timer_close(gpmctx);
        return ret;
    }

    return 0;
}

static void gpm_epoll_close(struct gpm_ctx *gpmctx)
{
    if (gpmctx->epollfd < 0) {
        return;
    }

    close(gpmctx->epollfd);
    gpmctx->epollfd = -1;
}

static int gpm_epoll_setup(struct gpm_ctx *gpmctx)
{
    struct epoll_event ev;
    int ret;

    memset(&ev, 0, sizeof(ev));

    if (gpmctx->epollfd >= 0) {
        gpm_epoll_close(gpmctx);
    }

    gpmctx->epollfd = epoll_create1(EPOLL_CLOEXEC);
    if (gpmctx->epollfd == -1) {
        return errno;
    }

    /* Add timer */
    ev.events = EPOLLIN;
    ev.data.fd = gpmctx->timerfd;
    ret = epoll_ctl(gpmctx->epollfd, EPOLL_CTL_ADD, gpmctx->timerfd, &ev);
    if (ret == -1) {
        ret = errno;
        gpm_epoll_close(gpmctx);
        return ret;
    }

    return ret;
}

static int gpm_release_sock(struct gpm_ctx *gpmctx)
{
    gpm_epoll_close(gpmctx);
    gpm_timer_close(gpmctx);
    return pthread_mutex_unlock(&gpmctx->lock);
}

static int gpm_grab_sock(struct gpm_ctx *gpmctx)
{
    int ret;
    pid_t p;
    uid_t u;
    gid_t g;

    ret = pthread_mutex_lock(&gpmctx->lock);
    if (ret) {
        return ret;
    }

    /* Detect fork / setresuid and friends */
    p = getpid();
    u = geteuid();
    g = getegid();

    if (gpmctx->fd != -1 &&
        (p != gpmctx->pid || u != gpmctx->uid || g != gpmctx->gid)) {
        gpm_close_socket(gpmctx);
    }

    if (gpmctx->fd == -1) {
        ret = gpm_open_socket(gpmctx);
        if (ret) {
            goto done;
        }
    }

    /* setup timer */
    ret = gpm_timer_setup(gpmctx, RESPONSE_TIMEOUT);
    if (ret) {
        goto done;
    }
    /* create epoll fd as well */
    ret = gpm_epoll_setup(gpmctx);

done:
    if (ret) {
        gpm_release_sock(gpmctx);
    }
    return ret;
}

static int gpm_epoll_wait(struct gpm_ctx *gpmctx, uint32_t event_flags)
{
    int ret;
    int epoll_ret;
    struct epoll_event ev;
    struct epoll_event events[2];
    uint64_t timer_read;

    memset(&ev, 0, sizeof(ev));
    memset(&events[0], 0, sizeof(events[0]));
    memset(&events[1], 0, sizeof(events[1]));

    if (gpmctx->epollfd < 0) {
        ret = gpm_epoll_setup(gpmctx);
        if (ret)
            return ret;
    }

    ev.events = event_flags;
    ev.data.fd = gpmctx->fd;
    epoll_ret = epoll_ctl(gpmctx->epollfd, EPOLL_CTL_ADD, gpmctx->fd, &ev);
    if (epoll_ret == -1) {
        ret = errno;
        gpm_epoll_close(gpmctx);
        return ret;
    }

    do {
        epoll_ret = epoll_wait(gpmctx->epollfd, events, 2, SAFETY_TIMEOUT);
    } while (epoll_ret < 0 && errno == EINTR);

    if (epoll_ret < 0) {
        /* Error while waiting that isn't EINTR */
        ret = errno;
        gpm_epoll_close(gpmctx);
    } else if (epoll_ret == 0) {
        ret = ETIMEDOUT;
        gpm_epoll_close(gpmctx);
    } else if (epoll_ret == 1 && events[0].data.fd == gpmctx->timerfd) {
        /* Got an event which is only our timer */
        if ((events[0].events & EPOLLIN) == 0) {
            /* We got an event which was not EPOLLIN; assume this is an error,
             * and exit with EBADF: epoll_wait said timerfd had an event,
             * but that event is not an EPOLIN event. */
            ret = EBADF;
        } else {
            ret = read(gpmctx->timerfd, &timer_read, sizeof(uint64_t));
            if (ret == -1 && errno != EAGAIN && errno != EWOULDBLOCK) {
                /* In the case when reading from the timer failed, don't hide the
                 * timer error behind ETIMEDOUT such that it isn't retried */
                ret = errno;
            } else {
                /* If ret == 0, then we definitely timed out. Else, if ret == -1
                 * and errno == EAGAIN or errno == EWOULDBLOCK, we're in a weird
                 * edge case where epoll thinks the timer can be read, but it
                 * is blocking more; treat it like a TIMEOUT and retry, as
                 * nothing around us would handle EAGAIN from timer and retry
                 * it. */
                ret = ETIMEDOUT;
            }
        }
        gpm_epoll_close(gpmctx);
    } else {
        /* If ret == 2, then we ignore the timerfd; that way if the next
         * operation cannot be performed immediately, we timeout and retry.
         * Always check the returned event of the socket fd. */
        int fd_index = 0;
        if (epoll_ret == 2 && events[fd_index].data.fd != gpmctx->fd) {
            fd_index = 1;
        }

        if ((events[fd_index].events & event_flags) == 0) {
            /* We cannot call EPOLLIN/EPOLLOUT at this time; assume that this
             * is a fatal error; return with EBADFD to distinguish from
             * EBADF in timer_fd case. */
            ret = EBADFD;
            gpm_epoll_close(gpmctx);
        } else {
            /* We definintely got a EPOLLIN/EPOLLOUT event; return success. */
            ret = 0;
        }
    }

    epoll_ret = epoll_ctl(gpmctx->epollfd, EPOLL_CTL_DEL, gpmctx->fd, NULL);
    if (epoll_ret == -1) {
        /* If we previously had an error, expose that error instead of
         * clobbering it with errno; else if no error, then assume it is
         * better to notify of the error deleting the event than it is
         * to continue. */
        if (ret == 0)
            ret = errno;
        gpm_epoll_close(gpmctx);
    }

    return ret;
}

static int gpm_retry_socket(struct gpm_ctx *gpmctx)
{
    gpm_epoll_close(gpmctx);
    gpm_close_socket(gpmctx);
    return gpm_open_socket(gpmctx);
}

/* must be called after the lock has been grabbed */
static int gpm_send_buffer(struct gpm_ctx *gpmctx,
                           char *buffer, uint32_t length)
{
    uint32_t size;
    ssize_t wn;
    size_t pos;
    bool retry;
    int ret;

    if (length > MAX_RPC_SIZE) {
        return EINVAL;
    }

    size = length | FRAGMENT_BIT;
    size = htonl(size);

    retry = false;
    do {
        do {
            ret = gpm_epoll_wait(gpmctx, EPOLLOUT);
            if (ret != 0) {
                goto done;
            }

            ret = 0;
            wn = write(gpmctx->fd, &size, sizeof(uint32_t));
            if (wn == -1) {
                ret = errno;
            }
        } while (ret == EINTR);
        if (wn != 4) {
            /* reopen and retry once */
            if (retry == false) {
                ret = gpm_retry_socket(gpmctx);
                if (ret == 0) {
                    retry = true;
                    continue;
                }
            } else {
                ret = EIO;
            }
            goto done;
        }
        retry = false;
    } while (retry);

    pos = 0;
    while (length > pos) {
        ret = gpm_epoll_wait(gpmctx, EPOLLOUT);
        if (ret) {
            goto done;
        }

        wn = write(gpmctx->fd, buffer + pos, length - pos);
        if (wn == -1) {
            if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) {
                continue;
            }
            ret = errno;
            goto done;
        }
        pos += wn;
    }

    ret = 0;

done:
    /* we only need to return as gpm_retry_socket closes the socket */
    return ret;
}

/* must be called after the lock has been grabbed */
static int gpm_recv_buffer(struct gpm_ctx *gpmctx,
                           char **buffer, uint32_t *length)
{
    uint32_t size;
    ssize_t rn;
    size_t pos;
    int ret;

    do {
        ret = gpm_epoll_wait(gpmctx, EPOLLIN);
        if (ret) {
            goto done;
        }

        ret = 0;
        rn = read(gpmctx->fd, &size, sizeof(uint32_t));
        if (rn == -1) {
            ret = errno;
        }
    } while (ret == EINTR);
    if (rn != 4) {
        ret = EIO;
        goto done;
    }

    *length = ntohl(size);
    *length &= ~FRAGMENT_BIT;

    if (*length > MAX_RPC_SIZE) {
        ret = EMSGSIZE;
        goto done;
    }

    *buffer = malloc(*length);
    if (*buffer == NULL) {
        ret = ENOMEM;
        goto done;
    }

    pos = 0;
    while (*length > pos) {
        ret = gpm_epoll_wait(gpmctx, EPOLLIN);
        if (ret) {
            goto done;
        }

        rn = read(gpmctx->fd, *buffer + pos, *length - pos);
        if (rn == -1) {
            if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) {
                continue;
            }
            ret = errno;
            goto done;
        }
        if (rn == 0) {
            ret = EIO;
            goto done;
        }
        pos += rn;
    }

    ret = 0;

done:
    if (ret) {
        /* on errors, free the buffer to prevent calling
         * xdr_destroy(&xdr_reply_ctx); */
        free(*buffer);
        *buffer = NULL;
    }
    return ret;
}

/* must be called after the lock has been grabbed */
static uint32_t gpm_next_xid(struct gpm_ctx *gpmctx)
{
    uint32_t xid;

    if (gpmctx->next_xid < 0) {
        gpmctx->next_xid = 1;
        xid = 0;
    } else {
        xid = gpmctx->next_xid++;
    }

    return xid;
}

static struct gpm_ctx *gpm_get_ctx(void)
{
    pthread_once(&gpm_init_once_control, gpm_init_once);

    return &gpm_global_ctx;
}

static int gpm_send_recv_loop(struct gpm_ctx *gpmctx, char *send_buffer,
                              uint32_t send_length, char** recv_buffer,
                              uint32_t *recv_length)
{
    int ret;
    int retry_count;

    for (retry_count = 0; retry_count < MAX_TIMEOUT_RETRY; retry_count++) {
        /* send to proxy */
        ret = gpm_send_buffer(gpmctx, send_buffer, send_length);

        if (ret == 0) {
            /* No error, continue to recv */
        } else if (ret == ETIMEDOUT) {
            /* Close and reopen socket before trying again */
            ret = gpm_retry_socket(gpmctx);
            if (ret != 0)
                return ret;
            ret = ETIMEDOUT;

            /* RETRY entire send */
            continue;
        } else {
            /* Other error */
            return ret;
        }

        /* receive answer */
        ret = gpm_recv_buffer(gpmctx, recv_buffer, recv_length);
        if (ret == 0) {
            /* No error */
            break;
        } else if (ret == ETIMEDOUT) {
            /* Close and reopen socket before trying again */
            ret = gpm_retry_socket(gpmctx);

            if (ret != 0)
                return ret;
            ret = ETIMEDOUT;
        } else {
            /* Other error */
            return ret;
        }
    }

    return ret;
}

OM_uint32 gpm_release_buffer(OM_uint32 *minor_status,
                             gss_buffer_t buffer)
{
    *minor_status = 0;
    if (buffer != GSS_C_NO_BUFFER) {
        if (buffer->value) {
            free(buffer->value);
        }
        buffer->length = 0;
        buffer->value = NULL;
    }
    return GSS_S_COMPLETE;
}

struct gpm_rpc_fn_set {
    xdrproc_t arg_fn;
    xdrproc_t res_fn;
} gpm_xdr_set[] = {
    { /* NULLPROC */
        (xdrproc_t)xdr_void,
        (xdrproc_t)xdr_void,
    },
    { /* GSSX_INDICATE_MECHS */
        (xdrproc_t)xdr_gssx_arg_indicate_mechs,
        (xdrproc_t)xdr_gssx_res_indicate_mechs,
    },
    { /* GSSX_GET_CALL_CONTEXT */
        (xdrproc_t)xdr_gssx_arg_get_call_context,
        (xdrproc_t)xdr_gssx_res_get_call_context,
    },
    { /* GSSX_IMPORT_AND_CANON_NAME */
        (xdrproc_t)xdr_gssx_arg_import_and_canon_name,
        (xdrproc_t)xdr_gssx_res_import_and_canon_name,
    },
    { /* GSSX_EXPORT_CRED */
        (xdrproc_t)xdr_gssx_arg_export_cred,
        (xdrproc_t)xdr_gssx_res_export_cred,
    },
    { /* GSSX_IMPORT_CRED */
        (xdrproc_t)xdr_gssx_arg_import_cred,
        (xdrproc_t)xdr_gssx_res_import_cred,
    },
    { /* GSSX_ACQUIRE_CRED */
        (xdrproc_t)xdr_gssx_arg_acquire_cred,
        (xdrproc_t)xdr_gssx_res_acquire_cred,
    },
    { /* GSSX_STORE_CRED */
        (xdrproc_t)xdr_gssx_arg_store_cred,
        (xdrproc_t)xdr_gssx_res_store_cred,
    },
    { /* GSSX_INIT_SEC_CONTEXT */
        (xdrproc_t)xdr_gssx_arg_init_sec_context,
        (xdrproc_t)xdr_gssx_res_init_sec_context,
    },
    { /* GSSX_ACCEPT_SEC_CONTEXT */
        (xdrproc_t)xdr_gssx_arg_accept_sec_context,
        (xdrproc_t)xdr_gssx_res_accept_sec_context,
    },
    { /* GSSX_RELEASE_HANDLE */
        (xdrproc_t)xdr_gssx_arg_release_handle,
        (xdrproc_t)xdr_gssx_res_release_handle,
    },
    { /* GSSX_GET_MIC */
        (xdrproc_t)xdr_gssx_arg_get_mic,
        (xdrproc_t)xdr_gssx_res_get_mic,
    },
    { /* GSSX_VERIFY */
        (xdrproc_t)xdr_gssx_arg_verify_mic,
        (xdrproc_t)xdr_gssx_res_verify_mic,
    },
    { /* GSSX_WRAP */
        (xdrproc_t)xdr_gssx_arg_wrap,
        (xdrproc_t)xdr_gssx_res_wrap,
    },
    { /* GSSX_UNWRAP */
        (xdrproc_t)xdr_gssx_arg_unwrap,
        (xdrproc_t)xdr_gssx_res_unwrap,
    },
    { /* GSSX_WRAP_SIZE_LIMIT */
        (xdrproc_t)xdr_gssx_arg_wrap_size_limit,
        (xdrproc_t)xdr_gssx_res_wrap_size_limit,
    }
};

int gpm_make_call(int proc, union gp_rpc_arg *arg, union gp_rpc_res *res)
{
    struct gpm_ctx *gpmctx;
    gp_rpc_msg msg;
    XDR xdr_call_ctx = {0};
    XDR xdr_reply_ctx = {0};
    char *send_buffer = NULL;
    char *recv_buffer = NULL;
    uint32_t send_length;
    uint32_t recv_length;
    uint32_t xid;
    bool xdrok;
    bool sockgrab = false;
    int ret;

    send_buffer = malloc(MAX_RPC_SIZE);
    if (send_buffer == NULL)
        return ENOMEM;

    xdrmem_create(&xdr_call_ctx, send_buffer, MAX_RPC_SIZE, XDR_ENCODE);

    memset(&msg, 0, sizeof(gp_rpc_msg));
    msg.header.type = GP_RPC_CALL;
    msg.header.gp_rpc_msg_union_u.chdr.rpcvers = 2;
    msg.header.gp_rpc_msg_union_u.chdr.prog = GSSPROXY;
    msg.header.gp_rpc_msg_union_u.chdr.vers = GSSPROXYVERS;
    msg.header.gp_rpc_msg_union_u.chdr.proc = proc;
    msg.header.gp_rpc_msg_union_u.chdr.cred.flavor = GP_RPC_AUTH_NONE;
    msg.header.gp_rpc_msg_union_u.chdr.cred.body.body_len = 0;
    msg.header.gp_rpc_msg_union_u.chdr.cred.body.body_val = NULL;
    msg.header.gp_rpc_msg_union_u.chdr.verf.flavor = GP_RPC_AUTH_NONE;
    msg.header.gp_rpc_msg_union_u.chdr.verf.body.body_len = 0;
    msg.header.gp_rpc_msg_union_u.chdr.verf.body.body_val = NULL;

    gpmctx = gpm_get_ctx();
    if (!gpmctx) {
        return EINVAL;
    }

    /* grab the lock for the whole conversation */
    ret = gpm_grab_sock(gpmctx);
    if (ret) {
        goto done;
    }
    sockgrab = true;

    msg.xid = xid = gpm_next_xid(gpmctx);

    /* encode header */
    xdrok = xdr_gp_rpc_msg(&xdr_call_ctx, &msg);
    if (!xdrok) {
        ret = EINVAL;
        goto done;
    }

    /* encode data */
    xdrok = gpm_xdr_set[proc].arg_fn(&xdr_call_ctx, (char *)arg);
    if (!xdrok) {
        ret = EINVAL;
        goto done;
    }

    /* set send_length */
    send_length = xdr_getpos(&xdr_call_ctx);

    /* Send request, receive response with timeout */
    ret = gpm_send_recv_loop(gpmctx, send_buffer, send_length, &recv_buffer,
                             &recv_length);
    if (ret)
        goto done;

    /* release the lock */
    gpm_release_sock(gpmctx);
    sockgrab = false;

    /* Create the reply context */
    xdrmem_create(&xdr_reply_ctx, recv_buffer, recv_length, XDR_DECODE);

    /* decode header */
    memset(&msg, 0, sizeof(gp_rpc_msg));
    xdrok = xdr_gp_rpc_msg(&xdr_reply_ctx, &msg);
    if (!xdrok) {
        ret = EINVAL;
        goto done;
    }

    if (msg.xid != xid ||
        msg.header.type != GP_RPC_REPLY ||
        msg.header.gp_rpc_msg_union_u.rhdr.status != GP_RPC_MSG_ACCEPTED ||
        msg.header.gp_rpc_msg_union_u.rhdr.gp_rpc_reply_header_u.accepted.reply_data.status != GP_RPC_SUCCESS) {
        ret = EINVAL;
        goto done;
    }

    /* decode answer */
    xdrok = gpm_xdr_set[proc].res_fn(&xdr_reply_ctx, (char *)res);
    if (!xdrok) {
        ret = EINVAL;
    }

done:
    if (sockgrab) {
        gpm_release_sock(gpmctx);
    }
    xdr_free((xdrproc_t)xdr_gp_rpc_msg, (char *)&msg);
    xdr_destroy(&xdr_call_ctx);

    if (recv_buffer != NULL)
        xdr_destroy(&xdr_reply_ctx);

    free(send_buffer);
    free(recv_buffer);

    return ret;
}

void gpm_free_xdrs(int proc, union gp_rpc_arg *arg, union gp_rpc_res *res)
{
    xdr_free(gpm_xdr_set[proc].arg_fn, (char *)arg);
    xdr_free(gpm_xdr_set[proc].res_fn, (char *)res);
}
