/*
   Copyright (c) 2011-2012 Red Hat, Inc. <http://www.redhat.com>
   This file is part of GlusterFS.

   This file is licensed to you under your choice of the GNU Lesser
   General Public License, version 3 or any later version (LGPLv3 or
   later), or the GNU General Public License, version 2 (GPLv2), in all
   cases as published by the Free Software Foundation.
*/
#include <glusterfs/compat.h>
#include <glusterfs/syscall.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/param.h> /* for PATH_MAX */
#include <unistd.h>

/* NOTE (USE_LIBGLUSTERFS):
 * ------------------------
 * When USE_LIBGLUSTERFS debugging sumbol is passed; perform
 * glusterfs translator like initialization so that glusterfs
 * globals, contexts are valid when glustefs api's are invoked.
 * We unconditionally pass then while building gsyncd binary.
 */
#ifdef USE_LIBGLUSTERFS
#include <glusterfs/defaults.h>
#include <glusterfs/globals.h>
#include <glusterfs/glusterfs.h>
#endif

#include "procdiggy.h"
#include <glusterfs/common-utils.h>
#include <glusterfs/run.h>

#define _GLUSTERD_CALLED_ "_GLUSTERD_CALLED_"
#define _GSYNCD_DISPATCHED_ "_GSYNCD_DISPATCHED_"
#define GSYNCD_CONF_TEMPLATE "geo-replication/gsyncd_template.conf"
#define GSYNCD_PY "gsyncd.py"
#define RSYNC "rsync"

int restricted = 0;

static int
duplexpand(void **buf, size_t tsiz, size_t *len)
{
    size_t osiz = tsiz * *len;
    char *p = realloc(*buf, osiz << 1);
    if (!p) {
        return -1;
    }

    memset(p + osiz, 0, osiz);
    *buf = p;
    *len <<= 1;

    return 0;
}

static int
str2argv(char *str, char ***argv)
{
    char *p = NULL;
    char *savetok = NULL;
    char *temp = NULL;
    char *temp1 = NULL;
    int argc = 0;
    size_t argv_len = 32;
    int ret = 0;
    int i = 0;

    assert(str);
    temp = str = strdup(str);
    if (!str)
        goto error;

    *argv = calloc(argv_len, sizeof(**argv));
    if (!*argv)
        goto error;

    while ((p = strtok_r(str, " ", &savetok))) {
        str = NULL;

        argc++;
        if (argc == argv_len) {
            ret = duplexpand((void *)argv, sizeof(**argv), &argv_len);
            if (ret == -1)
                goto error;
        }
        temp1 = strdup(p);
        if (!temp1)
            goto error;
        (*argv)[argc - 1] = temp1;
    }

    free(temp);
    return argc;

error:
    fprintf(stderr, "out of memory\n");
    free(temp);
    for (i = 0; i < argc - 1; i++)
        free((*argv)[i]);
    free(*argv);
    return -1;
}

static int
invoke_gsyncd(int argc, char **argv)
{
    int i = 0;
    int j = 0;
    char *nargv[argc + 4];
    char *python = NULL;

    if (chdir("/") == -1)
        goto error;

    j = 0;
    python = getenv("PYTHON");
    if (!python)
        python = PYTHON;
    nargv[j++] = python;
    nargv[j++] = GSYNCD_PREFIX "/python/syncdaemon/" GSYNCD_PY;
    for (i = 1; i < argc; i++)
        nargv[j++] = argv[i];

    nargv[j++] = NULL;

    execvp(python, nargv);

    fprintf(stderr, "exec of '%s' failed\n", python);
    return 127;

error:
    fprintf(stderr, "gsyncd initializaion failed\n");
    return 1;
}

static int
find_gsyncd(pid_t pid, pid_t ppid, char *name, void *data)
{
    char buf[NAME_MAX * 2] = {
        0,
    };
    char path[PATH_MAX] = {
        0,
    };
    char *p = NULL;
    int zeros = 0;
    int ret = 0;
    int fd = -1;
    pid_t *pida = (pid_t *)data;

    if (ppid != pida[0])
        return 0;

    snprintf(path, sizeof path, PROC "/%d/cmdline", pid);
    fd = open(path, O_RDONLY);
    if (fd == -1)
        return 0;
    ret = sys_read(fd, buf, sizeof(buf));
    sys_close(fd);
    if (ret == -1)
        return 0;
    for (zeros = 0, p = buf; zeros < 2 && p < buf + ret; p++)
        zeros += !*p;

    ret = 0;
    switch (zeros) {
        case 2:
            if ((strcmp(basename(buf), basename(PYTHON)) ||
                 strcmp(basename(buf + strlen(buf) + 1), GSYNCD_PY)) == 0) {
                ret = 1;
                break;
            }
            /* fallthrough */
        case 1:
            if (strcmp(basename(buf), GSYNCD_PY) == 0)
                ret = 1;
    }

    if (ret == 1) {
        if (pida[1] != -1) {
            fprintf(stderr, GSYNCD_PY " sibling is not unique");
            return -1;
        }
        pida[1] = pid;
    }

    return 0;
}

static int
invoke_rsync(int argc, char **argv)
{
    int i = 0;
    char path[PATH_MAX] = {
        0,
    };
    pid_t pid = -1;
    pid_t ppid = -1;
    pid_t pida[] = {-1, -1};
    char *name = NULL;
    char buf[PATH_MAX + 1] = {
        0,
    };
    int ret = 0;

    assert(argv[argc] == NULL);

    if (argc < 2 || strcmp(argv[1], "--server") != 0)
        goto error;

    for (i = 2; i < argc && argv[i][0] == '-'; i++)
        ;

    if (!(i == argc - 2 && strcmp(argv[i], ".") == 0 &&
          argv[i + 1][0] == '/')) {
        fprintf(stderr, "need an rsync invocation without protected args\n");
        goto error;
    }

    /* look up sshd we are spawned from */
    for (pid = getpid();; pid = ppid) {
        ppid = pidinfo(pid, &name);
        if (ppid < 0) {
            fprintf(stderr, "sshd ancestor not found\n");
            goto error;
        }
        if (strcmp(name, "sshd") == 0) {
            GF_FREE(name);
            break;
        }
        GF_FREE(name);
    }
    /* look up "ssh-sibling" gsyncd */
    pida[0] = pid;
    ret = prociter(find_gsyncd, pida);
    if (ret == -1 || pida[1] == -1) {
        fprintf(stderr, "gsyncd sibling not found\n");
        goto error;
    }
    /* check if rsync target matches gsyncd target */
    snprintf(path, sizeof path, PROC "/%d/cwd", pida[1]);
    ret = sys_readlink(path, buf, sizeof(buf));
    if (ret == -1 || ret == sizeof(buf))
        goto error;

    buf[ret] = '\0';

    if (strcmp(argv[argc - 1], "/") == 0 /* root dir cannot be a target */ ||
        (strcmp(argv[argc - 1], path) /* match against gluster target */ &&
         strcmp(argv[argc - 1], buf) /* match against file target */) != 0) {
        fprintf(stderr, "rsync target does not match " GEOREP " session\n");
        goto error;
    }

    argv[0] = RSYNC;

    execvp(RSYNC, argv);

    fprintf(stderr, "exec of " RSYNC " failed\n");
    return 127;

error:
    fprintf(stderr, "disallowed " RSYNC " invocation\n");
    return 1;
}

static int
invoke_gluster(int argc, char **argv)
{
    int i = 0;
    int j = 0;
    int optsover = 0;
    char *ov = NULL;

    for (i = 1; i < argc; i++) {
        ov = strtail(argv[i], "--");
        if (ov && !optsover) {
            if (*ov == '\0')
                optsover = 1;
            continue;
        }
        switch (++j) {
            case 1:
                if (strcmp(argv[i], "volume") != 0)
                    goto error;
                break;
            case 2:
                if (strcmp(argv[i], "info") != 0)
                    goto error;
                break;
            case 3:
                break;
            default:
                goto error;
        }
    }

    argv[0] = "gluster";
    execvp(SBIN_DIR "/gluster", argv);
    fprintf(stderr, "exec of gluster failed\n");
    return 127;

error:
    fprintf(stderr, "disallowed gluster invocation\n");
    return 1;
}

struct invocable {
    char *name;
    int (*invoker)(int argc, char **argv);
};

struct invocable invocables[] = {{"rsync", invoke_rsync},
                                 {"gsyncd", invoke_gsyncd},
                                 {"gluster", invoke_gluster},
                                 {NULL, NULL}};

int
main(int argc, char **argv)
{
    int ret = -1;
    char *evas = NULL;
    struct invocable *i = NULL;
    char *b = NULL;
    char *sargv = NULL;
    int j = 0;

#ifdef USE_LIBGLUSTERFS
    glusterfs_ctx_t *ctx = NULL;

    ctx = glusterfs_ctx_new();
    if (!ctx)
        return ENOMEM;

    if (glusterfs_globals_init(ctx))
        return 1;

    THIS->ctx = ctx;
    ret = default_mem_acct_init(THIS);
    if (ret) {
        fprintf(stderr, "internal error: mem accounting failed\n");
        return 1;
    }
#endif

    evas = getenv(_GLUSTERD_CALLED_);
    if (evas && strcmp(evas, "1") == 0)
        /* OK, we know glusterd called us, no need to look for further config
         *...although this conclusion should not inherit to our children
         */
        unsetenv(_GLUSTERD_CALLED_);
    else {
        /* we regard all gsyncd invocations unsafe
         * that do not come from glusterd and
         * therefore restrict it
         */
        restricted = 1;

        if (!getenv(_GSYNCD_DISPATCHED_)) {
            evas = getenv("SSH_ORIGINAL_COMMAND");
            if (evas)
                sargv = evas;
            else {
                evas = getenv("SHELL");
                if (evas && strcmp(basename(evas), "gsyncd") == 0 &&
                    argc == 3 && strcmp(argv[1], "-c") == 0)
                    sargv = argv[2];
            }
        }
    }

    if (!(sargv && restricted))
        return invoke_gsyncd(argc, argv);

    argc = str2argv(sargv, &argv);

    if (argc == -1) {
        fprintf(stderr, "internal error\n");
        return 1;
    }

    if (setenv(_GSYNCD_DISPATCHED_, "1", 1) == -1) {
        fprintf(stderr, "internal error\n");
        goto out;
    }

    if ( argc >= 1 )
        b = basename(argv[0]);
    else
        goto out;

    for (i = invocables; i->name; i++) {
        if (strcmp(b, i->name) == 0)
            return i->invoker(argc, argv);
    }

    fprintf(stderr, "invoking %s in restricted SSH session is not allowed\n",
            b);

out:
    for (j = 1; j < argc; j++)
        free(argv[j]);
    free(argv);
    return 1;
}
