/*
 * kdesu_stub.c: KDE su executes this stub through su or ssh. This stub in turn
 *               executes the target program. Before that, startup parameters
 *               are sent through stdin.
 *
 * This file is part of the KDE project, module kdesu.
 * Copyright (C) 1999,2000 Geert Jansen <jansen@kde.org>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 *
 * Available parameters:
 *
 *   Parameter       Description         Format (csl = comma separated list)
 *
 * - kdesu_stub      Header              "ok" | "stop"
 * - display         X11 display         string
 * - display_auth    X11 authentication  "type cookie" pair
 * - command         Command to run      string
 * - path            PATH env. var       string
 * - build_sycoca    Rebuild sycoca?     "yes" | "no"
 * - user            Target user         string
 * - priority        Process priority    0 <= int <= 100
 * - scheduler       Process scheduler   "fifo" | "normal"
 * - app_startup_id  DESKTOP_STARTUP_ID  string
 * - environment     Additional envvars  strings, last one is empty
 */

#include <config-kdesu.h>

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <pwd.h>
#include <termios.h>
#include <signal.h>

#if HAVE_INITGROUPS
#include <grp.h>
#endif

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <sys/resource.h>

#ifdef POSIX1B_SCHEDULING
#include <sched.h>
#endif

/**
 * Params sent by the peer.
 */

struct param_struct {
    const char *name;
    char *value;
};

struct param_struct params[] = {
    { "kdesu_stub", 0L },
    { "display", 0L },
    { "display_auth", 0L },
    { "command", 0L },
    { "path", 0L },
    { "xwindows_only", 0L },
    { "user", 0L },
    { "priority", 0L },
    { "scheduler", 0L },
    /* obsoleted by app_startup_id    { "app_start_pid", 0L } */
    { "app_startup_id", 0L }
};

#define P_HEADER 0
#define P_DISPLAY 1
#define P_DISPLAY_AUTH 2
#define P_COMMAND 3
#define P_PATH 4
#define P_XWIN_ONLY 5
#define P_USER 6
#define P_PRIORITY 7
#define P_SCHEDULER 8
#define P_APP_STARTUP_ID 9
#define P_LAST 10

/**
 * Safe malloc functions.
 */
char *xmalloc(size_t size)
{
    char *ptr = malloc(size);
    if (ptr) {
        return ptr;
    }
    perror("malloc()");
    exit(1);
}

char **xrealloc(char **ptr, int size)
{
    ptr = realloc(ptr, size);
    if (ptr) {
        return ptr;
    }
    perror("realloc()");
    exit(1);
}

/**
 * Solaris does not have a setenv()...
 */
int xsetenv(const char *name, const char *value)
{
    char *s = malloc(strlen(name) + strlen(value) + 2);
    if (!s) {
        return -1;
    }
    strcpy(s, name);
    strcat(s, "=");
    strcat(s, value);
    return putenv(s); /* yes: no free()! */
}

/**
 * Safe strdup and strip newline
 */
char *xstrdup(char *src)
{
    int len = strlen(src);
    char *dst = xmalloc(len + 1);
    strcpy(dst, src);
    if (dst[len - 1] == '\n') {
        dst[len - 1] = '\000';
    }
    return dst;
}

/**
 * Split comma separated list.
 */
char **xstrsep(char *str)
{
    int i = 0;
    int size = 10;
    char **list = (char **)xmalloc(size * sizeof(char *));
    char *ptr = str, *nptr;
    while ((nptr = strchr(ptr, ',')) != 0L) {
        if (i > size - 2) {
            list = xrealloc(list, (size *= 2) * sizeof(char *));
        }
        *nptr = '\000';
        list[i++] = ptr;
        ptr = nptr + 1;
    }
    if (*ptr != '\000') {
        list[i++] = ptr;
    }
    list[i] = 0L;
    return list;
}

#define BUFSIZE 8192

static void dequote(char *buf)
{
    char *in, *out;
    for (in = buf, out = buf; *in; in++, out++) {
        char c = *in;
        if (c == '\\') {
            c = *++in;
            if (c == '/') {
                *out = '\\';
            } else {
                *out = c - '@';
            }
        } else {
            *out = c;
        }
    }
    *out = 0;
}

/**
 * The main program
 */

int main()
{
    char buf[BUFSIZE + 1];
    char xauthority[200];
    int i, prio;
    pid_t pid;
    FILE *fout;
    struct passwd *pw;
    const char *kdesu_lc_all;

    xauthority[0] = '\0';

    /* Get startup parameters. */

    for (i = 0; i < P_LAST; i++) {
        printf("%s\n", params[i].name);
        fflush(stdout);
        if (fgets(buf, BUFSIZE, stdin) == 0L) {
            printf("end\n"); fflush(stdout);
            perror("kdesu_stub: fgets()");
            exit(1);
        }
        params[i].value = xstrdup(buf);
        /* Installation check? */
        if (i == 0 && !strcmp(params[i].value, "stop")) {
            printf("end\n");
            exit(0);
        }
    }
    printf("environment\n");
    fflush(stdout);
    for (;;) {
        char *tmp;
        if (fgets(buf, BUFSIZE, stdin) == 0L) {
            printf("end\n"); fflush(stdout);
            perror("kdesu_stub: fgets()");
            exit(1);
        }
        dequote(buf);
        tmp = xstrdup(buf);
        if (tmp[0] == '\0') { /* terminator */
            break;
        }
        putenv(tmp);
    }

    printf("end\n");
    fflush(stdout);

    xsetenv("PATH", params[P_PATH].value);
    xsetenv("DESKTOP_STARTUP_ID", params[P_APP_STARTUP_ID].value);

    kdesu_lc_all = getenv("KDESU_LC_ALL");
    if (kdesu_lc_all != NULL) {
        xsetenv("LC_ALL", kdesu_lc_all);
    } else {
        unsetenv("LC_ALL");
    }

    unsetenv("XDG_RUNTIME_DIR");

    /* Do we need to change uid? */

    pw = getpwnam(params[P_USER].value);
    if (pw == 0L) {
        printf("kdesu_stub: user %s does not exist!\n", params[P_USER].value);
        exit(1);
    }
    xsetenv("HOME", pw->pw_dir);

    /* New user won't be able to connect it anyway. */
    unsetenv("DBUS_SESSION_BUS_ADDRESS");

    /* Set scheduling/priority */

    prio = atoi(params[P_PRIORITY].value);
    if (!strcmp(params[P_SCHEDULER].value, "realtime")) {
#ifdef POSIX1B_SCHEDULING
        struct sched_param sched;
        int min = sched_get_priority_min(SCHED_FIFO);
        int max = sched_get_priority_max(SCHED_FIFO);
        sched.sched_priority = min + (int)(((double) prio) * (max - min) / 100 + 0.5);
        sched_setscheduler(0, SCHED_FIFO, &sched);
#else
        printf("kdesu_stub: realtime scheduling not supported\n");
#endif
    } else {
#if HAVE_SETPRIORITY
        int val = 20 - (int)(((double) prio) * 40 / 100 + 0.5);
        setpriority(PRIO_PROCESS, getpid(), val);
#endif
    }

    /* Drop privileges (this is permanent) */

    if (getuid() != pw->pw_uid) {
        if (setgid(pw->pw_gid) == -1) {
            perror("kdesu_stub: setgid()");
            exit(1);
        }
#if HAVE_INITGROUPS
        if (initgroups(pw->pw_name, pw->pw_gid) == -1) {
            perror("kdesu_stub: initgroups()");
            exit(1);
        }
#endif
        if (setuid(pw->pw_uid) == -1) {
            perror("kdesu_stub: setuid()");
            exit(1);
        }
        xsetenv("HOME", pw->pw_dir);
    }

    /* Handle display */

    if (strcmp(params[P_DISPLAY].value, "no")) {
        xsetenv("DISPLAY", params[P_DISPLAY].value);
        if (params[P_DISPLAY_AUTH].value[0]) {
            int fd2;
            /*
             ** save umask and set to 077, so we create those files only
             ** readable for root. (if someone else could read them, we
             ** are in deep shit).
             */
            int oldumask = umask(077);
            const char *disp = params[P_DISPLAY].value;
            if (strncmp(disp, "localhost:", 10) == 0) {
                disp += 9;
            }

            strcpy(xauthority, "/tmp/xauth.XXXXXXXXXX");
            fd2 = mkstemp(xauthority);
            umask(oldumask);

            if (fd2 == -1) {
                perror("kdesu_stub: mkstemp()");
                exit(1);
            } else {
                close(fd2);
            }
            xsetenv("XAUTHORITY", xauthority);

            fout = popen("xauth >/dev/null 2>&1", "w");
            if (fout == NULL) {
                perror("kdesu_stub: popen(xauth)");
                exit(1);
            }
            fprintf(fout, "add %s %s\n", disp, params[P_DISPLAY_AUTH].value);
            pclose(fout);
        }
    }

    /* Rebuild the sycoca and start kdeinit? */

    if (strcmp(params[P_XWIN_ONLY].value, "no")) {
        system("kdeinit5 --suicide");
    }

    /* Execute the command */

    pid = fork();
    if (pid == -1) {
        perror("kdesu_stub: fork()");
        exit(1);
    }
    if (pid) {
        /* Parent: wait for child, delete tempfiles and return. */
        int ret, state, xit = 1;
        while (1) {
            ret = waitpid(pid, &state, 0);
            if (ret == -1) {
                if (errno == EINTR) {
                    continue;
                }
                if (errno != ECHILD) {
                    perror("kdesu_stub: waitpid()");
                }
                break;
            }
            if (WIFEXITED(state)) {
                xit = WEXITSTATUS(state);
            }
        }

        if (*xauthority) {
            unlink(xauthority);
        }
        exit(xit);
    } else {
        setsid();
        /* Child: exec command. */
        sprintf(buf, "%s", params[P_COMMAND].value);
        dequote(buf);
        execl("/bin/sh", "sh", "-c", buf, (void *)0);
        perror("kdesu_stub: exec()");
        _exit(1);
    }
}
