/* 
 * $Id: su.c,v 1.26 1999/02/18 12:29:56 saw Rel $
 *
 * Based on `su' implementation for Linux-PAM by
 * Andrew Morgan <morgan@linux.kernel.org>
 *
 * Modified by Andrey V. Savochkin <saw@msu.ru>
 *
 * Rewritten for PNIAM by Alexei V. Galatenko <agalat@castle.nmd.msu.ru>
 *
 */

#define ROOT_UID                  0
#define DEFAULT_HOME              "/"
#define DEFAULT_SHELL             "/bin/sh"  
#define SLEEP_TO_KILL_CHILDREN    3  /* seconds to wait after SIGTERM before
					SIGKILL */
#define SU_FAIL_DELAY     2000000    /* usec on authentication failure */

#include <stdlib.h>
#include <signal.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <pwd.h>
#include <grp.h>
#include <string.h>
#include <syslog.h>
#include <errno.h>
#include <stdarg.h>

#include <pniam.h>

/*PNIAM-dependent*/
#include "../include/set_groups.h"
#include "../include/wtmp-gate.h"
#include "../include/make_env.h"
#include "../include/get_pniam_err.h"
#include "../include/com_lib.h"
#include "../include/get_item.h"

/*PNIAM-independent*/
#include "../../common/include/su_indep.h"
#include "../../common/include/shell_args.h"
#include "../../common/include/wait4shell.h"
#include "../../common/include/wtmp.h"
#include "../../common/include/checkfds.h"

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

static int state;

#define SU_STATE_HANDLE_INITIALIZED  2
#define SU_STATE_REQUEST_INITIALIZED 3
#define SU_STATE_AUTHENTICATED       4
#define SU_STATE_AUTHORIZED          5
#define SU_STATE_SESSION_OPENED      6
#define SU_STATE_CREDENTIALS_GOTTEN  7
#define SU_STATE_PROCESS_UNKILLABLE  8
#define SU_STATE_TERMINAL_REOWNED    9
#define SU_STATE_UTMP_WRITTEN        10

static void exit_child_now(int exit_code, struct pniam_handle *handle, 
		pniam_request_t *request, const char *format, ...);
static void su_exec_shell(const char *shell, uid_t uid, int is_login,
  	    struct pniam_handle *handle, pniam_request_t *request
	   ,const char *command, const char *user, const char *home);

/* -------------------------------------------- */
/* ------ the application itself -------------- */
/* -------------------------------------------- */

void main(int argc, char *argv[])
{
    int retcode, is_login, status;
    pniam_result_t retval;
    const char *command, *user;
    char *home = NULL;
    char *shell = NULL;
    pid_t child;
    uid_t uid;
    pniam_request_t *request;
    pniam_item_t *item;
    struct pniam_handle *handle;
    const char *place, *err_descr, *pniam_err;

    checkfds();

    /*
     * store terminal modes for later
     */
    store_terminal_modes();

    /*
     * Turn off terminal signals - this is to be sure that su gets a
     * chance to call pniam_end() in spite of the frustrated user
     * pressing Ctrl-C. (Only the superuser is exempt in the case that
     * they are trying to run su without a controling tty).
     */
    disable_terminal_signals();

    /* ------------ parse the argument list ----------- */

    parse_command_line(argc, argv, &is_login, &user, &command);

    /* ------ initialize the Linux-PNIAM interface ------ */

    /*
     * Starting from here all changes to the process and environment
     * state are reflected in the change of "state".
     * Random exits are strictly prohibited :-)
     *
     * If we break loop because of an error we either have got
     * retval != PNIAM_OK or set err_descr.  (SAW)
     */
    status = 1;                       /* fake exit status of a child */
    err_descr = NULL;
    do {                              /* abuse loop to avoid using goto... */

	place = "pniam_start";
	retval = pniam_start ("su", &handle);
	if (retval != PNIAM_OK)
	    break;
	state = SU_STATE_HANDLE_INITIALIZED;

	place = "pniam_create_request";
	retval = pniam_create_request (handle, &request);
	if (retval != PNIAM_OK)
	    break;
	state = SU_STATE_REQUEST_INITIALIZED;

        /*
         * Note. We have forgotten everything about the user. We will get
         * this info back after the user has been authenticated..
         */

	place = "item_add";
	retval = item_add (&(request->input), "USER", (unsigned char *)user, 
			strlen (user), PNIAM_ITEM_DEFAULT);
	if (retval != PNIAM_OK)
	    break; 

        place = "authentication";
        retval = authen (handle, request); /* authenticate the user */
	if (retval != PNIAM_OK)
            break;
	state = SU_STATE_AUTHENTICATED;

	/*
	 * The user is valid, but should they have access at this
	 * time?
	 */

	place = "authorization_setup";
	retval = prepare_author (&(request->ok_replies));
	if (retval != PNIAM_OK)
	    break;

	place = "authorization";
        retval = author (handle, request);
	if (retval != PNIAM_OK) {
	    if (getuid() == 0) 
		(void) fprintf(stderr, "Authorization:- \n(Ignored)\n");
    	    else 
		break;
	}
	state = SU_STATE_AUTHORIZED;

	/* 
	 * We open the su-session. 
         */

	place = "account_start";
        retval = open_session(handle, request);  /* Must take care to close */
	if (retval != PNIAM_OK)
        if (getuid() == 0) 
            (void) fprintf(stderr, "Account management error\n(Ignored)\n");
        else
            break;
	state = SU_STATE_SESSION_OPENED;

	place = "make_process_unkillable";
	if (make_process_unkillable(&place, &err_descr) != 0)
	    break;
	state = SU_STATE_PROCESS_UNKILLABLE;

	place = "pniam_get_uid";
	item = pniam_item_list_find (request->ok_replies, "UID");
	if (item == NULL || item->data == NULL || item->len != sizeof(uid_t)) {
	    err_descr = "failed";
	    break;
	}
	uid = *((uid_t *)(item->data));

	place = "pniam_get_data";
    retval = get_item (request->ok_replies, "HOME",
            &home);
    if (retval != PNIAM_OK)
        break;
    retval = get_item (request->ok_replies, "SHELL",
            &shell);
    if (retval != PNIAM_OK)
        break;
    if (shell == NULL)
    {
        /*shell unidentified; take default shell*/
        shell = (char *)malloc (sizeof (DEFAULT_SHELL));
        if (shell == NULL)
        {
            err_descr = "not enough memory";
            break;
        }
        memcpy (shell, DEFAULT_SHELL, sizeof (DEFAULT_SHELL));
    }            

    retcode = change_terminal_owner(uid, is_login
            , &place, &err_descr);
	if (retcode > 0) {
	    (void) fprintf(stderr, "su: %s: %s\n", place, err_descr);
	    err_descr = NULL; /* forget about the problem */
	}
	else if (retcode < 0)
	    break;
	state = SU_STATE_TERMINAL_REOWNED;

        if (is_login) {
            /* [uw]tmp writing */
            retcode = utmp_open_session(request->ok_replies, getpid(), 
                                    &place, &err_descr);
            if (retcode > 0) {
                (void) fprintf(stderr, "su: %s: %s\n", place, err_descr);
                err_descr = NULL; /* forget about the problem */
            } else if (retcode < 0)
                break;
            state = SU_STATE_UTMP_WRITTEN;
        }

	/* this is where we execute the user's shell */
        child = fork();
        if (child == -1) {
            place = "fork";
            err_descr = strerror(errno);
            break;
        }

        if (child == 0) { /* child exec's shell */
            su_exec_shell(shell, uid, is_login, handle, request,
                            command, user, home);
            /* never reached */
        }

	/* wait for child to terminate */

        /* job control is off for login sessions */
        prepare_for_job_control(!is_login && command != NULL);
	status = wait_for_child(child);

    }while (0);                       /* abuse loop to avoid using goto... */

    if (retval != PNIAM_OK) { 
        /*An error occured while invoking one of PNIAM functions*/
        pniam_err = get_pniam_err (retval);
        (void) fprintf(stderr, "\nsu: %s: %s\n", place, pniam_err);
    } else if (err_descr != NULL) {
        /*Other error has happened*/ 
        (void) fprintf(stderr, "\nsu: %s: %s\n", place, err_descr);
    }

    /* do [uw]tmp cleanup */
    if (state >= SU_STATE_UTMP_WRITTEN) {
        retcode = utmp_close_session(&place, &err_descr);
        if (retcode)
            (void) fprintf(stderr, "su: %s: %s\n", place, err_descr);
    }

    /* return terminal to local control */
    if (state >= SU_STATE_TERMINAL_REOWNED)
	restore_terminal_owner();

    if (state >= SU_STATE_PROCESS_UNKILLABLE)
	make_process_killable();

    if (state >= SU_STATE_SESSION_OPENED) {
	/* Close session. */

	retval = close_session (handle, request);
	if (retval != PNIAM_OK) 
	    (void) fprintf(stderr,"WARNING: could not close session\n\t\n");
    }
    if (home != NULL)
        free (home);
    if (shell != NULL)
        free (shell);

    /* clean up */
    if (state >= SU_STATE_REQUEST_INITIALIZED)
        pniam_destroy_request (handle, request, PNIAM_PARENT);
    if (state >= SU_STATE_HANDLE_INITIALIZED)
        pniam_end(handle);

    /* reset the terminal */
    if (reset_terminal_modes() != 0 && !status)
	    status = 1;

    exit(status);                 /* transparent exit */
}

static void exit_child_now(int exit_code, struct pniam_handle *handle
		, pniam_request_t *request,const char *format, ...)
{
    va_list args;

    va_start(args,format);
    vfprintf(stderr, format, args);
    va_end(args);

    if (handle != NULL)
    {
        if (request != NULL)
            pniam_destroy_request (handle, request, PNIAM_CHILD);
        pniam_end(handle);
    }

    exit(exit_code);
}

/* ------ shell invoker ----------------------- */

static void su_exec_shell(const char *shell, uid_t uid, int is_login
		  , struct pniam_handle *handle, pniam_request_t *request
		  , const char *command, const char *user, const char *home)
{
    char * const * shell_args;
    char **shell_env;
    const char *dir;
    int retcode;

    retcode = set_groups (request->ok_replies);
    if (retcode != PNIAM_OK)
	exit_child_now(1, handle, request, "su: group setup failed; exiting\n");

    dir = home;
    if (!dir || dir[0] == '\0') 
    {
	/* Not set so far, so we get it now. */
	(void) fprintf(stderr, "su: setting home directory for %s to %s\n"
		       , user, DEFAULT_HOME);
	dir = DEFAULT_HOME;
    }

    if (is_login && chdir(dir)) 
	exit_child_now(1, handle, request, "su: %s not available; exiting\n"
                , dir);

    /* Break up the shell command into a command and arguments. */
    shell_args = build_shell_args(shell, is_login, command);
    if (shell_args == NULL) 
	exit_child_now(1, handle, request
                , "su: could not identify appropriate shell\n");

    pniam_destroy_request (handle, request, PNIAM_CHILD);	
    pniam_end(handle);
    user = NULL;                            /* user's name not valid now */
    if (setuid(uid) != 0) 
	exit_child_now(1, NULL, NULL, "su: cannot assume uid\n");

    /*
     * Restore a signal status: information if the signal is ingored
     * is inherited accross exec() call.  (SAW)
     */
    enable_terminal_signals();

    /* Set shell environment. */
    retcode = make_environment(!is_login, home, &shell_env);
    if (retcode != 0)
        exit_child_now(1, NULL, NULL, "su: cannot get environment\n");

    execve(shell_args[0], shell_args+1, (char * const *)shell_env);

    free_env (shell_env);
    exit_child_now(1, NULL, NULL, "su: exec failed\n");
}
