/*
    SSSD

    LDAP Backend Module -- child helpers

    Authors:
        Jakub Hrozek <jhrozek@redhat.com>

    Copyright (C) 2009 Red Hat

    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/>.
*/

#include <sys/types.h>
#include <sys/wait.h>
#include <pwd.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>

#include "util/util.h"
#include "util/sss_krb5.h"
#include "providers/ldap/ldap_common.h"
#include "providers/ldap/sdap_async_private.h"
#include "util/child_common.h"

#ifndef SSSD_LIBEXEC_PATH
#error "SSSD_LIBEXEC_PATH not defined"
#else
#define LDAP_CHILD SSSD_LIBEXEC_PATH"/ldap_child"
#endif

#ifndef LDAP_CHILD_USER
#define LDAP_CHILD_USER  "nobody"
#endif

struct sdap_child {
    /* child info */
    pid_t pid;
    struct child_io_fds *io;
};

static void sdap_close_fd(int *fd)
{
    int ret;

    if (*fd == -1) {
        DEBUG(SSSDBG_TRACE_FUNC, "fd already closed\n");
        return;
    }

    ret = close(*fd);
    if (ret) {
        ret = errno;
        DEBUG(SSSDBG_OP_FAILURE, "Closing fd %d, return error %d (%s)\n",
                  *fd, ret, strerror(ret));
    }

    *fd = -1;
}

static void child_callback(int child_status,
                           struct tevent_signal *sige,
                           void *pvt)
{
    if (WEXITSTATUS(child_status) == CHILD_TIMEOUT_EXIT_CODE) {
        DEBUG(SSSDBG_CRIT_FAILURE,
              "LDAP child was terminated due to timeout\n");

        struct tevent_req *req = talloc_get_type(pvt, struct tevent_req);
        tevent_req_error(req, ETIMEDOUT);
    }
}

static errno_t sdap_fork_child(struct tevent_context *ev,
                               struct sdap_child *child, struct tevent_req *req)
{
    int pipefd_to_child[2] = PIPE_INIT;
    int pipefd_from_child[2] = PIPE_INIT;
    pid_t pid;
    errno_t ret;

    ret = pipe(pipefd_from_child);
    if (ret == -1) {
        ret = errno;
        DEBUG(SSSDBG_CRIT_FAILURE,
              "pipe(from) failed [%d][%s].\n", ret, strerror(ret));
        goto fail;
    }
    ret = pipe(pipefd_to_child);
    if (ret == -1) {
        ret = errno;
        DEBUG(SSSDBG_CRIT_FAILURE,
              "pipe(to) failed [%d][%s].\n", ret, strerror(ret));
        goto fail;
    }

    pid = fork();

    if (pid == 0) { /* child */
        exec_child(child,
                   pipefd_to_child, pipefd_from_child,
                   LDAP_CHILD, LDAP_CHILD_LOG_FILE);

        /* We should never get here */
        DEBUG(SSSDBG_CRIT_FAILURE, "BUG: Could not exec LDAP child\n");
    } else if (pid > 0) { /* parent */
        child->pid = pid;
        child->io->read_from_child_fd = pipefd_from_child[0];
        PIPE_FD_CLOSE(pipefd_from_child[1]);
        child->io->write_to_child_fd = pipefd_to_child[1];
        PIPE_FD_CLOSE(pipefd_to_child[0]);
        sss_fd_nonblocking(child->io->read_from_child_fd);
        sss_fd_nonblocking(child->io->write_to_child_fd);

        ret = child_handler_setup(ev, pid, child_callback, req, NULL);
        if (ret != EOK) {
            goto fail;
        }

    } else { /* error */
        ret = errno;
        DEBUG(SSSDBG_CRIT_FAILURE,
              "fork failed [%d][%s].\n", ret, strerror(ret));
        goto fail;
    }

    return EOK;

fail:
    PIPE_CLOSE(pipefd_from_child);
    PIPE_CLOSE(pipefd_to_child);
    return ret;
}

static errno_t create_tgt_req_send_buffer(TALLOC_CTX *mem_ctx,
                                          const char *realm_str,
                                          const char *princ_str,
                                          const char *keytab_name,
                                          int32_t lifetime,
                                          struct io_buffer **io_buf)
{
    struct io_buffer *buf;
    size_t rp;

    buf = talloc(mem_ctx, struct io_buffer);
    if (buf == NULL) {
        DEBUG(SSSDBG_CRIT_FAILURE, "talloc failed.\n");
        return ENOMEM;
    }

    buf->size = 6 * sizeof(uint32_t);
    if (realm_str) {
        buf->size += strlen(realm_str);
    }
    if (princ_str) {
        buf->size += strlen(princ_str);
    }
    if (keytab_name) {
        buf->size += strlen(keytab_name);
    }

    DEBUG(SSSDBG_TRACE_FUNC, "buffer size: %zu\n", buf->size);

    buf->data = talloc_size(buf, buf->size);
    if (buf->data == NULL) {
        DEBUG(SSSDBG_CRIT_FAILURE, "talloc_size failed.\n");
        talloc_free(buf);
        return ENOMEM;
    }

    rp = 0;

    /* realm */
    if (realm_str) {
        SAFEALIGN_SET_UINT32(&buf->data[rp], strlen(realm_str), &rp);
        safealign_memcpy(&buf->data[rp], realm_str, strlen(realm_str), &rp);
    } else {
        SAFEALIGN_SET_UINT32(&buf->data[rp], 0, &rp);
    }

    /* principal */
    if (princ_str) {
        SAFEALIGN_SET_UINT32(&buf->data[rp], strlen(princ_str), &rp);
        safealign_memcpy(&buf->data[rp], princ_str, strlen(princ_str), &rp);
    } else {
        SAFEALIGN_SET_UINT32(&buf->data[rp], 0, &rp);
    }

    /* keytab */
    if (keytab_name) {
        SAFEALIGN_SET_UINT32(&buf->data[rp], strlen(keytab_name), &rp);
        safealign_memcpy(&buf->data[rp], keytab_name, strlen(keytab_name), &rp);
    } else {
        SAFEALIGN_SET_UINT32(&buf->data[rp], 0, &rp);
    }

    /* lifetime */
    SAFEALIGN_SET_UINT32(&buf->data[rp], lifetime, &rp);

    /* UID and GID to drop privileges to, if needed. The ldap_child process runs as
     * setuid if the back end runs unprivileged as it needs to access the keytab
     */
    SAFEALIGN_SET_UINT32(&buf->data[rp], geteuid(), &rp);
    SAFEALIGN_SET_UINT32(&buf->data[rp], getegid(), &rp);

    *io_buf = buf;
    return EOK;
}

static int parse_child_response(TALLOC_CTX *mem_ctx,
                                uint8_t *buf, ssize_t size,
                                int  *result, krb5_error_code *kerr,
                                char **ccache, time_t *expire_time_out)
{
    size_t p = 0;
    uint32_t len;
    uint32_t res;
    char *ccn;
    time_t expire_time;
    krb5_error_code krberr;

    /* operation result code */
    SAFEALIGN_COPY_UINT32_CHECK(&res, buf + p, size, &p);

    /* krb5 error code */
    safealign_memcpy(&krberr, buf+p, sizeof(krberr), &p);

    /* ccache name size */
    SAFEALIGN_COPY_UINT32_CHECK(&len, buf + p, size, &p);

    if (len > size - p) return EINVAL;

    ccn = talloc_size(mem_ctx, sizeof(char) * (len + 1));
    if (ccn == NULL) {
        DEBUG(SSSDBG_CRIT_FAILURE, "talloc_size failed.\n");
        return ENOMEM;
    }
    safealign_memcpy(ccn, buf+p, sizeof(char) * len, &p);
    ccn[len] = '\0';

    if (p + sizeof(time_t) > size) {
        talloc_free(ccn);
        return EINVAL;
    }
    safealign_memcpy(&expire_time, buf+p, sizeof(time_t), &p);

    *result = res;
    *ccache = ccn;
    *expire_time_out = expire_time;
    *kerr = krberr;
    return EOK;
}

/* ==The-public-async-interface============================================*/

struct sdap_get_tgt_state {
    struct tevent_context *ev;
    struct sdap_child *child;
    ssize_t len;
    uint8_t *buf;

    struct tevent_timer *kill_te;
};

static errno_t set_tgt_child_timeout(struct tevent_req *req,
                                     struct tevent_context *ev,
                                     int timeout);
static void sdap_get_tgt_step(struct tevent_req *subreq);
static void sdap_get_tgt_done(struct tevent_req *subreq);

struct tevent_req *sdap_get_tgt_send(TALLOC_CTX *mem_ctx,
                                     struct tevent_context *ev,
                                     const char *realm_str,
                                     const char *princ_str,
                                     const char *keytab_name,
                                     int32_t lifetime,
                                     int timeout)
{
    struct tevent_req *req, *subreq;
    struct sdap_get_tgt_state *state;
    struct io_buffer *buf;
    int ret;

    req = tevent_req_create(mem_ctx, &state, struct sdap_get_tgt_state);
    if (!req) {
        return NULL;
    }

    state->ev = ev;

    state->child = talloc_zero(state, struct sdap_child);
    if (!state->child) {
        ret = ENOMEM;
        goto fail;
    }

    state->child->io = talloc(state, struct child_io_fds);
    if (state->child->io == NULL) {
        ret = ENOMEM;
        goto fail;
    }
    state->child->io->read_from_child_fd = -1;
    state->child->io->write_to_child_fd = -1;
    talloc_set_destructor((TALLOC_CTX *) state->child->io, child_io_destructor);

    /* prepare the data to pass to child */
    ret = create_tgt_req_send_buffer(state,
                                     realm_str, princ_str, keytab_name, lifetime,
                                     &buf);
    if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE, "create_tgt_req_send_buffer failed.\n");
        goto fail;
    }

    ret = sdap_fork_child(state->ev, state->child, req);
    if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE, "sdap_fork_child failed.\n");
        goto fail;
    }

    ret = set_tgt_child_timeout(req, ev, timeout);
    if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE, "set_tgt_child_timeout failed.\n");
        goto fail;
    }

    subreq = write_pipe_send(state, ev, buf->data, buf->size,
                             state->child->io->write_to_child_fd);
    if (!subreq) {
        ret = ENOMEM;
        goto fail;
    }
    tevent_req_set_callback(subreq, sdap_get_tgt_step, req);

    return req;

fail:
    tevent_req_error(req, ret);
    tevent_req_post(req, ev);
    return req;
}

static void sdap_get_tgt_step(struct tevent_req *subreq)
{
    struct tevent_req *req = tevent_req_callback_data(subreq,
                                                      struct tevent_req);
    struct sdap_get_tgt_state *state = tevent_req_data(req,
                                                  struct sdap_get_tgt_state);
    int ret;

    ret = write_pipe_recv(subreq);
    talloc_zfree(subreq);
    if (ret != EOK) {
        tevent_req_error(req, ret);
        return;
    }

    sdap_close_fd(&state->child->io->write_to_child_fd);

    subreq = read_pipe_send(state, state->ev,
                            state->child->io->read_from_child_fd);
    if (!subreq) {
        tevent_req_error(req, ENOMEM);
        return;
    }
    tevent_req_set_callback(subreq, sdap_get_tgt_done, req);
}

static void sdap_get_tgt_done(struct tevent_req *subreq)
{
    struct tevent_req *req = tevent_req_callback_data(subreq,
                                                      struct tevent_req);
    struct sdap_get_tgt_state *state = tevent_req_data(req,
                                                  struct sdap_get_tgt_state);
    int ret;

    ret = read_pipe_recv(subreq, state, &state->buf, &state->len);
    talloc_zfree(subreq);
    if (ret != EOK) {
        tevent_req_error(req, ret);
        return;
    }

    sdap_close_fd(&state->child->io->read_from_child_fd);

    if (state->kill_te == NULL) {
        tevent_req_done(req);
        return;
    }

    /* wait for child callback to terminate the request */
}

int sdap_get_tgt_recv(struct tevent_req *req,
                      TALLOC_CTX *mem_ctx,
                      int  *result,
                      krb5_error_code *kerr,
                      char **ccname,
                      time_t *expire_time_out)
{
    struct sdap_get_tgt_state *state = tevent_req_data(req,
                                             struct sdap_get_tgt_state);
    char *ccn;
    time_t expire_time;
    int  res;
    int ret;
    krb5_error_code krberr;

    TEVENT_REQ_RETURN_ON_ERROR(req);

    ret = parse_child_response(mem_ctx, state->buf, state->len,
                               &res, &krberr, &ccn, &expire_time);
    if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE,
              "Cannot parse child response: [%d][%s]\n", ret, strerror(ret));
        return ret;
    }

    DEBUG(SSSDBG_TRACE_FUNC,
          "Child responded: %d [%s], expired on [%ld]\n", res, ccn, (long)expire_time);
    *result = res;
    *kerr = krberr;
    *ccname = ccn;
    *expire_time_out = expire_time;
    return EOK;
}

static void get_tgt_sigkill_handler(struct tevent_context *ev,
                                    struct tevent_timer *te,
                                    struct timeval tv, void *pvt)
{
    struct tevent_req *req = talloc_get_type(pvt, struct tevent_req);
    struct sdap_get_tgt_state *state = tevent_req_data(req,
                                            struct sdap_get_tgt_state);
    int ret;

    DEBUG(SSSDBG_TRACE_ALL,
          "timeout for sending SIGKILL to TGT child [%d] reached.\n",
          state->child->pid);

    ret = kill(state->child->pid, SIGKILL);
    if (ret == -1) {
        DEBUG(SSSDBG_CRIT_FAILURE,
              "kill failed [%d][%s].\n", errno, strerror(errno));
    }

    tevent_req_error(req, ETIMEDOUT);
}

static void get_tgt_timeout_handler(struct tevent_context *ev,
                                      struct tevent_timer *te,
                                      struct timeval tv, void *pvt)
{
    struct tevent_req *req = talloc_get_type(pvt, struct tevent_req);
    struct sdap_get_tgt_state *state = tevent_req_data(req,
                                            struct sdap_get_tgt_state);
    int ret;

    DEBUG(SSSDBG_TRACE_ALL,
          "timeout for sending SIGTERM to TGT child [%d] reached.\n",
          state->child->pid);

    ret = kill(state->child->pid, SIGTERM);
    if (ret == -1) {
        ret = errno;
        DEBUG(SSSDBG_CRIT_FAILURE,
              "Sending SIGTERM failed [%d][%s].\n", ret, strerror(ret));
    }

    DEBUG(SSSDBG_TRACE_FUNC,
          "Setting %d seconds timeout for sending SIGKILL to TGT child\n",
          SIGTERM_TO_SIGKILL_TIME);

    tv = tevent_timeval_current_ofs(SIGTERM_TO_SIGKILL_TIME, 0);

    state->kill_te = tevent_add_timer(ev, req, tv, get_tgt_sigkill_handler, req);
    if (state->kill_te == NULL) {
        DEBUG(SSSDBG_CRIT_FAILURE, "tevent_add_timer failed.\n");
        tevent_req_error(req, ECANCELED);
    }
}

static errno_t set_tgt_child_timeout(struct tevent_req *req,
                                     struct tevent_context *ev,
                                     int timeout)
{
    struct tevent_timer *te;
    struct timeval tv;

    DEBUG(SSSDBG_TRACE_FUNC,
          "Setting %d seconds timeout for TGT child\n", timeout);

    tv = tevent_timeval_current_ofs(timeout, 0);

    te = tevent_add_timer(ev, req, tv, get_tgt_timeout_handler, req);
    if (te == NULL) {
        DEBUG(SSSDBG_CRIT_FAILURE, "tevent_add_timer failed.\n");
        return ENOMEM;
    }

    return EOK;
}
