/*
Copyright (C) 1997-2008 ZSNES Team ( zsKnight, _Demo_, pagefault, Nach )

http://www.zsnes.com
http://sourceforge.net/projects/zsnes
https://zsnes.bountysource.com

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
version 2 as published by the Free Software Foundation.

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., 675 Mass Ave, Cambridge, MA 02139, USA.
*/

#include "../gblhdr.h"

#include <grp.h>
#include <paths.h>
#include <pwd.h>
#include <signal.h>
#include <sys/param.h>
#include <sys/wait.h>

#ifndef OPEN_MAX
#define OPEN_MAX 256
#endif

#include "safelib.h"

#include "../argv.h"

// Introducing secure forking ;) -Nach

#ifdef __linux__
static int z_setreuid(uid_t uid)
{
    return (setreuid(uid, uid));
}

static int z_setregid(gid_t gid)
{
    return (setregid(gid, gid));
}
#else
static int z_setreuid(uid_t uid)
{
    seteuid(uid);
    return (setuid(uid));
}

static int z_setregid(gid_t gid)
{
    setegid(gid);
    return (setgid(gid));
}
#endif

// Taken from the secure programming cookbook, somewhat modified
static bool spc_drop_privileges()
{
    gid_t newgid = getgid(), oldgid = getegid();
    uid_t newuid = getuid(), olduid = geteuid();

    char* name = getlogin();
    struct passwd* userinfo;

    if (name) {
        userinfo = getpwnam(name);
    }

    if (!olduid && !newuid && userinfo && userinfo->pw_uid) // If currently using su to root or sudo
    {
        newuid = userinfo->pw_uid;
    }

    if (!oldgid && !newgid && userinfo && userinfo->pw_gid) // Same thing, but now check the group
    {
        newgid = userinfo->pw_gid;
    }

    // If the above failed, we check for root via +s as a mode on the binary

    // If root privileges are to be dropped, be sure to pare down the ancillary
    // groups for the process before doing anything else because the setgroups()
    // system call requires root privileges.  Drop ancillary groups regardless of
    // whether privileges are being dropped temporarily or permanently.

    if (!olduid) {
        setgroups(1, &newgid);
    }

    if (((newgid != oldgid) && (z_setregid(newgid) == -1)) || ((newuid != olduid) && (z_setreuid(newuid) == -1))) {
        return (false);
    }

    // verify that the changes were successful
    if (newgid != oldgid && (setegid(oldgid) != -1 || getegid() != newgid)) {
        return (false);
    }
    if (newuid != olduid && (seteuid(olduid) != -1 || geteuid() != newuid)) {
        return (false);
    }

    return (true);
}

static int open_devnull(int fd)
{
    FILE* f = 0;

    if (!fd) {
        f = freopen(_PATH_DEVNULL, "rb", stdin);
    } else if (fd == 1) {
        f = freopen(_PATH_DEVNULL, "wb", stdout);
    } else if (fd == 2) {
        f = freopen(_PATH_DEVNULL, "wb", stderr);
    }
    return (f && fileno(f) == fd);
}

static bool array_contains(int* a, size_t size, int key)
{
    size_t i;
    for (i = 0; i < size; i++) {
        if (a[i] == key) {
            return (true);
        }
    }
    return (false);
}

static bool spc_sanitize_files(int* a, size_t size, int skip)
{
    int fd, fds;
    struct stat st;

    // Make sure all open descriptors other than the standard ones are closed
    if ((fds = getdtablesize()) == -1) {
        fds = OPEN_MAX;
    }
    for (fd = 3; fd < fds; fd++) {
        if ((fd != skip) && !array_contains(a, size, fd)) {
            close(fd);
        }
    }

    // Verify that the standard descriptors are open.  If they're not, attempt to
    // open them using /dev/null.  If any are unsuccessful, fail.
    for (fd = 0; fd < 3; fd++) {
        if (fstat(fd, &st) == -1 && (errno != EBADF || !open_devnull(fd))) {
            return (false);
        }
    }
    return (true);
}

// Pass array of file descriptors to leave open
pid_t safe_fork(int* a, size_t size)
{
    int filedes[2];
    if (!pipe(filedes)) {
        char success = 0;
        pid_t childpid;
        if ((childpid = fork()) == -1) // Fork Failed
        {
            close(filedes[0]);
            close(filedes[1]);
            return (-1);
        }

        if (childpid) // Parent Process
        {
            close(filedes[1]); // Close writing
            read(filedes[0], &success, 1);
            close(filedes[0]);
            if (success) {
                return (childpid);
            }
            waitpid(childpid, 0, 0);
            return (-1);
        }

        // This is the child proccess

        close(filedes[0]); // Close reading

        if (!spc_sanitize_files(a, size, filedes[1]) || !spc_drop_privileges()) {
            write(filedes[1], &success, 1);
            close(filedes[1]);
            _exit(0);
        }

        success = 1;
        write(filedes[1], &success, 1);
        close(filedes[1]);
        return (0);
    }
    return (-1);
}

// Introducing a popen which doesn't return until it knows for sure of program launched or couldn't open -Nach

// Forks, parent is paused until child successfully execs (returns child pid) or child exits (returns failure)
static pid_t parent_pause_fork()
{
    int filedes[2];
    if (!pipe(filedes)) {
        int pid = fork();
        if (pid == -1) // Failed
        {
            close(filedes[0]);
            close(filedes[1]);
        } else if (pid > 0) // Parent
        {
            char success = 1;
            close(filedes[1]);
            read(filedes[0], &success, 1);
            close(filedes[0]);
            if (success) {
                return (pid);
            }
            waitpid(pid, 0, 0);
        } else // Child
        {
            close(filedes[0]);
            fcntl(filedes[1], F_SETFD, FD_CLOEXEC);
            return (-filedes[1]);
        }
    }
    return (0);
}

static void close_child(pid_t pid)
{
    char success = 0;
    write(-pid, &success, 1);
    close(-pid);
    _exit(0);
}

#define IS_PARENT(x) ((x) > 0)
#define IS_CHILD(x) ((x) < 0)
#define IS_FAIL(x) ((x) == 0)

static struct fp_pid_link {
    FILE* fp;
    pid_t pid;
    struct fp_pid_link* next;
} fp_pids = { 0, 0, 0 };

FILE* safe_popen(char* command, const char* mode)
{
    // filedes[0] is for reading
    // filedes[1] is for writing.
    int filedes[2];

    if (mode && (*mode == 'r' || *mode == 'w') && !pipe(filedes)) {
        pid_t childpid = parent_pause_fork();
        if (IS_PARENT(childpid)) {
            FILE* fp;
            if (*mode == 'r') {
                close(filedes[1]);
                fp = fdopen(filedes[0], "r");
            } else {
                close(filedes[0]);
                fp = fdopen(filedes[1], "w");
            }

            if (fp) {
                struct fp_pid_link* link = &fp_pids;
                while (link->next) {
                    link = link->next;
                }

                link->next = (struct fp_pid_link*)malloc(sizeof(struct fp_pid_link));
                if (link->next) {
                    link->next->fp = fp;
                    link->next->pid = childpid;
                    link->next->next = 0;
                    return (fp);
                }
                fclose(fp);
            }
            kill(childpid, SIGTERM);
            waitpid(childpid, 0, 0);
        } else if (IS_CHILD(childpid)) {
            char** argv = build_argv(command);
            if (argv) {
                if (*mode == 'r') {
                    dup2(filedes[1], STDOUT_FILENO);
                } else {
                    dup2(filedes[0], STDIN_FILENO);
                }

                if (spc_sanitize_files(0, 0, -childpid) && spc_drop_privileges()) {
                    execvp(argv[0], argv);
                }
                free(argv);
            }
            close_child(childpid);
        }
        close(filedes[0]);
        close(filedes[1]);
    }
    return (0);
}

void safe_pclose(FILE* fp)
{
    struct fp_pid_link* link = &fp_pids;

    while (link->next && link->next->fp != fp) {
        link = link->next;
    }
    if (link->next->fp == fp) {
        struct fp_pid_link* dellink = link->next;
        fclose(fp);
        waitpid(link->next->pid, 0, 0);
        link->next = link->next->next;
        free(dellink);
    }
}
