/*
 * $Id: login.c,v 1.17 1999/03/28 08:17:11 saw Rel $
 */

/*
 * 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@linux.kernel.org>
 */

static const char rcsid[] =
"$Id: login.c,v 1.17 1999/03/28 08:17:11 saw Rel $\n"
" - Login application. <saw@msu.ru>"
;

#define _BSD_SOURCE
#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>
/* should be in above(?): */ extern int vhangup(void);

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

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

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

#include "../../common/include/shell_args.h"
#include "../../common/include/wait4shell.h"
#include "../../common/include/login_indep.h"
#include "../include/make_env.h"
#include "../include/setcred.h"
#include "../include/wtmp-gate.h"

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


#define LOGIN_STATE_ARGS_PARSED		2
#define LOGIN_STATE_TERMINAL_OBTAINED	3
#define LOGIN_STATE_PWDB_INITIALIZED	4
#define LOGIN_STATE_PAM_INITIALIZED	5
#define LOGIN_STATE_ENV_INITIALIZED	6
#define LOGIN_STATE_AUTHEN_SUCCESS	7
#define LOGIN_STATE_SESSION_OPENED	8
#define LOGIN_STATE_CREDENTIALS_SET	9
#define LOGIN_STATE_UTMP_OPENED		10

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

/* ------ 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 char const *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;
    }
}

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

static void serious_abort(const char *s)
{
    if (pamh != NULL)
        (void) pam_end(pamh, PAM_ABORT);
#ifdef HAVE_PWDB
    while (pwdb_end() == PWDB_SUCCESS);
#endif

    (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-)PAM -- 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 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:
	    (void) 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, delay = 0, pam_retval = PAM_ABORT;
    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 = pam_getenv(pamh, "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");
	    }
	    if (pam_misc_setenv(pamh, "HOME", pw_dir, 0) != PAM_SUCCESS) {
		D(("failed to set $HOME"));
		(void) 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) 
	{
	    delay = STANDARD_DELAY;
	    pam_retval = PAM_BUF_ERR;
	    (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 */

	D(("get the environment"));
	shell_env = pam_getenvlist(pamh);
	if (shell_env == NULL) 
	{
	    delay = STANDARD_DELAY;
	    (void) fprintf (stderr, "environment corrupt; sorry..");
	    break;
	}

	/*
	 * 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) 
	{
	    delay =STANDARD_DELAY;
	    pam_retval = retval;
	    (void) fprintf (stderr, "login failed to release authenticator");
	    break;
	}

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

	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)) 
	{
	    delay = STANDARD_DELAY;
	    (void) fprintf (stderr, "Failed to change access permission	to terminal %s\n", terminal_name);
	    break;
	}

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

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

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

	/* should never get here */

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

#ifdef HAVE_PWDB
    while (pwdb_end() == PWDB_SUCCESS);
#endif
    (void) sleep(delay);
    exit (1);
}

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

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

    parse_args(argc, argv, &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;
	    pam_retval = PAM_SUCCESS;
	    (void) fprintf (stderr, "unable to attach to terminal\n");
	    err_descr = "terminal error";
	    break;
	}
	state = LOGIN_STATE_TERMINAL_OBTAINED;

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

#ifdef HAVE_PWDB
	place = "pwdb_start()";
	retval = pwdb_start();
	if (retval != PWDB_SUCCESS) 
	{
	    delay = 60;
            pam_retval = PAM_ABORT;
            (void) fprintf (stderr, "Problem initializing;\n\t%s\n", 
							pwdb_strerror(retval));
	    err_descr = "start error";
            break;
	}
	state = LOGIN_STATE_PWDB_INITIALIZED;
#endif

	place = "pam_start";
	retval = pam_start("login", user, &conv, &pamh);
	if (retval != PAM_SUCCESS) 
	{
	    delay = 60;
	    pam_retval = retval;
	    (void) fprintf (stderr, "Error initializing;\n\t%s\n", 
						pam_strerror(pamh,retval));
	    err_descr = "start error";
	    break;
	}
	state = LOGIN_STATE_PAM_INITIALIZED;

	user = NULL;                   /* reset to avoid possible confusion */

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

	place = "make_environment";
	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) 
	{
	    delay = 60;
	    pam_retval = retval;
	    (void) fprintf (stderr, "Internal failure;\n\t%s\n", 
						pam_strerror(pamh,retval));
	    err_descr = "environment setting error";
	    break;
	}
	state = LOGIN_STATE_ENV_INITIALIZED;

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

	set_timeout(LOGIN_TRUE);

	/*
	 * Proceed to authenticate the user.
	 */

	place = "login_authenticate_user";
	retval = login_authenticate_user();
	delay = SLEEP_AFTER_MAX_LOGIN;
	pam_retval = retval;
	switch (retval) 
	{
	    case PAM_SUCCESS:
	    case PAM_NEW_AUTHTOK_REQD:/* user just needs a new password */
		break;
	    case PAM_MAXTRIES:
	    {
		err_descr = "Login failed - too many bad attempts\n";
		break;
	    }
	    case PAM_CONV_ERR:
	    case PAM_SERVICE_ERR:
	    {
		err_descr = "Login failed\n";
		break;
	    }
	    default:
	    {
		D(("Login failed; %s", pam_strerror(pamh,retval)));
		delay = STANDARD_DELAY;
		err_descr = "Login failed\n";
		break;
	    }
	}
	if (err_descr != NULL)
	    break;

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

	place = "pam_chauthtok";
	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;
		(void) 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) 
	{
	    delay = STANDARD_DELAY;
	    pam_retval = retval;
	    (void) fprintf (stderr, "Login failure;\n\t%s\n", 
						pam_strerror(pamh,retval));
	    err_descr = "authentication failure";
	    break;
	}
	state = LOGIN_STATE_AUTHEN_SUCCESS;

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

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

	place = "pam_open_session";
	D(("open session"));
	retval = pam_open_session(pamh, 0);
	if (retval != PAM_SUCCESS) 
	{
	    delay = STANDARD_DELAY;
	    pam_retval = retval;
	    (void) fprintf (stderr, "Error opening session;\n\t%s\n", 
						pam_strerror(pamh,retval));
	    err_descr = "unable to open session";
	    break;
	}
	state = LOGIN_STATE_SESSION_OPENED;

	/*
	 * Set the user's credentials
	 */

	place = "set_user_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)));
	    delay = STANDARD_DELAY;
	    pam_retval = retval;
	    err_descr = "Credential setting failure";
	    break;
	}
	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-)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");
	}

	retval = utmp_open_session(pamh, getpid(), &place, &err_descr);
	if (retval < 0) 
	{
	    delay = 60;
	    pam_retval = PAM_ABORT;
	    printf ("login: %s: %s\n", place, err_descr);
	    err_descr = "error opening utmp session";
	    break;
	}
	else if (retval > 0) 
	{
	    (void) fprintf (stderr, "login: %s: %s\n", place, err_descr);
	    err_descr = NULL;
	}
	state = LOGIN_STATE_UTMP_OPENED;
	/*
	 * Process is parent here... wait for the child to exit
	 */

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

    /*Cleaning up*/
    do
    {
	if (state >= LOGIN_STATE_UTMP_OPENED)
	{
	    retval = utmp_close_session(pamh, &place, &err_descr);
	    if (retval < 0) 
	    {
		delay = 60;
		pam_retval = PAM_ABORT;
		(void) fprintf (stderr, "login: %s: %s\n", place, err_descr);
		err_descr = "error closing utmp session";
		break;
	    }
	    else if (retval > 0)
	    { 
		(void) fprintf(stderr, "login: %s: %s\n", place, err_descr);
		err_descr = NULL;
	    }
	}
	
	if (state >= LOGIN_STATE_CREDENTIALS_SET)
	/* Delete the user's credentials. */
	{
	    retval = pam_setcred(pamh, PAM_DELETE_CRED);
	    if (retval != PAM_SUCCESS) 
		(void) fprintf(stderr, "WARNING: could not delete credentials\n\t%s\n", pam_strerror(pamh,retval));
	}

	if (state >= LOGIN_STATE_SESSION_OPENED)
	/* close down */
	    (void) pam_close_session(pamh,0);

	if (state >= LOGIN_STATE_PAM_INITIALIZED)
	{
	    (void) pam_end(pamh,PAM_SUCCESS);
	    pamh = NULL;
	}
	retcode = 0;
	delay = GOODBYE_DELAY;
	pam_retval = PAM_SUCCESS;
    }while (0);

    /* exit - leave getty to deal with resetting the terminal */
    if (pamh != NULL)
        (void) pam_end(pamh, pam_retval);

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

    /* delay - to read the message */

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

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

    exit (retcode);
}
