#define ROOT_UID                  0

#define _BSD_SOURCE                  /* for setgroups() and other calls */
#include <stdlib.h>
#include <signal.h>
#include <termios.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <pwd.h>
#include <grp.h>
#include <sys/file.h>
#include <string.h>
#include <stdarg.h>
#include <syslog.h>
#include <errno.h>

/* -------------------------------------------- */
/* ------ declarations ------------------------ */
/* -------------------------------------------- */

int is_terminal = 0;

/* -------------------------------------------- */
/* ------ some local (static) functions ------- */
/* -------------------------------------------- */

/* ------ terminal mode ----------------------- */

struct termios stored_mode;        /* initial terminal mode settings */

/* should be called once at the beginning */
void store_terminal_modes()
{
    if (isatty(STDIN_FILENO)) {
	is_terminal = 1;
	if (tcgetattr(STDIN_FILENO, &stored_mode) != 0) {
	    (void) fprintf(stderr, "su: couldn't copy terminal mode");
	    exit(1);
	}
    } else if (getuid()) {
	(void) fprintf(stderr, "su: must be run from a terminal\n");
	exit(1);
    } else 
	is_terminal = 0;
}

/*
 * Returns:
 *   0     ok
 * !=0     error
 */
int reset_terminal_modes()
{
    if (is_terminal && tcsetattr(STDIN_FILENO, TCSAFLUSH, &stored_mode) != 0) {
	(void) fprintf(stderr, "su: cannot reset terminal mode: %s\n"
		       , strerror(errno));
	return 1;
    }else
	return 0;
}

/* ------ unexpected signals ------------------ */

struct sigaction old_int_act, old_quit_act, old_tstp_act, old_pipe_act;

void disable_terminal_signals()
{
    /* 
     * Protect the process from dangerous terminal signals.
     * The protection is implemented via sigaction() because
     * the signals are sent regardless of the process' uid.
     */
    struct sigaction act;

    act.sa_handler = SIG_IGN;  /* ignore the signal */
    sigemptyset(&act.sa_mask); /* no signal blocking on handler
				  call needed */
    act.sa_flags = SA_RESTART; /* do not reset after first signal
				  arriving, restart interrupted
				  system calls if possible */
    sigaction(SIGINT, &act, &old_int_act);
    sigaction(SIGQUIT, &act, &old_quit_act);
    /*
     * Ignore SIGTSTP signals. Why? attacker could otherwise stop
     * a process and a. kill it, or b. wait for the system to
     * shutdown - either way, nothing appears in syslogs.
     */
    sigaction(SIGTSTP, &act, &old_tstp_act);
    /*
     * Ignore SIGPIPE. The parent `su' process may print something
     * on stderr. Killing of the process would be undesired.
     */
    sigaction(SIGPIPE, &act, &old_pipe_act);
}

void enable_terminal_signals()
{
    sigaction(SIGINT, &old_int_act, NULL);
    sigaction(SIGQUIT, &old_quit_act, NULL);
    sigaction(SIGTSTP, &old_tstp_act, NULL);
    sigaction(SIGPIPE, &old_pipe_act, NULL);
}

/* ------ terminal ownership ------------------ */

uid_t terminal_uid = (uid_t) -1;

/*
 * Change the ownership of STDIN if needed.
 * Returns:
 *   0     ok,
 *  -1     fatal error (continue of the work is impossible),
 *   1     non-fatal error.
 * In the case of an error "err_descr" is set to the error message
 * and "callname" to the name of the failed call.
 */
int change_terminal_owner(uid_t uid, int is_login, 
	  const char **callname, const char **err_descr)
{
    /* determine who owns the terminal line */
    if (is_terminal && is_login) {
	struct stat stat_buf;

	if (fstat(STDIN_FILENO,&stat_buf) != 0) {
            *callname = "fstat to STDIN";
	    *err_descr = strerror(errno);
	    return -1;
	}
	if(fchown(STDIN_FILENO, uid, -1) != 0) {
	    *callname = "fchown to STDIN";
            *err_descr = strerror(errno);
	    return 1;
	}
	terminal_uid = stat_buf.st_uid;
    }
    return 0;
}

void restore_terminal_owner()
{
    if (terminal_uid != (uid_t) -1) {
        if(fchown(STDIN_FILENO, terminal_uid, -1) != 0) {
            openlog("su", LOG_CONS | LOG_PERROR | LOG_PID, LOG_AUTHPRIV);
	    syslog(LOG_ALERT
		    , "Terminal owner hasn\'t been restored: %s"
		    , strerror(errno));
	    closelog();
        }
        terminal_uid = (uid_t) -1;
    }
}

/* ------ Process Death Control (tm) ---------- */

uid_t invoked_uid;

/*
 * Make the process unkillable by the user invoked it.
 * Returns:
 *   0     ok,
 *  -1     fatal error (continue of the work is impossible),
 *   1     non-fatal error.
 * In the case of an error "err_descr" is set to the error message
 * and "callname" to the name of the failed call.
 */
int make_process_unkillable(const char **callname
        , const char **err_descr)
{
    invoked_uid = getuid();
    if(setuid(geteuid()) != 0) {
        *callname = "setuid";
	*err_descr = strerror(errno);
	return -1;
    }else
	return 0;
}

void make_process_killable()
{
    setreuid(invoked_uid, -1);
}

/* ------ command line parser ----------------- */

void usage()
{
    (void) fprintf(stderr,"usage: su [-] [-c \"command\"] [username]\n");
    exit(1);
}

void parse_command_line(int argc, char *argv[]
	, int *is_login, const char **user, const char **command)
{
    int username_present, command_present;

    *is_login = 0;
    *user = NULL;
    *command = NULL;
    username_present = command_present = 0;

    while ( --argc > 0 ) {
	const char *token;

	token = *++argv;
	if (*token == '-') {
	    switch (*++token) {
	    case '\0':             /* su as a login shell for the user */
		if (*is_login)
		    usage();
		*is_login = 1;
		break;
	    case 'c':
		if (command_present) {
		    usage();
		} else {               /* indicate we are running commands */
		    if (*++token != '\0') {
			command_present = 1;
			*command = token;
		    } else if (--argc > 0) {
			command_present = 1;
			*command = *++argv;
		    } else
			usage();
		}
		break;
	    default:
		usage();
	    }
	} else {                       /* must be username */
	    if (username_present)
		usage();
	    username_present = 1;
	    *user = *argv;
	}
    }

    if (!username_present) {           /* default user is superuser */
	const struct passwd *pw;

	pw = getpwuid(ROOT_UID);
	if (pw == NULL)                               /* No ROOT_UID!? */
	{
	    printf ("\nsu:no access to superuser identity!? (%d)\n", 
				ROOT_UID);
	    exit (1);
	}
	
	*user = NULL;
	if (pw->pw_name != NULL)
	    *user = strdup(pw->pw_name);
    }
}
