/*
 * $Id: login.c,v 1.12 1999/11/02 09:41:51 agalat Rel $
 */

/*
 * This is login. Written to use the libpniam.
 *
 * The code was inspired from the one available at
 *
 *      ftp://ftp.daimi.aau.dk/pub/linux/poe/INDEX.html
 *
 * However, this file contains no code from the above software.
 *
 * Copyright (c) 1996,1997 Andrew G. Morgan <morgan@linux.kernel.org>
 *
 * Rewritten for PNIAM by Alexei V. Galatenko <agalat@castle.nmd.msu.ru>
 */

static const char rcsid[] =
"$Id: login.c,v 1.12 1999/11/02 09:41:51 agalat Rel $\n"
" - Login application. <saw@msu.ru>"
;

#include <ctype.h>
#include <fcntl.h>
#include <grp.h>
#include <paths.h>
#include <pwd.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <termios.h>
#include <unistd.h>
#include <signal.h>
#include <time.h>
/* should be in above(?): */ extern int vhangup(void);

#include <pniam.h>

#include "../../common/include/config.h"

#include "../../common/include/shell_args.h"
#include "../../common/include/wait4shell.h"
#include "../../common/include/login_indep.h"

#include "../include/make_env.h"
#include "../include/set_groups.h"
#include "../include/get_pniam_err.h"
#include "../include/com_lib.h"

/* delays and warnings... */

#define DEFAULT_SHELL             "/bin/sh"

#define GOODBYE_MESSAGE           ""  /* make "" for no message */
#define GOODBYE_DELAY             1  /* time to display good-bye */

#define SERIOUS_ABORT_DELAY       3600                    /* yes, an hour! */
#define STANDARD_DELAY            50    /* standard failures lead to this */

#define SLEEP_TO_KILL_CHILDREN    3  /* seconds to wait after SIGTERM before
					SIGKILL */
#define MIN_DELAY 1000000            /* minimum delay in usec -- take
				      * care that MIN_DELAY*2^MAX_LOGIN
				      * is not too large for (int) */

/*Internal states*/

#define LOGIN_STATE_ARGS_PARSED		    2
#define LOGIN_STATE_TERMINAL_OBTAINED	3
#define LOGIN_STATE_PNIAM_INITIALIZED	5
#define LOGIN_STATE_AUTHEN_SUCCESS	    7
#define LOGIN_STATE_SESSION_OPENED	    8
#define LOGIN_STATE_CREDENTIALS_SET	    9

/* internal strings and flags */

#define DEFAULT_HOME              "/"
#define LOGIN_ATTEMPT_FAILED      "Sorry, please try again\n\n"

/* for login session - after login */
#define TERMINAL_PERMS            (S_IRUSR|S_IWUSR | S_IWGRP)
#define TERMINAL_GROUP            "tty"      /* after login */

#define LOGIN_KEEP_ENV            01
#define LOGIN_FORCE_AUTH          04

#define LOGIN_TRUE                 1
#define LOGIN_FALSE                0

#define WRITE_UTMP                 1

/* ------ some static data objects ------- */

static const char *terminal_name=NULL;
static int login_flags=0;
static const char *login_remote_host="localhost";

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

/*
 *  This function is to be used in cases of programmer error.
 */

static void serious_abort(const char *s)
{
    (void) fprintf (stderr, "Login internal error: please seek help!\n");
    (void) fprintf (stderr, "This message will persist for an hour.\n");
    (void) fprintf (stderr, "The problem is that,\n\n %s\n\n", s);
    (void) fprintf (stderr, "Obviously, this should never happen! It could possibly be\n");
    (void) fprintf (stderr, "a problem with (Linux-)PNIAM -- A recently installed module\n");
    (void) fprintf (stderr, "perhaps? For reference, this is the version of this\n");
    (void) fprintf (stderr, "application:\n\n %s", rcsid);

    /* delay - to read the message */

    (void) sleep(SERIOUS_ABORT_DELAY);

    /* quit the program */

    exit(1);	
}

static pniam_result_t login_authenticate_user(struct pniam_handle *handle,
				pniam_request_t **request, char *user)
{
    pniam_result_t res;

    res = pniam_create_request (handle, request);
    if (res != PNIAM_OK)
        return res;
    /*
     *  This is the main authentication loop.
     */

    do {
        /* authenticate user */
        if (user != NULL) {
            res = pniam_item_add2list (&((*request)->input), "USER", 
            (unsigned char *)user, strlen (user), PNIAM_ITEM_DEFAULT);
            if (res != PNIAM_OK)
            break;
        }
        if (login_flags & LOGIN_FORCE_AUTH) 
            res = PNIAM_OK;
        else 
            res = authen (handle, *request);

        if (res == PNIAM_OK) {
            /* the user was authenticated - can they log in? */
            res = prepare_author (&((*request)->ok_replies));
            if (res != PNIAM_OK)
                break;
            res = author (handle, *request);

            if ((res == PNIAM_OK)||(res == PNIAM_AUTHTOKERR))
                break;

            /* was this a forced login? fail now if so */
            if (login_flags & LOGIN_FORCE_AUTH)
            break;
        }

        pniam_clear_request (*request);
        user = NULL;
    } while (res != PNIAM_ABORT);
    if ((res != PNIAM_OK)&&(res != PNIAM_AUTHTOKERR))
        pniam_destroy_request (handle, *request, PNIAM_PARENT);
    return res;
}

static void login_invoke_shell(const char *shell, uid_t uid, 
		const char *home, const char *user)
{
    char **shell_env=NULL;
    char * const *shell_args=NULL;
    const char *pw_dir=NULL;
    int retval, delay = 0;
    const struct group *gr;

    /*
     * We are the child here. This is where we invoke the shell.
     *
     * We gather the user information first.  Then "quietly"
     * close (Linux-)PAM [parent can do it normally], we then
     * take lose root privilege.
     */

    do {
	    pw_dir = home;
        if (!pw_dir || *pw_dir == '\0' || chdir(pw_dir)) {
            (void) fprintf (stderr, "home directory for %s does not work..", 
                                    user);
            if (!strcmp(pw_dir,DEFAULT_HOME) || chdir(DEFAULT_HOME)) {
                (void) fprintf (stderr, ". %s not available either; exiting\n", 
                                                        DEFAULT_HOME);
                break;
            }
            if (!pw_dir || *pw_dir == '\0') {
                (void) fprintf(stderr, ". setting to " DEFAULT_HOME "\n");
                pw_dir = DEFAULT_HOME;
            } 
            else 
                (void) fprintf(stderr, ". changing to " DEFAULT_HOME "\n");
        }

        /*
         * next we attempt to obtain the arglist
         */

        shell_args = build_shell_args(shell, LOGIN_TRUE, NULL);
        if (shell_args == NULL) {
            delay = STANDARD_DELAY;
            (void) fprintf (stderr, "unable to build shell arguments");
            break;
        }

        /*
         * Just before we shutdown PAM, we copy the PAM-environment to local
         * memory. (The parent process retains the PAM-environment so it can
         * shutdown using it, but the child is about to lose it)
         */

        /* now copy environment */

        retval = make_environment ((login_flags & LOGIN_KEEP_ENV), home, 
                                    &shell_env);
        if (retval != 0) {
            delay = STANDARD_DELAY;
            (void) fprintf (stderr, "environment corrupt; sorry..");
            break;
        }
        /* now set permissions on TTY */
        gr = getgrnam(TERMINAL_GROUP);
        if (gr == NULL) {
            delay = STANDARD_DELAY;
            (void) fprintf (stderr, "Failed to find `%s' group\n", 
                                TERMINAL_GROUP);
            break;
        }

        /* change owner of terminal */
        if (chown(terminal_name, uid, gr->gr_gid)
            || chmod(terminal_name, TERMINAL_PERMS)) {
            delay = STANDARD_DELAY;
            (void) fprintf (stderr, "Failed to change access permission	to terminal %s\n", terminal_name);
            break;
        }
        /*
         * become user irrevocably
         */
        if (setuid(uid) != 0) {
            (void) fprintf(stderr, "su: cannot assume uid\n");
            exit(1);
        }

        /* finally we invoke the user's preferred shell */

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

        /* should never get here */

    } while (0);
    (void) sleep(delay);
    exit (1);
}

/*
 * main program; login top-level skeleton
 */

void main(int argc, const char **argv)
{
    struct pniam_handle *handle;
    pniam_request_t *request;
    pniam_result_t res = PNIAM_OK;
    pniam_item_t *item;
    int retval=LOGIN_FALSE, status;
    pid_t child;
    uid_t uid= (uid_t) -1;
    const char *place, *err_descr = NULL;
    int state, delay;
    int retcode = 1;
    char *home = NULL, *shell = NULL, *user = NULL;
    char *hostname = "localhost";
    
    /*
     * Parse the arguments to login. There are static variables
     * above that indicate the intentions of the invoking process
     */

    parse_args(argc, argv, (const char **)&user, &login_remote_host, 
                                        &login_flags);
    state = LOGIN_STATE_ARGS_PARSED;

    do {
        /*
         * Obtain terminal
         */

        place = "obtain_terminal";
        if (getuid() == 0 && geteuid() == 0) 
            retval = login_get_terminal(&terminal_name);  
            /* must be root to try */

        if (retval != LOGIN_TRUE) {
            delay = 10;
            (void) fprintf (stderr, "unable to attach to terminal\n");
            err_descr = "terminal error";
            break;
        }
        state = LOGIN_STATE_TERMINAL_OBTAINED;

        place = "pniam_start";
        res = pniam_start("login", &handle);
        if (res != PNIAM_OK) {
            delay = 60;
            break;
        }
        /*request structure is initialized in login_authenticate_user*/
        state = LOGIN_STATE_PNIAM_INITIALIZED;

        /*
         * Proceed to authenticate the user.
         */

        place = "login_authenticate_user";
        res = login_authenticate_user(handle, &request, user);
        if ((res != PNIAM_OK)&&(res != PNIAM_AUTHTOKERR)) {
            delay = STANDARD_DELAY;
            break;
        }
        state = LOGIN_STATE_AUTHEN_SUCCESS;
        /*
         * Do we need to prompt the user for a new password?
         */

        place = "pam_chauthtok";
        if (res == PNIAM_AUTHTOKERR) {
            res = change (handle, request);
            if (res != PNIAM_OK) {
                delay = STANDARD_DELAY;
                break;
            }
        }

        /*
         * Open a session for the user.
         */

        place = "pniam_open_session";
        res = open_session(handle, request, hostname, WRITE_UTMP);
        if (res != PNIAM_OK) {
            delay = STANDARD_DELAY;
            break;
        }
        state = LOGIN_STATE_SESSION_OPENED;

        /*
         * Set the user's credentials
         */

        place = "set_user_credentials";
        res = set_groups (request->ok_replies);
        if (res != PNIAM_OK) {
            delay = STANDARD_DELAY;
            break;
        }
        res = pniam_get_data_piece (request->ok_replies, "USER", 
                                        (unsigned char **)(&user));
        if (res != PNIAM_OK) {
            delay = STANDARD_DELAY;
            break;
        }
        res = pniam_get_data_piece (request->ok_replies, "HOME", 
                                        (unsigned char **)(&home));
        if (res != PNIAM_OK) {
            delay = STANDARD_DELAY;
            break;
        }
        res = pniam_get_data_piece (request->ok_replies, "SHELL", 
                                        (unsigned char **)(&shell));
        if (res != PNIAM_OK) {
            delay = STANDARD_DELAY;
            break;
        }
        item = pniam_item_list_find (request->ok_replies, "UID");
        if ((item == NULL)||(item->len != sizeof (uid_t))||
                                                (item->data == NULL)) {
            delay = STANDARD_DELAY;
            break;
        }
        else
            uid = *((uid_t *)(item->data));
        state = LOGIN_STATE_CREDENTIALS_SET;

        /*
         * Summary: we know who the user is and all of their credentials
         *          From this point on, we have to be more careful to
         *          undo all that we have done when we shutdown.
         *
         * NOTE: Here the user is root. Though they actually have all the
         * group permissions appropriate for the user.
         */

        /*
         * This is where we fork() a process to deal with the user-shell
         * In the child execute the user's login shell, but in the parent
         * continue to interact with (Linux-)PNIAM.
         */

        child = fork();
        if (child == 0) {
            /*
             *  Process is child here.
             */
            pniam_destroy_request (handle, request, PNIAM_CHILD);
            pniam_end (handle);
            login_invoke_shell(shell, uid, home, user); /* never returns */

            serious_abort("shell failed to execute");
        }

        /*
         * Process is parent here... wait for the child to exit
         */

        prepare_for_job_control(0);
        status = wait_for_child(child);
    } while (0);

    /*Cleaning up*/
    do {
        if (state >= LOGIN_STATE_SESSION_OPENED) {
            /* close down */
            place = "pniam_account_end";
            res = pniam_account_end (handle, request);
            if (res != PNIAM_OK) {
                delay = 60;
                break;
            }
        }

        delay = GOODBYE_DELAY;
        retcode = 0;
    } while (0);

    /* exit - leave getty to deal with resetting the terminal */
    if (home != NULL)
	    free (home);
    if (shell != NULL)
	    free (shell);
    if (user != NULL)
        free (user);    

    if (state >= LOGIN_STATE_AUTHEN_SUCCESS)
        pniam_destroy_request (handle, request, PNIAM_PARENT);
    if (state >= LOGIN_STATE_PNIAM_INITIALIZED)
        pniam_end (handle);

    /* delay - to read the message */

    if (err_descr != NULL)
        if (res == PNIAM_OK)
            (void) fprintf (stderr, "%s: %s\n", place, err_descr);
        else
            (void) fprintf (stderr, "%s: %s\n", place, 
                get_pniam_err (res));
    else
        (void) fprintf (stderr, "%s\n", GOODBYE_MESSAGE); 

    /*Give time to read the goodbye message*/
    (void) sleep(delay);

    exit (retcode);
}
