/* 
 * $Id$
 *
 * checkpam.c
 *
 * ( based on an implementation of `su' by
 *
 *     Peter Orbaek  <poe@daimi.aau.dk>
 *
 * obtained from ftp://ftp.daimi.aau.dk/pub/linux/poe/
 *
 * Rewritten for Linux-PAM by Andrew Morgan <morgan@parc.power.net>
 *
 * Hacked to cooperate with pam_checkpassword.so to implement the
 *  checkpassword protocol by Tim Baverstock <warwick@sable.demon.co.uk>,
 *  who spent two days trying to do the programming equivalent of
 *  fitting the teapot into the kettle, to make tea. :] )
 *
 * Licensed under the GPL. <www.gnu.org>
 *
 * $Log$
 */

#define ROOT_UID                  0
#define ROOT_GID                  0
#define DEFAULT_HOME              "/"
#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 <termios.h>
#include <stdio.h>
#include <sys/types.h>

#include <unistd.h>

#include <pwd.h>
#define __USE_BSD
#include <grp.h>

#include <sys/file.h>
#include <string.h>
#include <stdarg.h>

#include <security/pam_appl.h>
#include <security/pam_misc.h>

#ifdef HAVE_PWDB
#include <pwdb/pwdb_public.h>
#endif

#include "../inc/make_env.-c"
#include "../inc/setcred.-c"
#include "../inc/wait4shell.-c"
#include "../inc/wtmp.-c"

#include "checkpam.h"

/* I can't believe they never put these into ANSI C. */

#ifndef TRUE
#define TRUE 1
#define FALSE 0
#endif

#if 0
#define D(func) {func;}
#else
#define D(func) {;}
#endif

#define KEEP_ORIGINAL_ENVIRONMENT FALSE

/* Some of this information's lost because pam_chpw_auth.so reads fd3,
 * and BADUSE seems to be optional anyway.
 */

enum {
  CHKPW_INVALIDPW	= 1,
  CHKPW_BADUSE	= 2, 
  CHKPW_TMPERR	= 111
};

/* ------ some local (static) functions ------- */

#define PAM(retval,mess) if (retval != PAM_SUCCESS) { exit_now(1, mess); }
#if 0
#define breakP(mess) { fprintf(stderr, "checkpam: " mess "\n"); break; }
#else
#define breakP(mess) { break; }
#endif

static void usage(void)
{
    fprintf(stderr,
			"usage: pam_checkpassword program arg arg ... (but see docs)\n"
			"requires root(0), wheel(0), and pam_checkpassword_auth.so.\n");
    exit(CHKPW_BADUSE);
}

static pam_handle_t *pamh=NULL;

static void exit_now(int exit_code, const char *format, ...)
{
    va_list args;

    va_start(args,format);
    vfprintf(stderr, format, args);
    va_end(args);

    if (pamh != NULL)
	pam_end(pamh, exit_code ? PAM_ABORT:PAM_SUCCESS);

#ifdef HAVE_PWDB
    while (pwdb_end() == PWDB_SUCCESS);                       /* clean up */
#endif /* HAVE_PWDB */

	/*
	 *   Actually, everything is `invalid' - this program shouldn't really exit
     */

    exit(exit_code?CHKPW_INVALIDPW:CHKPW_TMPERR);
}

#ifdef HAVE_PWDB
static void end_now(int retval, const char *mess)
{
    if (retval != PWDB_SUCCESS)
	exit_now(1, "checkpam: pwdb failed (%s)\n", mess);
}
#endif

static void checkpam_exec_secondary(uid_t uid, char * const *argv, const char *user)
{
    char ** secondary_env;
    const char *envvar;
    int retval;

    /*
     * Now, we obtain other information about the user
     */

	/*
	 *  Set argv[1]=="Maildir" to ".", if $MAILDIR exists and =="."
	 */

	if ( argv[1] && !strcmp(argv[1],"Maildir") ) {
	    envvar = pam_getenv(pamh, "MAILDIR");
		if ( envvar && !strcmp(envvar,".") )
			strcpy(argv[1],envvar);
	}
	
    envvar = pam_getenv(pamh, "HOME");
    if ( !envvar || envvar[0] == '\0' || (chdir(envvar)) ) {
	  fprintf(stderr, "home directory for %s does not work..", user);
	  if ( !strcmp(envvar,DEFAULT_HOME) || chdir(DEFAULT_HOME) ) {
	    exit_now(1, ". %s not available either; exiting\n"
				 , DEFAULT_HOME);
	  }
	  if (!envvar || envvar[0] == '\0') {
	    fprintf(stderr, ". setting to " DEFAULT_HOME "\n");
	    envvar = DEFAULT_HOME;
	  } else {
	    fprintf(stderr, ". changing to " DEFAULT_HOME "\n");
	  }
	  PAM( pam_misc_setenv(pamh, "HOME", DEFAULT_HOME, 0),
		   "checkpam: could not set HOME\n");
    }
		
    /*
     * and now copy the environment for non-PAM use
     */

    secondary_env = pam_misc_copy_env(pamh);
    if (secondary_env == NULL) {
	  exit_now(1, "checkpam: corrupt environment\n");
    }

    /*
     * 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(("pam_end"));
    retval = pam_end(pamh, PAM_SUCCESS
#ifdef PAM_DATA_QUIET
		     | PAM_DATA_QUIET
#endif
	);
    pamh = NULL;
    user = NULL;                            /* user's name not valid now */

    PAM(retval, "checkpam: failed to release authenticator\n");

#ifdef HAVE_PWDB
    while ( pwdb_end() == PWDB_SUCCESS );            /* forget all */
#endif

    if (setuid(uid) != 0) {
	  exit_now(1, "checkpam: cannot assume uid\n");
    }

	/* Not sure if this will quite do what I want, but it should. :) */

#if 0
	{
	  const char **p=secondary_env;
	  while ( p && *p )
		fprintf(stderr,"%s\n",*(p++));
	}
#endif

	{
#if 0
	  environ=secondary_env;
	  execvp(argv[0], argv);  /* checkpassword requires execvp() */
#else
	  execve(argv[0], argv, secondary_env);
#endif
	}
    exit_now(1, "checkpam: exec failed\n");
}

/* ------ some static data objects ------- */

static struct pam_conv conv = {
    checkpam_conv,                   /* defined in checkpam_conv.c */
    NULL
};

/* ------- the application itself -------- */

void main(int argc, char * const argv[])
{
    const char *user=NULL;
    const char *secondary=NULL;
    const char *service=NULL;
	int retval, status;
    pid_t child;
    uid_t uid;

      /* ----------- Run only as non-suid root ---------- */
	
	if ( ( getuid() != geteuid() || getuid()!=ROOT_UID ) ||
		 ( getgid() != getegid() || getgid()!=ROOT_GID ) )
	  usage();

      /* ------------ parse the argument list ----------- */

	if ( argc == 0 ) usage();

	secondary = *++argv;
	if ( ( service = rindex ( secondary, '/' ) ) )
	  ++service;
	else
	  service=secondary;

#ifdef HAVE_PWDB
	retval = pwdb_start();
	end_now(retval, "start");
#endif

   
    /* ------ initialize the Linux-PAM interface ------ */

	/*
	 * Note:
	 *   We haven't a clue who the user is (pam_checkpassword_auth.so's task).
	 *   The service name is derived from the argument list in flat
	 *     contravention of the guidelines, hence the no suid etc policy.
	 */

	D(fprintf(stderr,"pamstart %s:%s\n",service,user));

    retval = pam_start(service, user, &conv, &pamh);

	PAM(retval,"checkpam: failed to start pam\n");

    /*
     * Fill in some blanks
     */

    retval = make_environment(pamh, KEEP_ORIGINAL_ENVIRONMENT);

	PAM(retval,"checkpam: failed to make environment\n");

#if 0
    if (retval == PAM_SUCCESS) {
	  const char *terminal = ttyname(STDIN_FILENO);
	  if (terminal) {
		retval = pam_set_item(pamh, PAM_TTY, (const void *)terminal);
		PAM(retval,"checkpam: failed to set PAM_TTY\n");
	  } else {
	    retval = PAM_PERM_DENIED;    /* how did we get here? */
		PAM(retval,"checkpam: failed to get PAM_TTY\n");
	  }
	  terminal = NULL;
    }
#endif

	/* ----- Is RUSER appropriate? ----- */

	/*
	 *  RUSER should be the authenticated initiator of the session, but
	 *   this is going to run over TCP/IP.
     */

    if (retval == PAM_SUCCESS) {
	  const char *ruser = cuserid ( NULL );  /* Was: getlogin() */
	  if (ruser) {
	    retval = pam_set_item(pamh, PAM_RUSER, (const void *)ruser);
	  } else {
	    retval = PAM_PERM_DENIED;    /* must be known to system */
	  }
	  ruser = NULL;
    }

	PAM(retval,"checkpam: failed to set PAM_RUSER\n");

    if (retval == PAM_SUCCESS) {
	  retval = pam_set_item(pamh, PAM_RHOST, (const void *)"localhost");
    }

	PAM(retval,"checkpam: failed to set PAM_RHOST\n");

    if (retval != PAM_SUCCESS) {
	  exit_now(1, "checkpam: problem establishing environment\n");
    }

    /*
     * Note. We know nothing about the user. We will get
     * this info after the username and password have been read
	 * from file descriptor 3 by pam_checkpassword_auth.so, and
	 * checked by whatever's stacked after it..
     */

#ifdef HAVE_PAM_FAIL_DELAY
    /* have to pause on failure. At least this long (doubles..) */
    retval = pam_fail_delay(pamh, SU_FAIL_DELAY);
    if (retval != PAM_SUCCESS) {
	  exit_now(1, "checkpam: problem initializing failure delay\n");
    }
#endif /* HAVE_PAM_FAIL_DELAY */

    while (retval == PAM_SUCCESS) {    /* abuse loop to avoid using goto... */

	  retval = pam_authenticate(pamh, 0);	   /* authenticate the user */
	  if (retval != PAM_SUCCESS)
	    breakP("auth");
	  
	  /*
	   * The user is valid, but should they have access at this
	   * time?
	   */
	  
	  retval = pam_acct_mgmt(pamh, 0);
	  if (retval != PAM_SUCCESS)
		breakP("acmg");

	  /* open the su-session */

	  retval = pam_open_session(pamh,0);
	  if (retval != PAM_SUCCESS)
	    breakP("opse");

	  /*
	   * We obtain all of the new credentials of the user.
	   */
	  
	  {
		/* The `TRUE' here means `set LOGNAME', but perh. should be FALSE */
		const char *shell;
		retval = set_user_credentials(pamh, TRUE, &user, &uid, &shell);
		/* We're not interested in the shell, but suc() doesn't check
		 *  for NULL parameters, and we're allergic to SEGVs. 
		 */
	  }
	  if (retval != PAM_SUCCESS)
	    breakP("ucre");

	  /* this is where we execute the secondary secondary */

	  child = fork();
	  if (child == 0) {       /* child execs secondary with argv */
	    checkpam_exec_secondary(uid, argv, user);
	    exit_now(1, "checkpam: secondary failed to execute!?\n");
	  }

	  if ( child<0 )
		breakP("Childerr");

	  /* wait for child to terminate */

	  status = wait_for_child(child);
	  if (status != 0) {
	    D(("shell returned %d", status));
	  }

	  /* Delete the user's credentials. */
	  retval = pam_setcred(pamh, PAM_CRED_DELETE);
	  if (retval != PAM_SUCCESS) {
	    fprintf(stderr, "WARNING: could not delete credentials\n\t%s\n"
				, pam_strerror(retval));
	  }

	  /* close down */
	  retval = pam_close_session(pamh,0);
	  if (retval != PAM_SUCCESS)
	    break;
	  
	  /* clean up */
	  
	  (void) pam_end(pamh,PAM_SUCCESS);
	  pamh = NULL;
	  
#ifdef HAVE_PWDB
	  while ( pwdb_end() == PWDB_SUCCESS );
#endif
	  
	  exit(status);                /* transparent exit */
    }
	
    /*
     * reaching here means that an error has occured; mention any
     * error and then exit
     */
	
    if (pamh != NULL)
	  (void) pam_end(pamh,retval);
	
    if (retval != PAM_SUCCESS)
	  fprintf(stderr, "checkpam: %s\n", pam_strerror(retval));
	
#ifdef HAVE_PWDB
    while (pwdb_end() == PWDB_SUCCESS);
#endif

    exit(1);
}

