/* $Id: child.c,v 1.27 2006/06/01 04:34:52 dm Exp $ */

/*
 *
 * Copyright (C) 2004 David Mazieres (dm@uun.org)
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2, or (at
 * your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
 * USA
 *
 */

#include "local.h"
#include <ctype.h>
#include <pwd.h>
#include <grp.h>
#ifdef HAVE_SETUSERCONTEXT
# ifdef HAVE_LOGIN_CAP_H
#  include <login_cap.h>
# endif /* HAVE_LOGIN_CAP_H */
#endif /* HAVE_SETUSERCONTEXT */
#if HAVE_GETSPNAM
# include <shadow.h>
#endif /* HAVE_GETSPNAM */
#include <sys/signal.h>
#include <sys/wait.h>

char *getusershell(void);
void setusershell(void);
void endusershell(void);

static int
validshell (const char *shell)
{
  const char *s;

  setusershell ();
  while ((s = getusershell ()))
    if (!strcmp (s, shell)) {
      endusershell ();
      return 1;
    }
  endusershell ();
  return 0;
}

struct passwd *
validuser (const char *user)
{
  char *avdir;
  struct stat sb;

  int expired = 0;
#ifdef HAVE_GETSPNAM
  struct spwd *spe;
#endif /* HAVE_GETSPNAM */
  struct passwd *pw = getpwnam (user);
  if (!pw || !pw->pw_uid || !validshell (pw->pw_shell) || pw->pw_dir[0] != '/')
    return NULL;

#ifdef HAVE_GETSPNAM
  spe = getspnam (pw->pw_name);
  if (spe && spe->sp_expire > 0
      && spe->sp_expire <= (time (NULL) / (24 * 60 * 60)))
    expired = 1;
#elif defined (HAVE_PASSWD_PW_EXPIRE)
  if (pw->pw_expire > 0 && pw->pw_expire <= time (NULL))
    expired = 1;
#endif /* HAVE_PASSWD_PW_EXPIRE */
  if (expired)
    return NULL;

  avdir = xmalloc (strlen (pw->pw_dir) + sizeof ("/.avenger"));
  strcpy (avdir, pw->pw_dir);
  strcat (avdir, "/.avenger");
  if (lstat (avdir, &sb)) {
    if (tmperr (errno)) {
      perror (avdir);
      exit (EX_OSERR);
    }
    pw = NULL;
  }
  else if (!S_ISDIR (sb.st_mode))
    pw = NULL;

  free (avdir);
  return pw;
}

#ifndef NGROUPS_MAX
# define NGROUPS_MAX 16
#endif /* !NGROUPS_MAX */

int
become_user (struct passwd *pw, int grouplist, int cd)
{
  int root = getuid () <= 0;
  u_int flags __attribute__ ((unused));

  if (setsid () == -1)
    perror ("setsid");
  if (!grouplist && setgroups (0, NULL)) {
    GETGROUPS_T gid = getgid ();
    if (setgroups (1, &gid) && root) {
      perror ("fatal: setgroups");
      return (EX_OSERR);
    }
  }

#ifdef HAVE_SETUSERCONTEXT
  flags = LOGIN_SETALL;
  flags &= ~LOGIN_SETUMASK;
  if (!grouplist) {
    flags &= ~LOGIN_SETGROUP;
    if (setgid (pw->pw_gid)) {
      perror ("setgid");
      if (root)
	return (EX_OSERR);
    }
  }
  if (root && setusercontext (NULL, pw, pw->pw_uid, flags)) {
    perror ("setusercontext");
    return (EX_OSERR);
  }
#else /* !HAVE_SETUSERCONTEXT */

# if HAVE_SETLOGIN
  if (setlogin (pw->pw_name))
    perror ("setlogin");
# endif /* HAVE_SETLOGIN */

# ifdef HAVE_INITGROUPS
  if (grouplist && initgroups (pw->pw_name, pw->pw_gid)) {
    fprintf (stderr, "initgroups failed\n");
    if (root)
      return (EX_OSERR);
  }
# else /* !HAVE_INITGROUPS */
  if (grouplist) {
    GETGROUPS_T gl[NGROUPS_MAX];
    int gn = 0;
    struct group *gr;
#  ifdef HAVE_EGID_IN_GROUPLIST
    gl[gn++] = pw->pw_gid;
#  endif /* HAVE_EGID_IN_GROUPLIST */
    setgrent ();
    while (gn < NGROUPS_MAX && (gr = getgrent ())) {
      int i;
      char **mp;
      if (gr->gr_gid == pw->pw_gid)
	continue;
      for (i = 0; i < gn; i++)
	if (gr->gr_gid == gl[i])
	  goto next;
      for (mp = gr->gr_mem; *mp; mp++)
	if (!strcmp (*mp, pw->pw_name)) {
	  gl[gn++] = gr->gr_gid;
	  goto next;
	}
    next:;
    }

    if (setgroups (gn, gl)) {
      perror ("setgroups");
      if (root)
	return (EX_OSERR);
    }
  }
# endif /* !HAVE_INITGROUPS */

  if (setgid (pw->pw_gid)) {
    perror ("setgid");
    if (root)
      return (EX_OSERR);
  }
  if (setuid (pw->pw_uid))
    perror ("setuid");
#endif /* !HAVE_SETUSERCONTEXT */

  if (root && pw->pw_uid && !getuid ()) {
    fprintf (stderr, "bacome_user: failed to drop privileges\n");
    return (EX_OSERR);
  }

  mysetenv ("HOME", pw->pw_dir, -1);
  if (cd && chdir (pw->pw_dir)) {
    perror (pw->pw_dir);
    return tmperr (errno) ? EX_OSERR : EX_CONFIG;
  }

  /* Who knows what the system libraries are doing.  Wouldn't want to
   * inherit a file descriptor to the shadow file across an exec. */
  endpwent ();
#ifdef HAVE_GETSPNAM
  endspent ();
#endif /* HAVE_GETSPNAM */

  return 0;
}

static int
linefromprog (char **resp, const char *prog, int mfd)
{
  pid_t pid = 0;
  int status = -1;
  int fds[2];
  static char buf[1025];
  int n, nn;

  *resp = NULL;
  while (*prog && isspace (*prog))
    prog++;
  if (!*prog)
    return 0;

  if (pipe (fds)) {
    perror ("pipe");
    return EX_OSERR;
  }
  if (lseek (mfd, 0, SEEK_SET) == -1 || (pid = fork ()) == -1) {
    perror (pid ? "fork" : "lseek");
    close (fds[0]);
    close (fds[1]);
    return EX_OSERR;
  }
  if (!pid) {
    close (fds[0]);
    errno = 0;
    if (mfd && dup2 (mfd, 0)) {
      perror ("dup2 (mfd)");
      _exit (EX_OSERR);
    }
    else if (fds[1] != 1 && dup2 (fds[1], 1) != 1) {
      perror ("dup2 (pipe)");
      _exit (EX_OSERR);
    }
    execl ("/bin/sh", "/bin/sh", "-c", prog, (char *) NULL);
    perror ("/bin/sh");
    _exit (EX_OSFILE);
  }
  close (fds[1]);

  n = 0;
  while ((nn = read (fds[0], buf + n, sizeof (buf) - n)))
    if (nn < 0 || (size_t) (n += nn) >= sizeof (buf)) {
      close (fds[0]);
      waitpid (pid, &status, 0);
      fprintf (stderr, "command '%s' produced too much output\n", prog);
      return EX_SOFTWARE;
    }
  close (fds[0]);
  waitpid (pid, &status, 0);
  if (!status) {
    if (n > 0 && memchr (buf, '\n', n) == buf + n - 1) {
      buf[n - 1] = '\0';
      *resp = buf;
    }
    else
      fprintf (stderr, "command '%s' didn't output exactly one line\n", prog);
  }

  if (WIFEXITED (status)) {
    status = WEXITSTATUS (status);
    if (status < 64
	|| (status > 68 && status != 99 && status != 100
	    && status != 111 && status != 112))
      status = 0;
    return status;
  }
  if (WIFSIGNALED (status))
    fprintf (stderr, "command '%s' exited with signal %d\n",
	     prog, WTERMSIG (status));
  return EX_TEMPFAIL;
}

static int
do_line (char *line, size_t len, FILE *parent, int mfd)
{
  char *in;
  int n;

  if (len > 0)
    line[--len] = '\0';
  switch (line[0]) {
  case '\n':
  case '\0':
  case '#':
    return 0;
  case '&':
    fprintf (parent, "%s\n", line);
    return 0;
  case '<':
    if (line[1] == '!') {
      n = linefromprog (&in, line + 2, mfd);
      if (in)
	fprintf (parent, "<%s\n", in);
      return n;
    }
    else
      fprintf (parent, "<%s\n", line + 1);
    return 0;
  case '!':
    n = linefromprog (&in, line + 1, mfd);
    if (!in || !*in)
      return n;
    if (in[0] != '.' && in[0] != '/')
      fprintf (stderr, "invalid mailbox name '%s' from '%s'\n", in, line);
    line = in;
    len = strlen (line);
    /* cascade */
  case '.':
  case '/':
    if (len < 2) {
      fprintf (stderr, "invalid line '%s' in .avenger/local* file\n", line);
      return 0;
    }
    if (line[len - 1] == '/') {
      line[len - 1] = '\0';
      return deliver_maildir (line, mfd, 1);
    }
    return deliver_mbox (line, mfd, 1);
  case '|':
    {
      pid_t pid = 0;
      int status = -1;
      while (*++line && isspace (*line))
	;
      if (!*line)
	return 0;
      if (lseek (mfd, 0, SEEK_SET) == -1 || (pid = fork ()) == -1) {
	perror (pid ? "fork" : "lseek");
	return EX_OSERR;
      }
      if (!pid) {
	if (dup2 (mfd, 0)) {
	  perror ("dup2");
	  _exit (EX_OSERR);
	}
	execl ("/bin/sh", "/bin/sh", "-c", line, (char *) NULL);
	perror ("/bin/sh");
	_exit (EX_OSFILE);
      }
      if (waitpid (pid, &status, 0) != pid) {
	perror ("waitpid");
	return EX_OSFILE;
      }
      if (WIFEXITED (status)) {
	status = WEXITSTATUS (status);
	if (status > 0 && status < 64)
	  fprintf (stderr, "command '%s' exited with status %d\n",
		   line, status);
	return status;
      }
      if (WIFSIGNALED (status))
	fprintf (stderr, "command '%s' exited with signal %d\n",
		 line, WTERMSIG (status));
      return EX_TEMPFAIL;
    }
  default:
    fprintf (stderr, "invalid line '%s' in .avenger/local* file\n", line);
    return 0;
  }
}

static int
open_local (const char *extra, char **path)
{
  char buf[300];
  const char *p;

  if (!extra) {
    int fd = open (".avenger/local", O_RDONLY);
    if (fd < 0)
      fd = open ("/dev/null", O_RDONLY);
    if (fd < 0)
      errno = EAGAIN;
    return fd;
  }

  p = extra + strlen (extra);
  snprintf (buf, sizeof (buf), ".avenger/local%c%s",
	    opt_separator, extra);
  for (;;) {
    int fd = open (buf, O_RDONLY);
    if (fd >= 0) {
      if (!*p)
	mysetenv ("PREFIX", extra, -1);
      else {
	int i;
	char sufn[80];
	mysetenv ("PREFIX", extra, p - extra - 1);
	mysetenv ("SUFFIX", p, -1);
	for (i = 1; (p = strchr (p, opt_separator)); i++) {
	  snprintf (sufn, sizeof (sufn), "SUFFIX%d", i);
	  mysetenv (sufn, ++p, -1);
	}
      }
      if (path)
	*path = xstrdup (buf);
      return fd;
    }
    if (p <= extra || tmperr (errno))
      return -1;
    do {
      p--;
    } while (p > extra && p[-1] != opt_separator);
    snprintf (buf, sizeof (buf), ".avenger/local%c%.*sdefault",
	      opt_separator, (int) (p - extra), extra);
  }
}

static void
nothing (int sig)
{
}

int
child (struct passwd *pw, int pfd, int mfd)
{
  FILE *parent = fdopen (pfd, "w");
  FILE *local;
  int err;
  int fd;
  struct lnbuf buf;
  int gotone = 0;
  char *localpath = NULL;	/* XXX - initialize to work around gcc4 */

  if (getenv ("FORKDEBUG")) {
    signal (SIGCONT, nothing);
    fprintf (stderr, "%s: pid %d waiting for debugger\n", progname, getpid ());
    pause ();
  }

  if (!parent) {
    perror ("fdopen");
    return EX_OSERR;
  }
  setvbuf (parent, NULL, _IOLBF, 0);
  if ((err = become_user (pw, 1, 1)))
    return err;

  fd = open_local (opt_extra, &localpath);
  if (fd < 0) {
    if (tmperr (errno))
      return EX_OSERR;
    else if (errno == ENOENT)
      return EX_NOUSER;
    else
      return EX_CONFIG;
  }
  local = fdopen (fd, "r");
  if (!local) {
    perror ("fdopen");
    close (fd);
    return EX_OSERR;
  }

  bzero (&buf, sizeof (buf));
  while ((err = readln (&buf, local, 8192))) {
    switch (err) {
    case LNBUF_OK:
      if (!gotone && buf.buf[0] == '#' && buf.buf[1] == '!') {
	struct stat sb;
	if (fstat (fd, &sb) || !(sb.st_mode & 0111)) {
	  fprintf (stderr, "%s starts '#!' but not executable\n", localpath);
	  return EX_TEMPFAIL;
	}
	if (lseek (mfd, 0, SEEK_SET) == -1) {
	  perror ("lseek");
	  return EX_OSERR;
	}
	if (mfd != 0 && (dup2 (mfd, 0) == -1 || close (mfd))) {
	  perror ("dup2");
	  return EX_OSERR;
	}
	fclose (parent);
	execl (localpath, localpath, (char *) NULL);
	perror (localpath);
	return EX_TEMPFAIL;
      }
      gotone = 1;
      err = do_line (buf.buf, buf.size, parent, mfd);
      if (err)
	return err;
      break;
    case LNBUF_EOFNL:
      fprintf (stderr, "ignoring incomplete line in %s\n", localpath);
      break;
    case LNBUF_NOMEM:
      fprintf (stderr, "out of memory reading line from %s\n", localpath);
      return EX_OSERR;
    case LNBUF_TOOBIG:
      fprintf (stderr, "line too long in %s file\n", localpath);
      return EX_CONFIG;
    case LNBUF_IOERR:
      fprintf (stderr, "error reading %s file\n", localpath);
      return EX_OSERR;
    }
  }

  if (gotone)
    return 0;
  else {
    char *dl = xmalloc (strlen (opt_default) + 2);
    sprintf (dl, "%s\n", opt_default);
    err = do_line (dl, strlen (dl), parent, mfd);
    free (dl);
    return err;
  }
}


syntax highlighted by Code2HTML, v. 0.9.1