/*
 * $Id: login.c,v 1.5 1997/06/24 14:18:56 morgan Exp morgan $
 *
 * $Log: login.c,v $
 * Revision 1.5  1997/06/24 14:18:56  morgan
 * update for .55
 *
 * Revision 1.4  1997/02/24 05:59:58  morgan
 * delay only if function available
 * 10 second missuse delay (not 60)
 *
 * Revision 1.3  1997/02/11 15:21:30  morgan
 * update for .54 release
 *
 * Revision 1.2  1997/01/29 03:35:03  morgan
 * update for release
 *
 * Revision 1.1  1996/12/01 00:57:40  morgan
 * Initial revision
 *
 */

/*
 * This is login. Written to use the libpam and (optionally the
 * libpwdb) librarie(s),
 *
 * 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@parc.power.net>
 */

static const char rcsid[] =
"$Id: login.c,v 1.5 1997/06/24 14:18:56 morgan Exp morgan $\n"
" - Login application. <morgan@parc.power.net>"
;

#include <ctype.h>
#include <fcntl.h>
#define __USE_BSD
#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>
/* should be in above(?): */ extern int vhangup(void);

#include <security/pam_appl.h>
#include <security/pam_misc.h>

#ifdef HAVE_PWDB
#include <pwdb/pwdb_public.h>
#endif /* HAVE_PWDB */

/* delays and warnings... */

#define DEFAULT_SHELL             "/bin/sh"

#define LOGIN_WARNING_TIMEOUT     65
#define LOGIN_WARNING_TEXT        "\a..Hurry! Login will terminate soon..\n"

#define LOGIN_ABORT_TIMEOUT       80
#define LOGIN_ABORT_TEXT          "\a..Login canceled!\n"

#define MAX_LOGIN                 3  /* largest tolerated delay */
#define SLEEP_AFTER_MAX_LOGIN     5  /* failed login => delay before retry */

#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            5      /* 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) */

#include "../inc/make_env.-c"
#include "../inc/setcred.-c"
#include "../inc/shell_args.-c"
#include "../inc/wait4shell.-c"
#include "../inc/wtmp.-c"

/* internal strings and flags */

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

#define LOGIN_PERMS               (S_IRUSR|S_IWUSR)    /* for login prompts */

/* 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_HAVE_RHOST          02
#define LOGIN_FORCE_AUTH          04
#define LOGIN_GARBLED            010

#define LOGIN_TRUE                 1
#define LOGIN_FALSE                0

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

static struct pam_conv conv = {
    misc_conv,                   /* defined in <security/pam_misc.h> */
    NULL
};

static pam_handle_t *pamh=NULL;
static const char *user=NULL;
static const char *terminal_name=NULL;
static int login_flags=0;
static const char *login_remote_host="localhost";
static const char *login_remote_user="[system]";
static const char *login_prompt = "Login: ";    /* default first time round */
static const char *user_prompt = "Login: ";     /* default second, third... */

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

/*
 * set up the conversation timeout facilities.
 */

static void set_timeout(int set)
{
    if (set) {
	time_t now;

	(void) time(&now);
	pam_misc_conv_warn_time = now + LOGIN_WARNING_TIMEOUT;
	pam_misc_conv_die_time  = now + LOGIN_ABORT_TIMEOUT;
	pam_misc_conv_warn_line = LOGIN_WARNING_TEXT;
	pam_misc_conv_die_line  = LOGIN_ABORT_TEXT;
    } else {
	pam_misc_conv_warn_time = 0;                   /* cancel timeout */
	pam_misc_conv_die_time  = 0;
    }
}

static int exit_program(int retval, int delay, int pam_retval
			, const char *fmt, ... )
{
    /* Print some complaint/comment */

    va_list ap;
    va_start(ap, fmt);
    vfprintf(stderr, fmt, ap);
    va_end(ap);

    /* clean up */

    if (pamh != NULL)
	(void) pam_end(pamh, pam_retval);

#ifdef HAVE_PWDB
    while (pwdb_end() == PWDB_SUCCESS);
#endif

    /* delay - to read the message */

    (void) sleep(delay);

    /* quit the program */

    exit(retval);
}

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

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

static void parse_args(int argc, const char **argv)
{
    int is_option = 1;      /*
			     * this is to allow "--" to terminate options
			     * -it also allows login names to begin with
			     * "-" - which seems strange.
			     */
    while (--argc > 0) {
	if ( (*++argv)[0] == '-' && is_option ) {
	    switch ((*argv)[1]) {
	    case '-':
		is_option = 0;                   /* last option entered */
		break;
	    case 'p':
		login_flags |= LOGIN_KEEP_ENV;
		break;
	    case 'h':
		login_flags |= LOGIN_HAVE_RHOST;
		{
		    const char *hn;

		    hn = 2+*argv;
		    if (hn[0] || (--argc > 0 && (hn = *++argv))) {
			login_remote_host = x_strdup(hn);
			hn = NULL;
			if (login_remote_host != NULL)
			    break;
		    }
		    hn = NULL;
		}
		login_flags |= LOGIN_GARBLED;
		break;
	    case 'f':
		login_flags |= LOGIN_FORCE_AUTH;
		break;
	    default:
		fprintf(stderr,"unrecognized request: %s ?\n", argv[0]);
		login_flags |= LOGIN_GARBLED;
	    }
	} else {
	    user = *argv;
	}
    }

    if ((login_flags & LOGIN_GARBLED)) {
	exit_program(1, 60, PAM_SUCCESS,
		     "usage:\n"
		     "\tlogin [`username']\t - login [as user]\n"
		     "\t      -p\t\t - do not destroy environment\n"
		     "\t      -h `hostname'\t - from hostname\n"
		     "\t      -f\t\t - pre-authenticated\n"
		     "\t      --\t\t - stop treating arguments as flags\n"
	    );
    }
}

static int login_get_terminal(void)
{
    /* identify terminal */

    terminal_name = ttyname(STDIN_FILENO);
    if (terminal_name == NULL || !terminal_name[0]) { /* no terminal known? */
	return LOGIN_FALSE;
    }

    /* possess terminal */
    {
	if (chown(terminal_name, 0, 0)
	    || chmod(terminal_name, LOGIN_PERMS)) {
	    return LOGIN_FALSE;                  /* couldn't own terminal */
	}
    }

    /* reset terminal */
    {
	struct termios old_terminal;
	struct sigaction old_action, tmp_action;

	/* remember the current terminal settings; set by getty etc.. */
	if (tcgetattr(STDIN_FILENO, &old_terminal) < 0) {
	    return LOGIN_FALSE;
	}

	/*
	 * terminate all the processes (other than this one)
	 * that want to use this terminal...
	 */

	sigemptyset(&tmp_action.sa_mask);
	tmp_action.sa_flags = 0;
	tmp_action.sa_handler = SIG_IGN;       /* ignore signal */
	if (sigaction(SIGHUP, &tmp_action, &old_action) != 0) {
	    return LOGIN_FALSE;                /* trouble with signals */
	}

	/* signal shield up here */
	(void) vhangup();           /* SIGHUP to all users of terminal */
	/* signal shield down again */

	if (sigaction(SIGHUP, &old_action, NULL) != 0) {
	    return LOGIN_FALSE;
	}

	/*
	 * We have the terminal to ourselves now...
	 */

	setsid();

	/* ensure STDIN/OUT/ERR_FILENOs point to the terminal */
	{
	    int fildes;

	    fildes = open(terminal_name, O_RDWR);
	    if (fildes < 0) {
		return LOGIN_FALSE;
	    }

	    /* realign STD.. to terminal (just in case) */
	    if ( dup2(fildes, STDIN_FILENO) < 0
		 || dup2(fildes, STDOUT_FILENO) < 0
		 || dup2(fildes, STDERR_FILENO) < 0 ) {
		return LOGIN_FALSE;
	    }

	    if ( fildes != STDIN_FILENO && fildes != STDOUT_FILENO
		 && fildes != STDERR_FILENO ) {
		close(fildes);
	    }
	}

	/* reset the terminal settings to their previous values.. */

	if ( tcsetattr(STDIN_FILENO, TCSAFLUSH, &old_terminal) < 0 ) {
	    return LOGIN_FALSE;
	}
    }

    /* we have the terminal for our use. */

    return LOGIN_TRUE;
}

static int login_authenticate_user(void)
{
    int delay, retval ,logins;

    /*
     *  This is the main authentication loop.
     */

    for (delay=MIN_DELAY, logins=0; logins++ < MAX_LOGIN; delay *= 2) {

#ifdef HAVE_PAM_FAIL_DELAY
	/* have to pause on failure. At least this long (doubles..) */
	retval = pam_fail_delay(pamh, delay);
	if (retval != PAM_SUCCESS) {
	    D(("Error setting delay; %s", pam_strerror(pamh,retval)));
	    return retval;
	}
#endif /* HAVE_PAM_FAIL_DELAY */

	/* authenticate user */
	if ( login_flags & LOGIN_FORCE_AUTH ) {
	    D(("skipping authentication phase"));
	    retval = PAM_SUCCESS;
	} else {
	    D(("authenticating user"));
	    retval = pam_authenticate(pamh, 0);
	}

	D(("authentication => %s", pam_strerror(pamh,retval)));
	if (retval == PAM_SUCCESS) {

	    /* the user was authenticated - can they log in? */
	    retval = pam_acct_mgmt(pamh, 0);

	    D(("account => %s", pam_strerror(pamh,retval)));
	    if (retval == PAM_SUCCESS || retval == PAM_NEW_AUTHTOK_REQD) {
		return retval;
	    }

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

	/* did the conversation time out? */
	if (pam_misc_conv_died) {
	    D(("conversation timed out"));
	    return PAM_PERM_DENIED;
	}

	/* was that too many failures? */
	if (retval == PAM_MAXTRIES || logins >= MAX_LOGIN) {
	    D(("Tried too many times"));
	    return PAM_MAXTRIES;
	}

	/* what should we do about the failure? */
	switch (retval) {
	case PAM_ABORT:
	case PAM_CRED_INSUFFICIENT:
	case PAM_AUTHINFO_UNAVAIL:
	case PAM_CONV_ERR:
	case PAM_SERVICE_ERR:
	    D(("system failed; %s", pam_strerror(pamh,retval)));
	    return retval;
	default:
	    fprintf(stderr, LOGIN_ATTEMPT_FAILED);
	}

	/* reset the login prompt */
	retval = pam_set_item(pamh, PAM_USER_PROMPT, user_prompt);

	if (retval == PAM_SUCCESS) {
	    retval = pam_set_item(pamh, PAM_USER, NULL);
	}

	if (retval != PAM_SUCCESS) {
	    D(("Internal failure; %s",pam_strerror(pamh,retval)));
	    return retval;
	}
    }

    /* report lack of success */
    return PAM_USER_UNKNOWN;
}

static void login_invoke_shell(const char *shell, uid_t uid)
{
    char * const *shell_env=NULL;
    char * const *shell_args=NULL;
    const char *pw_dir=NULL;
    int retval;
    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.
     */

    pw_dir = pam_getenv(pamh, "HOME");
    if ( !pw_dir || *pw_dir == '\0' || chdir(pw_dir) ) {
	fprintf(stderr, "home directory for %s does not work..", user);
	if (!strcmp(pw_dir,DEFAULT_HOME) || chdir(DEFAULT_HOME) ) {
	    exit_program(1, 0, PAM_ABORT,". %s not available either; exiting\n"
			 , DEFAULT_HOME);
	}
	if (!pw_dir || *pw_dir == '\0') {
	    fprintf(stderr, ". setting to " DEFAULT_HOME "\n");
	    pw_dir = DEFAULT_HOME;
	} else {
	    fprintf(stderr, ". changing to " DEFAULT_HOME "\n");
	}
	if (pam_misc_setenv(pamh, "HOME", pw_dir, 0) != PAM_SUCCESS) {
	    D(("failed to set $HOME"));
	    fprintf(stderr
		    , "Warning: unable to set HOME environment variable\n");
	}
    }

    /*
     * next we attempt to obtain the preferred shell + arglist
     */

    D(("what is their shell?"));
    shell_args = build_shell_args(shell, LOGIN_TRUE, NULL);
    if (shell_args == NULL) {
	exit_program(1,STANDARD_DELAY,PAM_BUF_ERR
		     ,"unable to build shell arguments");
    }

    /*
     * 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 */

    D(("get the environment"));
    shell_env = pam_getenvlist(pamh);
    if (shell_env == NULL) {
	exit_program(1, STANDARD_DELAY, PAM_ABORT
		     , "environment corrupt; sorry..");
    }

    /*
     * close PAM (quietly = this is a forked process so ticket files
     * should *not* be deleted logs should not be written - the parent
     * will take care of this)
     */

    D(("end pam"));
    retval = pam_end(pamh, PAM_SUCCESS
#ifdef PAM_DATA_QUIET                                  /* Linux-PAM only */
		     | PAM_DATA_QUIET
#endif
	);
    pamh = NULL;                                              /* tidy up */
    user = NULL;                            /* user's name not valid now */
    if (retval != PAM_SUCCESS) {
	exit_program(1, STANDARD_DELAY, retval
		     , "login failed to release authenticator");
    }

    /* now set permissions on TTY */
    D(("locating group %s", TERMINAL_GROUP));
    gr = getgrnam(TERMINAL_GROUP);
    if (gr == NULL) {
	exit_program(1, STANDARD_DELAY, PAM_ABORT
		     , "Failed to find `%s' group\n", TERMINAL_GROUP);
    }

    D(("group tty = gid(%d)", gr->gr_gid));
    /* change owner of terminal */
    if (chown(terminal_name, uid, gr->gr_gid)
	    || chmod(terminal_name, TERMINAL_PERMS)) {
	exit_program(1, STANDARD_DELAY, PAM_ABORT
		     , "Failed to change access permission to terminal %s\n"
		     , terminal_name);
    }

#ifdef HAVE_PWDB
    /* close password database */
    while ( pwdb_end() == PWDB_SUCCESS );            /* forget all */
#endif

    /*
     * become user irrevocably
     */

    if (setuid(uid) != 0) {
	fprintf(stderr, "su: cannot assume uid\n");
	exit(1);
    }

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

    D(("exec shell"));
    execve(shell_args[0], shell_args+1, shell_env);

    /* should never get here */

    exit_program(1, STANDARD_DELAY, PAM_ABORT, "login failed exec shell");
}

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

void main(int argc, const char **argv)
{
    static const char *shell=NULL;
    int retval=LOGIN_FALSE, status, child;
    uid_t uid= (uid_t) -1;
    
    /*
     * Parse the arguments to login. There are static variables
     * above that indicate the intentions of the invoking process
     */

    parse_args(argc, argv);

    /*
     * Obtain terminal
     */

    if (getuid() == 0 && geteuid() == 0) {
	retval = login_get_terminal();               /* must be root to try */
    }

    if (retval != LOGIN_TRUE) {
	exit_program(1, 10, PAM_SUCCESS, "unable to attatch to terminal\n");
    }

    /*
     * We have the terminal to ourselves; initialize shared libraries
     */

#ifdef HAVE_PWDB
    retval = pwdb_start();
    if (retval != PWDB_SUCCESS) {
	exit_program(1, 60, PAM_ABORT /* <- should be ignored */
		     , "Problem initializing;\n\t%s\n"
		     , pwdb_strerror(retval));
    }
#endif

    retval = pam_start("login", user, &conv, &pamh);
    if (retval != PAM_SUCCESS) {
	exit_program(1, 60, retval
		     , "Error initializing;\n\t%s\n"
		     , pam_strerror(pamh,retval));
    }
    user = NULL;                   /* reset to avoid possible confusion */

    /*
     * Fill in some blanks: environment and PAM items.
     */

    retval = make_environment(pamh, (login_flags & LOGIN_KEEP_ENV));

    if (retval == PAM_SUCCESS) {
	D(("return = %s", pam_strerror(pamh,retval)));
	D(("login prompt: %s", login_prompt));
	retval = pam_set_item( pamh, PAM_USER_PROMPT
			     , (const void *) login_prompt );
	D(("rhost: %s", login_remote_host));
	(void) pam_set_item(pamh, PAM_RHOST
			      , (const void *) login_remote_host );
	D(("requesting user: %s", login_remote_user));
    	(void) pam_set_item(pamh, PAM_RUSER
			    , (const void *) login_remote_user );
	D(("terminal[%p]: %s", pamh, terminal_name));
	(void) pam_set_item( pamh, PAM_TTY, (const void *) terminal_name );
    }

    if (retval != PAM_SUCCESS) {
	exit_program(1, 60, retval, "Internal failure;\n\t%s\n"
		     , pam_strerror(pamh,retval));
    }

    /*
     * We set up the conversation timeout facilities.
     */

    set_timeout(LOGIN_TRUE);

    /*
     * Proceed to authenticate the user.
     */

    retval = login_authenticate_user();
    switch (retval) {
    case PAM_SUCCESS:
    case PAM_NEW_AUTHTOK_REQD:  /* user is user, just need a new password */
	break;
    case PAM_MAXTRIES:
	exit_program(1, SLEEP_AFTER_MAX_LOGIN, retval
		     , "Login failed - too many bad attempts\n");
    case PAM_CONV_ERR:
    case PAM_SERVICE_ERR:
	exit_program(1, SLEEP_AFTER_MAX_LOGIN, retval, "Login failed\n");
    default:
	D(("Login failed; %s", pam_strerror(pamh,retval)));
	exit_program(1, STANDARD_DELAY, retval, "Login failed\n");
    }

    /*
     * Do we need to prompt the user for a new password?
     */

    if (retval == PAM_NEW_AUTHTOK_REQD) {
	set_timeout(LOGIN_TRUE);
	retval = pam_chauthtok(pamh, PAM_CHANGE_EXPIRED_AUTHTOK);

	/* test for specific errors */
	switch (retval) {
	case PAM_AUTHTOK_LOCK_BUSY:
	case PAM_TRY_AGAIN:
	    D(("chauthtok: %s", pam_strerror(pamh,retval)));
	    retval = PAM_SUCCESS;
	    fprintf(stderr
		    , "login: please update your authentication token(s)\n");
	}
    }

    /*
     * How are we doing? Didn't need or successfully updated password?
     */

    if (retval != PAM_SUCCESS) {
	exit_program(1, STANDARD_DELAY, retval, "Login failure;\n\t%s\n"
		     , pam_strerror(pamh,retval));
    }

    set_timeout(LOGIN_FALSE);           /* from here we don't need timeouts */

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

    D(("open session"));
    retval = pam_open_session(pamh, 0);
    if (retval != PAM_SUCCESS) {
	exit_program(1, STANDARD_DELAY, retval
		     , "Error opening session;\n\t%s\n"
		     , pam_strerror(pamh,retval));
    }

    /*
     * Set the user's credentials
     */

    D(("establish user credentials"));
    retval = set_user_credentials(pamh, LOGIN_TRUE, &user, &uid, &shell);
    if (retval != PAM_SUCCESS) {
	D(("failure; %s", pam_strerror(pamh,retval)));
	(void) pam_close_session(pamh, retval);
	exit_program(1, STANDARD_DELAY, retval, "Credential setting failure");
    }

    /*
     * 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-)PAM.
     */

    child = fork();
    if (child == 0) {
	/*
	 *  Process is child here.
	 */
	D(("started child"));
	login_invoke_shell(shell, uid);                    /* never returns */

	D(("this should not have returned"));
	serious_abort("shell failed to execute");
    }
    
    utmp_open_session(pamh, child);

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

    status = wait_for_child(child, -1);
    if (status != 0) {
	D(("shell returned %d", status));
    }

    utmp_close_session(pamh, child);

    /* Delete the user's credentials. */
    retval = pam_setcred(pamh, PAM_DELETE_CRED);
    if (retval != PAM_SUCCESS) {
	fprintf(stderr, "WARNING: could not delete credentials\n\t%s\n"
		, pam_strerror(pamh,retval));
    }

    /* close down */
    (void) pam_close_session(pamh,0);

    (void) pam_end(pamh,PAM_SUCCESS);
    pamh = NULL;

    /* exit - leave getty to deal with resetting the terminal */

    exit_program(0, GOODBYE_DELAY, PAM_SUCCESS, GOODBYE_MESSAGE);
}
