/*
 * Copyright (c) 2006, 2008 Thorsten Kukuk <kukuk@thkukuk.de>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, and the entire permission notice in its entirety,
 *    including the disclaimer of warranties.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote
 *    products derived from this software without specific prior
 *    written permission.
 *
 * ALTERNATIVELY, this product may be distributed under the terms of
 * the GNU Public License, in which case the provisions of the GPL are
 * required INSTEAD OF the above restrictions.  (This clause is
 * necessary due to a potential bad interaction between the GPL and
 * the restrictions contained in a BSD-style copyright.)
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#if defined(HAVE_CONFIG_H)
#include "config.h"
#endif

#include <time.h>
#include <errno.h>
#include <fcntl.h>
#include <glob.h>
#include <stdio.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/types.h>


#define PAM_SM_AUTH
#define PAM_SM_ACCOUNT
#define PAM_SM_SESSION
#define PAM_SM_PASSWORD

#include <security/pam_modules.h>
#include <security/pam_modutil.h>
#include <security/pam_ext.h>
#include <security/_pam_macros.h>

#define ENV_ITEM(n) { (n), #n }
static struct {
  int item;
  const char *name;
} env_items[] = {
  ENV_ITEM(PAM_SERVICE),
  ENV_ITEM(PAM_USER),
  ENV_ITEM(PAM_TTY),
  ENV_ITEM(PAM_RHOST),
  ENV_ITEM(PAM_RUSER),
};

/* move_fd_to_non_stdio copies the given file descriptor to something other
 * than stdin, stdout, or stderr.  Assumes that the caller will close all
 * unwanted fds after calling. */
static int
move_fd_to_non_stdio (pam_handle_t *pamh, int fd)
{
  while (fd < 3)
    {
      fd = dup(fd);
      if (fd == -1)
	{
	  int err = errno;
	  pam_syslog (pamh, LOG_ERR, "dup failed: %m");
	  _exit (err);
	}
    }
  return fd;
}

static char *
get_krb5cc_path_from_environment (pam_handle_t *pamh)
{
  const char *krb5cc_envvalue;

  if ((krb5cc_envvalue = pam_getenv(pamh, "KRB5CCNAME")) == NULL)
    return NULL;

  if (strncmp(krb5cc_envvalue, "FILE:", sizeof("FILE:")-1) == 0)
    return strdup(&krb5cc_envvalue[ sizeof("FILE:")-1 ]);

  return NULL;
}

static int
lookup_existing_krb5cc_path(pam_handle_t *pamh, char **krb5cc_path,
    const char *pam_user)
{
  struct passwd pwd;
  struct passwd *result;
  struct stat statbuf;
  glob_t pglob;
  char *buf, *krb5cc_glob, *new_krb5cc_path;
  size_t bufsize, i;
  int r, ret, s;

  buf = NULL;
  krb5cc_glob = NULL;
  ret = -1;

  bufsize = sysconf(_SC_GETPW_R_SIZE_MAX);
  if (bufsize == -1)
    bufsize = 16384;

  if ((buf = malloc(bufsize)) == NULL) {
    pam_syslog (pamh, LOG_WARNING, "malloc() error");
    goto finish;
  }

  getpwnam_r(pam_user, &pwd, buf, bufsize, &result);
  if (result == NULL) {
    if (s == 0)
      goto finish;
    pam_syslog (pamh, LOG_WARNING, "getpwnam_r() error");
  }

  if (asprintf(&krb5cc_glob, "/tmp/krb5cc_%s_*", pam_user) < 0) {
    pam_syslog (pamh, LOG_WARNING, "asprintf() error");
    goto finish;
  }

  r = glob(krb5cc_glob, 0, NULL, &pglob);
  if (r == GLOB_NOMATCH) {
    goto finish;
  } else if (r != 0) {
    pam_syslog (pamh, LOG_WARNING, "glob() error");
    goto finish;
  }

  for (i = 0; i < pglob.gl_pathc; i++) {
    if ((new_krb5cc_path = strdup(pglob.gl_pathv[i])) == NULL) {
      pam_syslog (pamh, LOG_WARNING, "strdup() error");
      continue;
    }

    if (stat(new_krb5cc_path, &statbuf) == -1) {
      pam_syslog (pamh, LOG_WARNING, "stat() error when checking %s",
        new_krb5cc_path);
      continue;
    }

    if (statbuf.st_uid != pwd.pw_uid)
      continue;

    *krb5cc_path = new_krb5cc_path;
    ret = 0;
    break;
  }

finish:
  if (buf)
    free(buf);
  if (krb5cc_glob)
    free(krb5cc_glob);

  return ret;
}

static int
call_exec (const char *pam_type, pam_handle_t *pamh,
	   int argc, const char **argv)
{
  int debug = 0;
  int call_setuid = 0;
  int quiet = 0;
  int expose_authtok = 0;
  int exitcode_to_pam = 0;
  int set_krb5ccname = 0;
  int use_stdout = 0;
  int optargc;
  int retval;
  const char *logfile = NULL;
  const char *authtok = NULL;
  char *krb5cc_envstr = NULL;
  char *krb5cc_path = NULL;
  pid_t pid;
  int fds[2];
  int stdout_fds[2];
  FILE *stdout_file = NULL;
  int child_exitcode = -1;

  if (argc < 1) {
    pam_syslog (pamh, LOG_ERR,
		"This module needs at least one argument");
    return PAM_SERVICE_ERR;
  }

  for (optargc = 0; optargc < argc; optargc++)
    {
      if (argv[optargc][0] == '/') /* paths starts with / */
	break;

      if (strcasecmp (argv[optargc], "debug") == 0)
	debug = 1;
      else if (strcasecmp (argv[optargc], "stdout") == 0)
	use_stdout = 1;
      else if (strncasecmp (argv[optargc], "log=", 4) == 0)
	logfile = &argv[optargc][4];
      else if (strncasecmp (argv[optargc], "type=", 5) == 0)
	{
	  if (strcmp (pam_type, &argv[optargc][5]) != 0)
	    return PAM_IGNORE;
	}
      else if (strcasecmp (argv[optargc], "seteuid") == 0)
	call_setuid = 1;
      else if (strcasecmp (argv[optargc], "quiet") == 0)
	quiet = 1;
      else if (strcasecmp (argv[optargc], "expose_authtok") == 0)
	expose_authtok = 1;
      else if (strcasecmp (argv[optargc], "exitcode_to_pam") == 0)
	exitcode_to_pam = 1;
      else if (strcasecmp (argv[optargc], "set_krb5ccname") == 0)
	set_krb5ccname = 1;
      else
	break; /* Unknown option, assume program to execute. */
    }

  if (set_krb5ccname) {
    const char *pam_user;
    int tmp_fd;

    retval = pam_get_item (pamh, PAM_USER, (const void **) &pam_user);
    if (retval != PAM_SUCCESS)
      {
	if (!quiet) {
	  pam_error (pamh, _("%s failed: pam_get_item PAM_USER failed"),
	    argv[optargc]);
	}
	return PAM_SYSTEM_ERR;
      }

    if (lookup_existing_krb5cc_path(pamh, &krb5cc_path, pam_user) != 0) {
      if (asprintf(&krb5cc_path, "/tmp/krb5cc_%s_XXXXXX", pam_user) < 0) {
        if (!quiet) {
          pam_error (pamh, "%s failed: could not create KRB5CCNAME path",
            argv[optargc]);
        }
        return PAM_SYSTEM_ERR;
      }

      if ((tmp_fd = mkstemp(krb5cc_path)) < 0) {
        if (!quiet) {
          pam_error (pamh, "%s failed: could not setup KRB5CCNAME temporary file",
            argv[optargc]);
        }
        free(krb5cc_path);
        return PAM_SYSTEM_ERR;
      }
      (void) close(tmp_fd);
    }

    if (asprintf(&krb5cc_envstr, "KRB5CCNAME=FILE:%s", krb5cc_path) < 0) {
      pam_error (pamh, "%s failed: could not setup KRB5CCNAME", argv[optargc]);
      retval = PAM_SYSTEM_ERR;
      goto finish;
    }

    if (pam_putenv(pamh, krb5cc_envstr) != PAM_SUCCESS) {
      if (!quiet) {
	pam_error (pamh, _("%s failed: failed to set KRB5CCNAME"),
		   argv[optargc]);
      }
      free(krb5cc_envstr);
      retval = PAM_SYSTEM_ERR;
      goto finish;
    }
  } else {
    /* Get the krb5cc_path from environment in case we need to unlink() it. */
    krb5cc_path = get_krb5cc_path_from_environment(pamh);
  }

  if (expose_authtok == 1)
    {
      if (strcmp (pam_type, "auth") != 0)
	{
	  pam_syslog (pamh, LOG_ERR,
		      "expose_authtok not supported for type %s", pam_type);
	  expose_authtok = 0;
	}
      else
	{
	  const void *void_pass;

	  retval = pam_get_item (pamh, PAM_AUTHTOK, &void_pass);
	  if (retval != PAM_SUCCESS)
	    {
	      if (debug)
		pam_syslog (pamh, LOG_DEBUG,
			    "pam_get_item (PAM_AUTHTOK) failed, return %d",
			    retval);
	      goto finish;
	    }
	  else if (void_pass == NULL)
	    {
	      char *resp = NULL;

	      retval = pam_prompt (pamh, PAM_PROMPT_ECHO_OFF,
				   &resp, _("Password: "));

	      if (retval != PAM_SUCCESS)
		{
		  _pam_drop (resp);
		  if (retval == PAM_CONV_AGAIN)
		    retval = PAM_INCOMPLETE;
		  goto finish;
		}

	      pam_set_item (pamh, PAM_AUTHTOK, resp);
	      authtok = strdupa (resp);
	      _pam_drop (resp);
	    }
	  else
	    authtok = void_pass;

	  if (pipe(fds) != 0)
	    {
	      pam_syslog (pamh, LOG_ERR, "Could not create pipe: %m");
	      retval = PAM_SYSTEM_ERR;
	      goto finish;
	    }
	}
    }

  if (use_stdout)
    {
      if (pipe(stdout_fds) != 0)
	{
	  pam_syslog (pamh, LOG_ERR, "Could not create pipe: %m");
	  retval = PAM_SYSTEM_ERR;
	  goto finish;
	}
      stdout_file = fdopen(stdout_fds[0], "r");
      if (!stdout_file)
	{
	  pam_syslog (pamh, LOG_ERR, "Could not fdopen pipe: %m");
	  retval = PAM_SYSTEM_ERR;
	  goto finish;
	}
    }

  if (optargc >= argc) {
    pam_syslog (pamh, LOG_ERR, "No path given as argument");
    retval = PAM_SERVICE_ERR;
    goto finish;
  }

  pid = fork();
  if (pid == -1) {
    retval = PAM_SYSTEM_ERR;
    goto finish;
  }
  if (pid > 0) /* parent */
    {
      int status = 0;
      pid_t retval_pid;

      if (set_krb5ccname)
	free(krb5cc_envstr);

      if (expose_authtok) /* send the password to the child */
	{
	  if (authtok != NULL)
	    {            /* send the password to the child */
	      if (debug)
		pam_syslog (pamh, LOG_DEBUG, "send password to child");
	      if (write(fds[1], authtok, strlen(authtok)+1) == -1)
		pam_syslog (pamh, LOG_ERR,
			    "sending password to child failed: %m");
	      authtok = NULL;
	    }
	  else
	    {
	      if (write(fds[1], "", 1) == -1)   /* blank password */
		pam_syslog (pamh, LOG_ERR,
			    "sending password to child failed: %m");
	    }
        close(fds[0]);       /* close here to avoid possible SIGPIPE above */
        close(fds[1]);
	}

      if (use_stdout)
	{
	  char buf[4096];
	  close(stdout_fds[1]);
	  while (fgets(buf, sizeof(buf), stdout_file) != NULL)
	    {
	      size_t len;
	      len = strlen(buf);
	      if (buf[len-1] == '\n')
		buf[len-1] = '\0';
	      pam_info(pamh, "%s", buf);
	    }
	  fclose(stdout_file);
	}

      while ((retval_pid = waitpid (pid, &status, 0)) == -1 &&
	     errno == EINTR);
      if (retval_pid == (pid_t)-1)
	{
	  pam_syslog (pamh, LOG_ERR, "waitpid returns with -1: %m");
	  retval = PAM_SYSTEM_ERR;
	  goto finish;
	}
      else if (status != 0)
	{
	  if (WIFEXITED(status))
	    {
	      child_exitcode = WEXITSTATUS(status);
	      pam_syslog (pamh, LOG_ERR, "%s failed: exit code %d",
			  argv[optargc], child_exitcode);
		if (!quiet)
	      pam_error (pamh, _("%s failed: exit code %d"),
			 argv[optargc], child_exitcode);
	    }
	  else if (WIFSIGNALED(status))
	    {
	      pam_syslog (pamh, LOG_ERR, "%s failed: caught signal %d%s",
			  argv[optargc], WTERMSIG(status),
			  WCOREDUMP(status) ? " (core dumped)" : "");
		if (!quiet)
	      pam_error (pamh, _("%s failed: caught signal %d%s"),
			 argv[optargc], WTERMSIG(status),
			 WCOREDUMP(status) ? " (core dumped)" : "");
	    }
	  else
	    {
	      pam_syslog (pamh, LOG_ERR, "%s failed: unknown status 0x%x",
			  argv[optargc], status);
		if (!quiet)
	      pam_error (pamh, _("%s failed: unknown status 0x%x"),
			 argv[optargc], status);
	    }

	  if (exitcode_to_pam && child_exitcode >= 0) {
	    retval = child_exitcode;
	    goto finish;
	  }

	  retval = PAM_SYSTEM_ERR;
	  goto finish;
	}
      retval = PAM_SUCCESS;
      goto finish;
    }
  else /* child */
    {
      char **arggv;
      int i;
      char **envlist, **tmp;
      int envlen, nitems;
      char *envstr;

      /* First, move all the pipes off of stdin, stdout, and stderr, to ensure
       * that calls to dup2 won't close them. */

      if (expose_authtok)
	{
	  fds[0] = move_fd_to_non_stdio(pamh, fds[0]);
	  close(fds[1]);
	}

      if (use_stdout)
	{
	  stdout_fds[1] = move_fd_to_non_stdio(pamh, stdout_fds[1]);
	  close(stdout_fds[0]);
	}

      /* Set up stdin. */

      if (expose_authtok)
	{
	  /* reopen stdin as pipe */
	  if (dup2(fds[0], STDIN_FILENO) == -1)
	    {
	      int err = errno;
	      pam_syslog (pamh, LOG_ERR, "dup2 of STDIN failed: %m");
	      _exit (err);
	    }
	}
      else
	{
	  close (STDIN_FILENO);

	  /* New stdin.  */
	  if ((i = open ("/dev/null", O_RDWR)) < 0)
	    {
	      int err = errno;
	      pam_syslog (pamh, LOG_ERR, "open of /dev/null failed: %m");
	      _exit (err);
	    }
	}

      /* Set up stdout. */

      if (use_stdout)
	{
	  if (dup2(stdout_fds[1], STDOUT_FILENO) == -1)
	    {
	      int err = errno;
	      pam_syslog (pamh, LOG_ERR, "dup2 to stdout failed: %m");
	      _exit (err);
	    }
	}
      else if (logfile)
	{
	  time_t tm = time (NULL);
	  char *buffer = NULL;

	  close (STDOUT_FILENO);
	  if ((i = open (logfile, O_CREAT|O_APPEND|O_WRONLY,
			 S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1)
	    {
	      int err = errno;
	      pam_syslog (pamh, LOG_ERR, "open of %s failed: %m",
			  logfile);
	      _exit (err);
	    }
	  if (asprintf (&buffer, "*** %s", ctime (&tm)) > 0)
	    {
	      pam_modutil_write (i, buffer, strlen (buffer));
	      free (buffer);
	    }
	}
      else
	{
	  close (STDOUT_FILENO);
	  if ((i = open ("/dev/null", O_RDWR)) < 0)
	    {
	      int err = errno;
	      pam_syslog (pamh, LOG_ERR, "open of /dev/null failed: %m");
	      _exit (err);
	    }
	}

      if (dup2 (STDOUT_FILENO, STDERR_FILENO) == -1)
	{
	  int err = errno;
	  pam_syslog (pamh, LOG_ERR, "dup2 failed: %m");
	  _exit (err);
	}

      for (i = 3; i < sysconf (_SC_OPEN_MAX); i++)
	close (i);

      if (call_setuid)
	if (setuid (geteuid ()) == -1)
	  {
	    int err = errno;
	    pam_syslog (pamh, LOG_ERR, "setuid(%lu) failed: %m",
			(unsigned long) geteuid ());
	    _exit (err);
	  }

      if (setsid () == -1)
	{
	  int err = errno;
	  pam_syslog (pamh, LOG_ERR, "setsid failed: %m");
	  _exit (err);
	}

      arggv = calloc (argc + 4, sizeof (char *));
      if (arggv == NULL)
	_exit (ENOMEM);

      for (i = 0; i < (argc - optargc); i++)
	arggv[i] = strdup(argv[i+optargc]);
      arggv[i] = NULL;

      /*
       * Set up the child's environment list.  It consists of the PAM
       * environment, plus a few hand-picked PAM items.
       */
      envlist = pam_getenvlist(pamh);
      for (envlen = 0; envlist[envlen] != NULL; ++envlen)
        /* nothing */ ;
      nitems = sizeof(env_items) / sizeof(*env_items);
      /* + 3 because of PAM_TYPE, KRB5CCNAME and NULL entry */
      tmp = realloc(envlist, (envlen + nitems + 3) * sizeof(*envlist));
      if (tmp == NULL)
      {
        free(envlist);
        pam_syslog (pamh, LOG_ERR, "realloc environment failed: %m");
        _exit (ENOMEM);
      }
      envlist = tmp;
      for (i = 0; i < nitems; ++i)
      {
        const void *item;

        if (pam_get_item(pamh, env_items[i].item, &item) != PAM_SUCCESS || item == NULL)
          continue;
        if (asprintf(&envstr, "%s=%s", env_items[i].name, (const char *)item) < 0)
        {
          free(envlist);
          pam_syslog (pamh, LOG_ERR, "prepare environment failed: %m");
          _exit (ENOMEM);
        }
        envlist[envlen++] = envstr;
        envlist[envlen] = NULL;
      }

      if (asprintf(&envstr, "PAM_TYPE=%s", pam_type) < 0)
        {
          free(envlist);
          pam_syslog (pamh, LOG_ERR, "prepare environment failed: %m");
          _exit (ENOMEM);
        }
      envlist[envlen++] = envstr;
      envlist[envlen] = NULL;

      if (set_krb5ccname) {
	envlist[envlen++] = krb5cc_envstr;
	envlist[envlen] = NULL;
      }

      if (debug)
	pam_syslog (pamh, LOG_DEBUG, "Calling %s ...", arggv[0]);

      execve (arggv[0], arggv, envlist);
      i = errno;
      pam_syslog (pamh, LOG_ERR, "execve(%s,...) failed: %m", arggv[0]);
      free(envlist);
      _exit (i);
    }

finish:
  if (krb5cc_path) {
    if (retval != PAM_SUCCESS && retval != PAM_AUTHINFO_UNAVAIL) {
      unlink(krb5cc_path);
    }
    free(krb5cc_path);
  }

  return retval;
}

PAM_EXTERN int
pam_sm_authenticate (pam_handle_t *pamh, int flags UNUSED,
		     int argc, const char **argv)
{
  return call_exec ("auth", pamh, argc, argv);
}

PAM_EXTERN int
pam_sm_setcred (pam_handle_t *pamh UNUSED, int flags UNUSED,
		int argc UNUSED, const char **argv UNUSED)
{
  return PAM_IGNORE;
}

/* password updating functions */

PAM_EXTERN int
pam_sm_chauthtok(pam_handle_t *pamh, int flags,
		 int argc, const char **argv)
{
  if (flags & PAM_PRELIM_CHECK)
    return PAM_SUCCESS;
  return call_exec ("password", pamh, argc, argv);
}

PAM_EXTERN int
pam_sm_acct_mgmt(pam_handle_t *pamh, int flags UNUSED,
		 int argc, const char **argv)
{
  return call_exec ("account", pamh, argc, argv);
}

PAM_EXTERN int
pam_sm_open_session(pam_handle_t *pamh, int flags UNUSED,
		    int argc, const char **argv)
{
  return call_exec ("open_session", pamh, argc, argv);
}

PAM_EXTERN int
pam_sm_close_session(pam_handle_t *pamh, int flags UNUSED,
		     int argc, const char **argv)
{
  char *krb5cc_path;
  int retval;
  FILE *fd;

  krb5cc_path = get_krb5cc_path_from_environment(pamh);

  retval = call_exec ("close_session", pamh, argc, argv);

  if (krb5cc_path != NULL) {
    unlink(krb5cc_path);
    free(krb5cc_path);
  }

  return retval;
}

#ifdef PAM_STATIC
struct pam_module _pam_exec_modstruct = {
  "pam_exec",
  pam_sm_authenticate,
  pam_sm_setcred,
  pam_sm_acct_mgmt,
  pam_sm_open_session,
  pam_sm_close_session,
  pam_sm_chauthtok,
};
#endif
