/*
   Unix SMB/CIFS implementation.
   run a command as a specified user
   Copyright (C) Andrew Tridgell 1992-1998

   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 2 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, write to the Free Software
   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

   modified for netatalk dgautheron@magic.fr
*/

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif /* HAVE_CONFIG_H */

#include <errno.h>
#include <grp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/param.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

/* FIXME */
#if defined(__linux__)
#ifndef USE_SETRESUID
#define USE_SETRESUID 1
#endif
#else
#ifndef USE_SETEUID
#define USE_SETEUID 1
#endif
#endif

#include <atalk/logger.h>
#include <atalk/util.h>

/*!
 * This is a utility function of afprun().
 */
static int setup_out_fd(void)
{
    int fd;
    char path[MAXPATHLEN + 1];
    snprintf(path, sizeof(path) - 1, "%s/afp.XXXXXX", tmpdir());
    /* now create the file */
    fd = mkstemp(path);

    if (fd == -1) {
        LOG(log_error, logtype_afpd, "setup_out_fd: Failed to create file %s. (%s)",
            path, strerror(errno));
        return -1;
    }

    /* Ensure file only kept around by open fd. */
    unlink(path);
    return fd;
}

/*!
 * @brief Gain root privilege before doing something.
 * @note We want to end up with ruid==euid==0
 */
static void gain_root_privilege(void)
{
    if (seteuid(0) < 0) {
        LOG(log_error, logtype_afpd, "gain_root_privilege: could not seteuid(%i)", 0);
    }
}

/*!
 * @brief Ensure our real and effective groups are zero.
 * @note we want to end up with rgid==egid==0
 */
static void gain_root_group_privilege(void)
{
    if (setegid(0) < 0) {
        LOG(log_error, logtype_afpd, "gain_root_group_privilege: could not setegid(%i)",
            0);
    }
}

/*!
 * @brief Become the specified uid and gid - permanently !
 * @note there should be no way back if possible
 */
static int become_user_permanently(uid_t uid, gid_t gid)
{
    int ret;
    /*
     * First - gain root privilege. We do this to ensure
     * we can lose it again.
     */
    gain_root_privilege();
    gain_root_group_privilege();
    ret = setgroups(0, NULL);

    if (ret != 0) {
        return -1;
    }

#if USE_SETRESUID
    ret = setresgid(gid, gid, gid);

    if (ret != 0) {
        LOG(log_error, logtype_afpd, "could not setresgid(%i, %i, %i)",
            gid, gid, gid);
        return -1;
    }

    ret = setgid(gid);

    if (ret != 0) {
        LOG(log_error, logtype_afpd, "could not setgid(%i)", gid);
        return -1;
    }

    ret = setresuid(uid, uid, uid);

    if (ret != 0) {
        LOG(log_error, logtype_afpd, "could not setresuid(%i, %i, %i)",
            uid, uid, uid);
        return -1;
    }

    ret = setuid(uid);

    if (ret != 0) {
        LOG(log_error, logtype_afpd, "could not setuid(%i)", uid);
        return -1;
    }

#endif
#if USE_SETREUID
    ret = setregid(gid, gid);

    if (ret != 0) {
        LOG(log_error, logtype_afpd, "could not setresgid(%i, %i)", gid, gid);
        return -1;
    }

    ret = setgid(gid);

    if (ret != 0) {
        LOG(log_error, logtype_afpd, "could not setgid(%i)", gid);
        return -1;
    }

    ret = setreuid(uid, uid);

    if (ret != 0) {
        LOG(log_error, logtype_afpd, "could not setreuid(%i, %i)", uid, uid);
        return -1;
    }

    ret = setuid(uid);

    if (ret != 0) {
        LOG(log_error, logtype_afpd, "could not setuid(%i)", uid);
        return -1;
    }

#endif
#if USE_SETEUID
    ret = setegid(gid);

    if (ret != 0) {
        LOG(log_error, logtype_afpd, "could not setegid(%i)", gid);
        return -1;
    }

    ret = setgid(gid);

    if (ret != 0) {
        LOG(log_error, logtype_afpd, "could not setgid(%i)", gid);
        return -1;
    }

    ret = setuid(uid);

    if (ret != 0) {
        LOG(log_error, logtype_afpd, "could not setuid(%i)", uid);
        return -1;
    }

    ret = seteuid(uid);

    if (ret != 0) {
        LOG(log_error, logtype_afpd, "could not seteuid(%i)", uid);
        return -1;
    }

    ret = setuid(uid);

    if (ret != 0) {
        LOG(log_error, logtype_afpd, "could not setuid(%i)", uid);
        return -1;
    }

#endif
#if USE_SETUIDX
    ret = setgidx(ID_REAL, gid);

    if (ret != 0) {
        LOG(log_error, logtype_afpd, "could not setgidx(%i, %i)", ID_REAL, gid);
        return -1;
    }

    ret = setgidx(ID_EFFECTIVE, gid);

    if (ret != 0) {
        LOG(log_error, logtype_afpd, "could not setgidx(%i, %i)",
            ID_EFFECTIVE, gid);
        return -1;
    }

    ret = setgid(gid);

    if (ret != 0) {
        LOG(log_error, logtype_afpd, "could not setgid(%i)", gid);
        return -1;
    }

    ret = setuidx(ID_REAL, uid);

    if (ret != 0) {
        LOG(log_error, logtype_afpd, "could not setuidx(%i, %i)", ID_REAL, uid);
        return -1;
    }

    ret = setuidx(ID_EFFECTIVE, uid);

    if (ret != 0) {
        LOG(log_error, logtype_afpd, "could not setuidx(%i, %i)",
            ID_EFFECTIVE, uid);
        return -1;
    }

    ret = setuid(uid);

    if (ret != 0) {
        LOG(log_error, logtype_afpd, "could not setuid(%i)", uid);
        return -1;
    }

#endif
    return 0;
}

/*!
 * @brief run a command
 * @note being careful about uid/gid handling and putting the output in
 * outfd (or discard it if outfd is NULL).
 */

int afprun(char *cmd, int *outfd)
{
    pid_t pid;
    uid_t uid = geteuid();
    gid_t gid = getegid();

    /* point our stdout at the file we want output to go into */
    if (outfd && ((*outfd = setup_out_fd()) == -1)) {
        return -1;
    }

    LOG(log_debug, logtype_afpd, "running %s as user %d", cmd, uid);
    /* in this method we will exec /bin/sh with the correct
       arguments, after first setting stdout to point at the file */

    if ((pid = fork()) < 0) {
        LOG(log_error, logtype_afpd, "afprun: fork failed with error %s",
            strerror(errno));

        if (outfd) {
            close(*outfd);
            *outfd = -1;
        }

        return errno;
    }

    if (pid) {
        /*
         * Parent.
         */
        int status = 0;
        pid_t wpid;

        /* the parent just waits for the child to exit */
        while ((wpid = waitpid(pid, &status, 0)) < 0) {
            if (errno == EINTR) {
                errno = 0;
                continue;
            }

            break;
        }

        if (wpid != pid) {
            LOG(log_error, logtype_afpd, "waitpid(%d) : %s", (int)pid, strerror(errno));

            if (outfd) {
                close(*outfd);
                *outfd = -1;
            }

            return -1;
        }

        /* Reset the seek pointer. */
        if (outfd) {
            lseek(*outfd, 0, SEEK_SET);
        }

#if defined(WIFEXITED) && defined(WEXITSTATUS)

        if (WIFEXITED(status)) {
            return WEXITSTATUS(status);
        }

#endif
        return status;
    }

    /* we are in the child. we exec /bin/sh to do the work for us. we
       don't directly exec the command we want because it may be a
       pipeline or anything else the config file specifies */

    /* point our stdout at the file we want output to go into */
    if (outfd) {
        close(1);

        if (dup2(*outfd, 1) != 1) {
            LOG(log_error, logtype_afpd, "Failed to create stdout file descriptor");
            close(*outfd);
            exit(80);
        }
    }

    if (chdir("/") < 0) {
        LOG(log_error, logtype_afpd, "afprun: can't change directory to \"/\" %s",
            strerror(errno));
        exit(83);
    }

    /* now completely lose our privileges. This is a fairly paranoid
       way of doing it, but it does work on all systems that I know of */

    if (become_user_permanently(uid, gid) != 0) {
        exit(82);
    }

    if (getuid() != uid || geteuid() != uid || getgid() != gid
            || getegid() != gid) {
        /* we failed to lose our privileges - do not execute the command */
        exit(81); /* we can't print stuff at this stage, instead use exit codes for debugging */
    }

    /* close all other file descriptors, leaving only 0, 1 and 2. 0 and
       2 point to /dev/null from the startup code */
    {
        int fd;

        for (fd = 3; fd < 256; fd++) {
            close(fd);
        }
    }
    execl("/bin/sh", "sh", "-c", cmd, NULL);
    /* not reached */
    exit(82);
    return 1;
}

/*!
 * @brief Run a command in the background without waiting
 * @note being careful about uid/gid handling
 */
int afprun_bg(char *cmd)
{
    pid_t pid;
    uid_t uid = geteuid();
    gid_t gid = getegid();
    int fd, fdlimit = sysconf(_SC_OPEN_MAX);
    LOG(log_debug, logtype_afpd, "running %s as user %d", cmd, uid);

    /* in this method we will exec /bin/sh with the correct
       arguments, after first setting stdout to point at the file */

    if ((pid = fork()) < 0) {
        LOG(log_error, logtype_afpd, "afprun: fork failed with error %s",
            strerror(errno));
        return errno;
    }

    if (pid)
        /* parent, just return */
    {
        return 0;
    }

    /* we are in the child. we exec /bin/sh to do the work for us. we
       don't directly exec the command we want because it may be a
       pipeline or anything else the config file specifies */

    if (chdir("/") < 0) {
        LOG(log_error, logtype_afpd, "afprun: can't change directory to \"/\" %s",
            strerror(errno));
        exit(83);
    }

    /* now completely lose our privileges. This is a fairly paranoid
       way of doing it, but it does work on all systems that I know of */

    if (become_user_permanently(uid, gid) != 0) {
        exit(82);
    }

    if (getuid() != uid || geteuid() != uid || getgid() != gid
            || getegid() != gid) {
        /* we failed to lose our privileges - do not execute the command */
        exit(81);
    }

    fd = 3;

    while (fd < fdlimit) {
        close(fd++);
    }

    execl("/bin/sh", "sh", "-c", cmd, NULL);
    /* not reached */
    exit(82);
    return 1;
}
