/* Hurdish login

   Copyright (C) 1995, 1996 Free Software Foundation, Inc.

   Written by Miles Bader <miles@gnu.ai.mit.edu>

   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., 675 Mass Ave, Cambridge, MA 02139, USA. */

#include <hurd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <paths.h>
#include <ctype.h>
#include <utmp.h>
#include <pwd.h>
#include <grp.h>
#include <netdb.h>
#include <time.h>
#include <assert.h>
#include <version.h>

#include <netinet/in.h>
#include <arpa/inet.h>

#include <sys/fcntl.h>

#include <argp.h>
#include <argz.h>
#include <envz.h>
#include <idvec.h>
#include <error.h>
#include <timefmt.h>
#include <hurd/lookup.h>

extern error_t
exec_reauth (auth_t auth, int secure, int must_reauth,
	     mach_port_t *ports, unsigned num_ports,
	     mach_port_t *fds, unsigned num_fds);

char *argp_program_version = STANDARD_HURD_VERSION (login);
     

/* Defaults for various login parameters.  */
char *default_args[] = {
  "SHELL=/bin/bash",
  /* A ':' separated list of what to try if can't exec user's shell. */
  "BACKUP_SHELLS=/bin/bash:" _PATH_BSHELL,
  "HOME=/etc/login",		/* Initial WD.  */
  "USER=login",
  "UMASK=0",
  "NAME=Not logged in",
  "HUSHLOGIN=.hushlogin",	/* Looked up relative new home dir.  */
  "MOTD=/etc/motd",
  "PATH=/bin",
  "NOBODY=login",
  "NOAUTH_TIMEOUT=300",		/* seconds before unauthed sessions die. */
  0
};
/* Default values for the new environment.  */
char *default_env[] = {
  "PATH=/bin",
  0
};

/* Which things are copied from the login parameters into the environment. */
char *copied_args[] = {
  "USER", "SHELL", "HOME", "NAME", "VIA", "VIA_ADDR", "PATH", 0
};

static struct argp_option options[] =
{
  {"arg0",	'0', "ARG",   0, "Make ARG the shell's argv[0]"},
  {"envvar",	'e', "ENTRY", 0, "Add ENTRY to the environment"},
  {"envvar-default", 'E', "ENTRY", 0, "Use ENTRY as a default environment variable"},
  {"no-args",	'x', 0,	      0, "Don't put login args into the environment"},
  {"arg",	'a', "ARG",   0, "Add login parameter ARG"},
  {"arg-default", 'A', "ARG", 0, "Use ARG as a default login parameter"},
  {"no-environment-args", 'X', 0, 0, "Don't add the parent environment as default login params"},
  {"user",	'u', "USER",  0, "Add USER to the effective uids"},
  {"avail-user",'U', "USER",  0, "Add USER to the available uids"},
  {"group",     'g', "GROUP", 0, "Add GROUP to the effective groups"},
  {"avail-group",'G',"GROUP", 0, "Add GROUP to the available groups"},
  {"no-login",  'L', 0,       0, "Don't modify the shells argv[0] to look"
   " like a login shell"},
  {"preserve-environment", 'p', 0, 0, "Inherit the parent's environment"},
  {"via",	'h', "HOST",  0, "This login is from HOST"},
  {"no-passwd", 'f', 0,       0, "Don't ask for passwords"},
  {"paranoid",  'P', 0,       0, "Don't admit that a user doesn't exist"},
  {"keep",      'k', 0,       0, "Keep the old available ids, and save the old"
     "effective ids as available ids"},
  {"shell-from-args", 'S', 0, 0, "Use the first shell arg as the shell to invoke"},
  {"retry",     'R', "ARG",   OPTION_ARG_OPTIONAL,
   "Re-exec login with no users after non-fatal errors; if ARG is supplied,"
   "add it to the list of args passed to login when retrying"},
  {0, 0}
};
static char *args_doc = "[USER [ARG...]]";
static char *doc =
"Exec a program with uids and/or the environment changed appropriately.\v"
"To give args to the shell without specifying a user, use - for USER.\n"
"Current login parameters include HOME, SHELL, USER, NAME, and ROOT.";

/* Outputs whatever can be read from the io_t NODE to standard output, and
   then close it.  If NODE is MACH_PORT_NULL, assumes an error happened, and
   prints an error message using ERRNO and STR.  */
static void
cat (mach_port_t node, char *str)
{
  error_t err;
  if (node == MACH_PORT_NULL)
    err = errno;
  else
    for (;;)
      {
	char buf[1024], *data = buf;
	mach_msg_type_number_t data_len = sizeof (buf);

	err = io_read (node, &data, &data_len, -1, 16384);
	if (err || data_len == 0)
	  break;
	else
	  {
	    write (0, data, data_len);
	    if (data != buf)
	      vm_deallocate (mach_task_self (), (vm_address_t)data, data_len);
	  }
    }
  if (err)
    error (0, errno, "%s", str);
}

/* Add a utmp entry based on the parameters in ARGS & ARGS_LEN.  If
   INHERIT_HOST is true, the host parameters in ARGS aren't to be trusted, so
   try to get the host from the existing utmp entry (this only works if
   re-logging in during an existing session).  */
static void
add_utmp_entry (char *args, unsigned args_len, int inherit_host)
{
  struct utmp utmp;
  char const *host = 0;
  long addr = 0;

  bzero (&utmp, sizeof (utmp));

  gettimeofday (&utmp.ut_tv, 0);
  strncpy (utmp.ut_name, envz_get (args, args_len, "USER") ?: "",
	   sizeof (utmp.ut_name));

  if (! inherit_host)
    {
      char *via_addr = envz_get (args, args_len, "VIA_ADDR");
      host = envz_get (args, args_len, "VIA");
      if (host && strlen (host) > sizeof (utmp.ut_host))
	host = via_addr ?: host;
      if (via_addr)
	addr = inet_addr (via_addr);
    }

  if (!host || !addr)
    /* Get the host from the `existing utmp entry'.  This is a crock.  */
    {
      int tty_fd = 0;
      char *tty = 0;

      /* Search for a file descriptor naming a tty.  */
      while (!tty && tty_fd < 3)
	tty = ttyname (tty_fd++);
      if (tty)
	/* Find the old utmp entry for TTY, and grab its host parameters.  */
	{
	  struct utmp *old_utmp;
	  strncpy (utmp.ut_line, basename (tty), sizeof (utmp.ut_line));
	  old_utmp = getutline (&utmp);
	  if (old_utmp)
	    {
	      if (! host)
		host = old_utmp->ut_host;
	      if (! addr)
		addr = old_utmp->ut_addr;
	    }
	}
    }

  strncpy (utmp.ut_host, host ?: "", sizeof (utmp.ut_host));
  utmp.ut_addr = addr;

  login (&utmp);
}

/* Lookup the host HOST, and add entries for VIA (the host name), and
   VIA_ADDR (the dotted decimal address) to ARGS & ARGS_LEN.  */
static error_t
add_canonical_host (char **args, unsigned *args_len, char *host)
{
  struct hostent *he = gethostbyname (host);

  if (he)
    {
      char *addr = 0;

      /* Try and get an ascii version of the numeric host address.  */
      switch (he->h_addrtype)
	{
	case AF_INET:
	  addr = strdup (inet_ntoa (*(struct in_addr *)he->h_addr));
	  break;
	}

      if (addr && strcmp (he->h_name, addr) == 0)
	/* gethostbyname() cheated!  Lookup the host name via the address
	   this time to get the actual host name.  */
	he = gethostbyaddr (he->h_addr, he->h_length, he->h_addrtype);

      if (he)
	host = he->h_name;

      if (addr)
	{
	  envz_add (args, args_len, "VIA_ADDR", addr);
	  free (addr);
	}
    }

  return envz_add (args, args_len, "VIA", host);
}

/* Add the `=' separated environment entry ENTRY to ENV & ENV_LEN, exiting
   with an error message if we can't.  */
static void
add_entry (char **env, unsigned *env_len, char *entry)
{
  char *name = strsep (&entry, "=");
  error_t err = envz_add (env, env_len, name, entry);
  if (err)
    error (8, err, "Adding %s", entry);
}

/* Return in OWNED whether PID has an owner, or an error.  */
static error_t
check_owned (process_t proc_server, pid_t pid, int *owned)
{
  int flags = PI_FETCH_TASKINFO;
  char *waits = 0;
  mach_msg_type_number_t num_waits = 0;
  struct procinfo _pi, *pi = &_pi;
  mach_msg_type_number_t pi_size = sizeof pi;
  error_t err =
    proc_getprocinfo (proc_server, pid, &flags, (procinfo_t *)&pi, &pi_size,
		      &waits, &num_waits);

  if (! err)
    {
      *owned = !(pi->state & PI_NOTOWNED);
      if (pi != &_pi)
	vm_deallocate (mach_task_self (), (vm_address_t)pi, pi_size);
    }

  return err;
}

/* Kills the login session PID with signal SIG.  */
static void
kill_login (process_t proc_server, pid_t pid, int sig)
{
  error_t err;
  size_t num_pids;
  pid_t self = getpid ();

  do
    {
      pid_t _pids[num_pids = 20], *pids = _pids;
      err = proc_getloginpids (proc_server, pid, &pids, &num_pids);
      if (! err)
	{
	  size_t i;
	  for (i = 0; i < num_pids; i++)
	    if (pids[i] != self)
	      kill (pids[i], sig);
	  if (pids != _pids)
	    vm_deallocate (mach_task_self (), (vm_address_t)pids, num_pids);
	}
    }
  while (!err && num_pids > 0);
}

/* Looks at the login collection LID.  If the root process (PID == LID) is
   owned by someone, then exit (0), otherwise, if it's exited, exit (42).  */
static void
check_login (process_t proc_server, int lid)
{
  int owned;
  error_t err = check_owned (proc_server, lid, &owned);

  if (err == ESRCH)
    exit (42);			/* Nothing left to watch. */
  else
    assert_perror (err);

  if (owned)
    exit (0);			/* Our task is done.  */
}

/* Forks a process which will kill the login session headed by PID after
   TIMEOUT seconds if PID still has no owner.  */
static void
dog (time_t timeout, pid_t pid, char **argv)
{
  if (fork () == 0)
    {
      char buf[25];		/* Be gratuitously pretty.  */
      char *name = basename (argv[0]);
      time_t left = timeout;
      struct timeval tv = { 0, 0 };
      process_t proc_server = getproc ();

      while (left)
	{
	  time_t interval = left < 5 ? left : 5;

	  tv.tv_sec = left;

	  /* Frob ARGV so that ps show something nice.  */
	  fmt_named_interval (&tv, 0, buf, sizeof buf);
	  asprintf (&argv[0], "(watchdog for %s %d: %s remaining)",
		    name, pid, buf);
	  argv[1] = 0;

	  sleep (interval);
	  left -= interval;

	  check_login (proc_server, pid);
	}

      check_login (proc_server, pid);

      /* Give you-forgot-to-login message.  */
      tv.tv_sec = timeout;
      fmt_named_interval (&tv, 0, buf, sizeof buf);

      putc ('\n', stderr);	/* Make sure our message starts a line.  */
      error (0, 0, "Timed out after %s.", buf);

      /* Kill login session, trying to be nice about it.  */
      kill_login (proc_server, pid, SIGHUP);
      sleep (5);
      kill_login (proc_server, pid, SIGKILL);
      exit (0);
    }
}

void
main(int argc, char *argv[])
{
  int i;
  io_t node;
  char *arg;
  char *path;
  error_t err = 0;
  char *args = 0;		/* The login parameters */
  unsigned args_len = 0;
  char *passwd = 0;		/* Login parameters from /etc/passwd */
  unsigned passwd_len = 0;
  char *args_defs = 0;		/* Defaults for login parameters.  */
  unsigned args_defs_len = 0;
  char *env = 0;		/* The new environment.  */
  unsigned env_len = 0;
  char *env_defs = 0;		/* Defaults for the environment.  */
  unsigned env_defs_len = 0;
  char *parent_env = 0;		/* The environment we got from our parent */
  unsigned parent_env_len = 0;
  int no_environ = 0;		/* If false, use the env as default params. */
  int no_args = 0;		/* If false, put login params in the env. */
  int inherit_environ = 0;	/* True if we shouldn't clear our env.  */
  int no_passwd = 0;		/* Don't bother verifying what we're doing.  */
  int no_login = 0;		/* Don't prepend `-' to the shells argv[0].  */
  int paranoid = 0;		/* Admit no knowledge.  */
  int retry = 0;		/* For some failures, exec a login shell.  */
  char *retry_args = 0;		/* Args passed when retrying.  */
  unsigned retry_args_len = 0;
  char *shell = 0;		/* The shell program to run.  */
  char *sh_arg0 = 0;		/* The shell's argv[0].  */
  char *sh_args = 0;		/* The args to the shell.  */
  unsigned sh_args_len = 0;
  int shell_arg = 0;		/* If there are shell args, use the first as
				   the shell name. */
  struct idvec *eff_uids = make_idvec (); /* The UIDs of the new shell.  */
  struct idvec *eff_gids = make_idvec (); /* The EFF_GIDs.  */
  struct idvec *avail_uids = make_idvec (); /* The aux UIDs of the new shell.  */
  struct idvec *avail_gids = make_idvec (); /* The aux EFF_GIDs.  */
  struct idvec *parent_uids = make_idvec (); /* Parent uids, -SETUID. */
  struct idvec *parent_gids = make_idvec (); /* Parent gids, -SETGID. */
  mach_port_t exec;		/* The shell executable.  */
  mach_port_t cwd;		/* The child's CWD.  */
  mach_port_t root;		/* The child's root directory.  */
  mach_port_t ports[INIT_PORT_MAX]; /* Init ports for the new process.  */
  int ints[INIT_INT_MAX];	/* Init ints for it.  */
  mach_port_t fds[3];		/* File descriptors passed. */
  mach_port_t auth;		/* The new shell's authentication.  */
  mach_port_t proc_server = getproc ();
  mach_port_t parent_auth = getauth ();
  pid_t pid = getpid (), sid;

  /* These three functions are to do child-authenticated lookups.  See
     <hurd/lookup.h> for an explanation.  */
  error_t use_child_init_port (int which, error_t (*operate)(mach_port_t))
    {
      return (*operate)(ports[which]);
    }
  mach_port_t get_child_fd_port (int fd)
    {
      return fd < 0 || fd > 2 ? __hurd_fail (EBADF) : fds[fd];
    }
  mach_port_t child_lookup (char *name, char *path, int flags)
    {
      mach_port_t port = MACH_PORT_NULL;
      errno =
	hurd_file_name_path_lookup (use_child_init_port, get_child_fd_port, 0,
				    name, path, flags, 0, &port, 0);
      return port;
    }

  /* Print an error message with FMT, STR and ERR.  Then, if RETRY is on,
     exec a default login shell, otherwise exit with CODE (must be non-0).  */
  void fail (int code, error_t err, char *fmt, const char *str)
    {
      int retry_argc;
      char **retry_argv;
      char *via = envz_get (args, args_len, "VIA");
      extern void _argp_unlock_xxx (); /* Secret unknown function.  */

      error (retry ? 0 : code, err, fmt, str); /* May exit... */

      if (via)
	envz_add (&retry_args, &retry_args_len, "--via", via);
      argz_insert (&retry_args, &retry_args_len, retry_args, argv[0]);

      retry_argc = argz_count (retry_args, retry_args_len);
      retry_argv = alloca ((retry_argc + 1) * sizeof (char *));
      argz_extract (retry_args, retry_args_len, retry_argv);

      /* Reinvoke ourselves with no userids or anything; shouldn't return.  */
      _argp_unlock_xxx ();	/* Hack to get around problems with getopt. */
      main (retry_argc, retry_argv);
      exit (code);		/* But if it does... */
    }

  /* Make sure that the parent_[ug]ids are filled in.  To make them useful
     for su'ing, each is the avail ids with all effective ids but the first
     appended; this gets rid of the effect of login being suid, and is useful
     as the new process's avail id list (e.g., the real id is right).   */
  void need_parent_ids ()
    {
      if (parent_uids->num == 0 && parent_gids->num == 0)
	{
	  struct idvec *p_eff_uids = make_idvec ();
	  struct idvec *p_eff_gids = make_idvec ();
	  if (!p_eff_uids || !p_eff_gids)
	    err = ENOMEM;
	  if (! err)
	    err = idvec_merge_auth (p_eff_uids, parent_uids,
				    p_eff_gids, parent_gids,
				    parent_auth);
	  if (! err)
	    {
	      idvec_delete (p_eff_uids, 0); /* Counteract setuid. */
	      idvec_delete (p_eff_gids, 0);
	      err = idvec_merge (parent_uids, p_eff_uids);
	      if (! err)
		err = idvec_merge (parent_gids, p_eff_gids);
	    }
	  if (err)
	    error (39, err, "Can't get uids");
	}
    }

  /* Returns true if the *caller* of this login program has UID.  */
  int parent_has_uid (uid_t uid)
    {
      need_parent_ids ();
      return idvec_contains (parent_uids, uid);
    }
  /* Returns true if the *caller* of this login program has GID.  */
  int parent_has_gid (gid_t gid)
    {
      need_parent_ids ();
      return idvec_contains (parent_gids, gid);
    }
  /* Returns the number of parent uids.  */
  int count_parent_uids ()
    {
      need_parent_ids ();
      return parent_uids->num;
    }
  /* Returns the number of parent gids.  */
  int count_parent_gids ()
    {
      need_parent_ids ();
      return parent_gids->num;
    }

  /* Make sure the user should be allowed to do this.  */
  void verify_passwd (const char *name, const char *password,
		      uid_t id, int is_group)
    {
      extern char *crypt (const char *string, const char salt[2]);
#pragma weak crypt
      char *prompt, *unencrypted, *encrypted;

      if (!password || !*password
	  || idvec_contains (is_group ? eff_gids : eff_uids, id)
	  || idvec_contains (is_group ? avail_gids : avail_uids, id)
	  || (no_passwd
	      && (parent_has_uid (0)
		  || (is_group ? parent_has_uid (id) : parent_has_gid (id)))))
	return;			/* Already got this one.  */

      if (name)
	asprintf (&prompt, "Password for %s%s:",
		  is_group ? "group " : "", name);
      else
	prompt = "Password:";

      unencrypted = getpass (prompt);
      if (crypt)
	{
	  encrypted = crypt (unencrypted, password);
	  /* Paranoia may destroya.  */
	  memset (unencrypted, 0, strlen (unencrypted));
	}
      else
	encrypted = unencrypted;

      if (name)
	free (prompt);

      if (strcmp (encrypted, password) != 0)
	fail (50, 0, "Incorrect password", 0);
    }

  /* Parse our options...  */
  error_t parse_opt (int key, char *arg, struct argp_state *state)
    {
      switch (key)
	{
	case 'p': inherit_environ = 1; break;
	case 'x': no_args = 1; break;
	case 'X': no_environ = 1; break;
	case 'e': add_entry (&env, &env_len, arg); break;
	case 'E': add_entry (&env_defs, &env_defs_len, arg); break;
	case 'a': add_entry (&args, &args_len, arg); break;
	case 'A': add_entry (&args_defs, &args_defs_len, arg); break;
	case '0': sh_arg0 = arg; break;
	case 'L': no_login = 1; break;
	case 'f': no_passwd = 1; break;
	case 'P': paranoid = 1; break;
	case 'S': shell_arg = 1; break;

	case 'R':
	  retry = 1;
	  if (arg)
	    {
	      err = argz_add (&retry_args, &retry_args_len, arg);
	      if (err)
		error (10, err, "Adding retry arg %s", arg);
	    }
	  break;

	case 'h':
	  add_canonical_host (&args, &args_len, arg);
	  retry = 1;
	  break;

	case 'k':
	  need_parent_ids ();
	  idvec_merge (avail_uids, parent_uids);
	  idvec_merge (avail_gids, parent_gids);
	  break;

	case ARGP_KEY_ARG:
	  if (state->arg_num > 0)
	    {
	      err = argz_create (state->argv + state->next - 1,
				 &sh_args, &sh_args_len);
	      state->next = state->argc; /* Consume all args */
	      if (err)
		error (9, err, "Adding %s", arg);
	      break;
	    }

	  if (strcmp (arg, "-") == 0)
	    arg = 0;		/* Just like there weren't any args at all.  */
	  /* Fall through to deal with adding the user.  */

	case 'u':
	case 'U':
	case ARGP_KEY_NO_ARGS:
	  {
	    /* USER is whom to look up.  If it's 0, then we hit the end of
	       the sh_args without seeing a user, so we want to add defaults
	       values for `nobody'.  */
	    char *user = arg ?: envz_get (args, args_len, "NOBODY");
	    struct passwd *pw =
	      isdigit (*user) ? getpwuid (atoi (user)) : getpwnam (user);
	    /* True if this is the user arg and there were no user options. */
	    int only_user =
	      (key == ARGP_KEY_ARG
	       && eff_uids->num == 0 && avail_uids->num <= count_parent_uids ()
	       && eff_gids->num == 0 && avail_gids->num <= count_parent_gids ());

	    if (! pw)
	      if (! arg)
		/* It was nobody anyway.  Just use the defaults.  */
		break;
	      else if (paranoid)
		/* In paranoid mode, we don't admit we don't know about a
		   user, so we just ask for a password we we know the user
		   can't supply.  */
		verify_passwd (only_user ? 0 : user, "*", -1, 0);
	      else
		fail (10, 0, "%s: Unknown user", user);

	    if (arg)
	      /* If it's not nobody, make sure we're authorized.  */
	      verify_passwd (only_user ? 0 : pw->pw_name, pw->pw_passwd,
			     pw->pw_uid, 0);

	    if (key == 'U')
	      /* Add available ids instead of effective ones.  */
	      {
		idvec_add_new (avail_uids, pw->pw_uid);
		idvec_add_new (avail_gids, pw->pw_gid);
	      }
	    else
	      {
		if (key == ARGP_KEY_ARG || eff_uids->num == 0)
		  /* If it's the argument (as opposed to option) specifying a
		     user, or the first option user, then we get defaults for
		     various things from the password entry.  */
		  {
		    envz_add (&passwd, &passwd_len, "HOME", pw->pw_dir);
		    envz_add (&passwd, &passwd_len, "SHELL", pw->pw_shell);
		    envz_add (&passwd, &passwd_len, "NAME", pw->pw_gecos);
		    envz_add (&passwd, &passwd_len, "USER", pw->pw_name);
		  }
		if (arg)	/* A real user.  */
		  if (key == ARGP_KEY_ARG)
		    /* The main user arg; add both effective and available
		       ids (the available ids twice, for posix compatibility
		       -- once for the real id, and again for the saved).  */
		    {
		      /* Updates the real id in IDS to be ID.  */
		      void update_real (struct idvec *ids, uid_t id)
			{
			  if (ids->num == 0
			      || !idvec_tail_contains (ids, 1, ids->ids[0]))
			    idvec_insert (ids, 0, id);
			  else
			    ids->ids[0] = id;
			}
			
		      /* Effective */
		      idvec_insert_only (eff_uids, 0, pw->pw_uid);
		      idvec_insert_only (eff_gids, 0, pw->pw_gid);
		      /* Real */
		      update_real (avail_uids, pw->pw_uid);
		      update_real (avail_gids, pw->pw_gid);
		      /* Saved */
		      idvec_insert_only (avail_uids, 1, pw->pw_uid);
		      idvec_insert_only (avail_gids, 1, pw->pw_gid);
		    }
		  else
		    {
		      idvec_add_new (eff_uids, pw->pw_uid);
		      idvec_add_new (eff_gids, pw->pw_gid);
		    }
	      }
	  }
	  break;

	case 'g':
	case 'G':
	  {
	    struct group *gr =
	      isdigit (*arg) ? getgrgid (atoi (arg)) : getgrnam (arg);
	    if (! gr)
	      fail (11, 0, "%s: Unknown group", arg);
	    verify_passwd (gr->gr_name, gr->gr_passwd, gr->gr_gid, 1);
	    idvec_add_new (key == 'g' ? eff_gids : avail_gids, gr->gr_gid);
	  }
	  break;

	default:
	  return ARGP_ERR_UNKNOWN;
	}
      return 0;
    }
  struct argp argp = {options, parse_opt, args_doc, doc};

  /* Don't allow logins if the nologin file exists.  */
  node = file_name_lookup (_PATH_NOLOGIN, O_RDONLY, 0);
  if (node != MACH_PORT_NULL)
    {
      cat (node, _PATH_NOLOGIN);
      exit (40);
    }

  /* Put in certain last-ditch defaults.  */
  err = argz_create (default_args, &args_defs, &args_defs_len);
  if (! err)
    err = argz_create (default_env, &env_defs, &env_defs_len);
  if (! err)
    /* Set the default path using confstr() if possible.  */
    {
      size_t path_len = confstr (_CS_PATH, 0, 0);
      if (path_len > 0)
	{
	  char path[path_len];
	  path_len = confstr (_CS_PATH, path, path_len);
	  if (path_len > 0)
	    err = envz_add (&env_defs, &env_defs_len, "PATH", path);
	}
    }
  if (err)
    error (23, err, "adding defaults");

  err = argz_create (environ, &parent_env, &parent_env_len);

  /* Parse our options.  */
  argp_parse (&argp, argc, argv, ARGP_IN_ORDER, 0, 0);

  /* Now that we've parsed the command line, put together all these
     environments we've gotten from various places.  There are two targets:
     (1) the login parameters, and (2) the child environment.

     The login parameters come from these sources (in priority order):
      a) User specified (with the --arg option)
      b) From the passwd file entry for the user being logged in as
      c) From the parent environment, if --no-environ wasn't specified
      d) From the user-specified defaults (--arg-default)
      e) From last-ditch defaults given by the DEFAULT_* defines above

     The child environment is from:
      a) User specified (--environ)
      b) From the login parameters (if --no-args wasn't specified)
      c) From the parent environment, if --inherit-environ was specified
      d) From the user-specified default env values (--environ-default)
      e) From last-ditch defaults given by the DEFAULT_* defines above
   */

  /* Merge the login parameters.  */
  err = envz_merge (&args, &args_len, passwd, passwd_len, 0);
  if (! err && ! no_environ)
    err = envz_merge (&args, &args_len, parent_env, parent_env_len, 0);
  if (! err)
    err = envz_merge (&args, &args_len, args_defs, args_defs_len, 0);
  if (err)
    error (24, err, "merging parameters");

  err =
    auth_makeauth (getauth (), 0, MACH_MSG_TYPE_COPY_SEND, 0,
		   eff_uids->ids, eff_uids->num,
		   avail_uids->ids, avail_uids->num,
		   eff_gids->ids, eff_gids->num,
		   avail_gids->ids, avail_gids->num,
		   &auth);
  if (err)
    fail (3, err, "Authentication failure", 0);

  err = proc_getsid (proc_server, pid, &sid);
  assert_perror (err);		/* This should never fail.  */

  if (!no_login && count_parent_uids () != 0)
    /* Make a new login collection (but only for real users).  */
    {
      char *user = envz_get (args, args_len, "USER");
      if (user && *user)
	setlogin (user);
      proc_make_login_coll (proc_server);

      if (eff_uids->num + avail_uids->num == 0)
	/* We're transiting from having some uids to having none, which means
	   this is probably a new login session.  Unless specified otherwise,
	   set a timer to kill this session if it hasn't aquired any ids by
	   then.  Note that we fork off the timer process before clearing the
	   process owner: because we're interested in killing unowned
	   processes, proc's in-same-login-session rule should apply to us
	   (allowing us to kill them), and this way they can't kill the
	   watchdog (because it *does* have an owner).  */
	{
	  char *to = envz_get (args, args_len, "NOAUTH_TIMEOUT");
	  time_t timeout = to ? atoi (to) : 0;
	  if (timeout)
	    dog (timeout, pid, argv);
	}
    }

  if (eff_uids->num > 0)
    proc_setowner (proc_server, eff_uids->ids[0], 0);
  else
    proc_setowner (proc_server, 0, 1); /* Clear the owner.  */

  /* Now start constructing the exec arguments.  */
  bzero (ints, sizeof (*ints) * INIT_INT_MAX);
  arg = envz_get (args, args_len, "UMASK");
  ints[INIT_UMASK] = arg && *arg ? strtoul (arg, 0, 8) : umask (0);

  for (i = 0; i < 3; i++)
    fds[i] = getdport (i);

  for (i = 0; i < INIT_PORT_MAX; i++)
    ports[i] = MACH_PORT_NULL;
  ports[INIT_PORT_PROC] = getproc ();
  ports[INIT_PORT_CRDIR] = getcrdir ();	/* May be replaced below. */
  ports[INIT_PORT_CWDIR] = getcwdir ();	/*  "  */

  /* Now reauthenticate all of the ports we're passing to the child.  */
  err = exec_reauth (auth, 0, 1, ports, INIT_PORT_MAX, fds, 3);
  if (err)
    error (40, err, "Port reauth failure");

  /* These are the default values for the child's root/cwd.  We don't want to
     modify PORTS just yet, because we use it to do child-authenticated
     lookups.  */
  root = ports[INIT_PORT_CRDIR];
  cwd = ports[INIT_PORT_CWDIR];

  /* Find the shell executable (we copy the name, as ARGS may be changed).  */
  if (shell_arg && sh_args && *sh_args)
    /* Special case for su mode: get the shell from the args if poss.  */
    {
      shell = strdup (sh_args);
      argz_delete (&sh_args, &sh_args_len, sh_args); /* Get rid of it. */
    }
  else
    {
      arg = envz_get (args, args_len, "SHELL");
      if (arg && *arg)
	shell = strdup (arg);
      else
	shell = 0;
    }

  path = envz_get (args, args_len, "PATH");
  exec = shell ? child_lookup (shell, path, O_EXEC) : MACH_PORT_NULL;
  if (exec == MACH_PORT_NULL)
    {
      char *backup = 0;
      char *backups = envz_get (args, args_len, "BACKUP_SHELLS");
      err = errno;		/* Save original lookup errno. */

      if (backups && *backups)
	{
	  backups = strdupa (backups); /* Copy so we can trash it. */
	  while (exec == MACH_PORT_NULL && backups)
	    {
	      backup = strsep (&backups, ":, ");
	      if (*backup && (!shell || strcmp (shell, backup) != 0))
		exec = child_lookup (backup, path, O_EXEC);
	    }
	}

      /* Give the error message, but only exit if we couldn't default. */
      if (exec == MACH_PORT_NULL)
	fail (1, err, "%s", shell);
      else
	error (0, err, "%s", shell);

      /* If we get here, we looked up the default shell ok.  */
      shell = strdup (backup);
      error (0, 0, "Using SHELL=%s", shell);
      envz_add (&args, &args_len, "SHELL", shell);
      err = 0;			/* Don't emit random err msgs later!  */
    }

  /* Now maybe change the cwd/root in the child.  */

  arg = envz_get (args, args_len, "HOME");
  if (arg && *arg)
    {
      cwd = child_lookup (arg, 0, O_RDONLY);
      if (cwd == MACH_PORT_NULL)
	{
	  error (0, errno, "%s", arg);
	  error (0, 0, "Using HOME=/");
	  envz_add (&args, &args_len, "HOME", "/");
	}
    }

  arg = envz_get (args, args_len, "ROOT");
  if (arg && *arg)
    {
      root = child_lookup (arg, 0, O_RDONLY);
      if (root == MACH_PORT_NULL)
	fail (40, errno, "%s", arg);
    }

  /* Build the child environment.  */
  if (! no_args)
    /* We can't just merge ARGS, because it may contain the parent
       environment, which we don't always want in the child environment, so
       we pick out only those values of args which actually *are* args.  */
    {
      char **name;
      char *user = envz_get (args, args_len, "USER");

      for (name = copied_args; *name && !err; name++)
	if (! envz_get (env, env_len, *name))
	  {
	    char *val = envz_get (args, args_len, *name);
	    if (val && *val)
	      err = envz_add (&env, &env_len, *name, val);
	  }

      if (user)
	/* Copy the user arg into the environment as LOGNAME.  */
	err = envz_add (&env, &env_len, "LOGNAME", user);
    }
  if (! err && inherit_environ)
    err = envz_merge (&env, &env_len, parent_env, parent_env_len, 0);
  if (! err)
    err = envz_merge (&env, &env_len, env_defs, env_defs_len, 0);
  if (err)
    error (24, err, "Can't build environment");

  if (! sh_arg0)
    /* The shells argv[0] defaults to the basename of the shell.  */
    {
      char *shell_base = rindex (shell, '/');
      if (shell_base)
	shell_base++;
      else
	shell_base = shell;

      if (no_login)
	sh_arg0 = shell_base;
      else
	{
	  sh_arg0 = malloc (strlen (shell_base) + 2);
	  if (! sh_arg0)
	    err = ENOMEM;
	  else
	    /* Prepend the name with a `-', as is the odd custom.  */
	    {
	      sh_arg0[0] = '-';
	      strcpy (sh_arg0 + 1, shell_base);
	    }
	}
    }
  if (! err)
    err = argz_insert (&sh_args, &sh_args_len, sh_args, sh_arg0);
  if (err)
    error (21, err, "Error building shell args");

  /* Maybe output the message of the day.  Note that we we the child's
     authentication to do it, so that this program can't be used to read
     arbitrary files!  */
  arg = envz_get (args, args_len, "MOTD");
  if (arg && *arg)
    {
      char *hush = envz_get (args, args_len, "HUSHLOGIN");
      mach_port_t hush_node =
	(hush && *hush) ? child_lookup (hush, 0, O_RDONLY) : MACH_PORT_NULL;
      if (hush_node == MACH_PORT_NULL)
	{
	  mach_port_t motd_node = child_lookup (arg, 0, O_RDONLY);
	  if (motd_node != MACH_PORT_NULL)
	    cat (motd_node, arg);
	}
      else
	mach_port_deallocate (mach_task_self (), hush_node);
    }

  /* Now that we don't need to use PORTS for lookups anymore, put the correct
     ROOT and CWD in.  */
  ports[INIT_PORT_CRDIR] = root;
  ports[INIT_PORT_CWDIR] = cwd;

  /* Get rid of any accumulated null entries in env.  */
  envz_strip (&env, &env_len);

  /* No more authentications to fail, so cross our fingers and add our utmp
     entry.  */
  
  if (pid == sid)
    /* Only add utmp entries for the session leader.  */
    add_utmp_entry (args, args_len, !parent_has_uid (0));

  if ((eff_uids->num | eff_gids->num) && !no_login)
    {
      char *tty = ttyname (0);
      if (tty)
	{
	  /* Change the terminal to be owned by the user.  */
	  err = chown (tty,
		       eff_uids->num ? eff_uids->ids[0] : -1,
		       eff_gids->num ? eff_gids->ids[0] : -1);
	  if (err)
	    error (0, errno, "chown: %s", tty);
	}
    }

  err = file_exec (exec, mach_task_self (), EXEC_DEFAULTS,
		   sh_args, sh_args_len, env, env_len,
		   fds, MACH_MSG_TYPE_COPY_SEND, 3,
		   ports, MACH_MSG_TYPE_COPY_SEND, INIT_PORT_MAX,
		   ints, INIT_INT_MAX,
		   0, 0, 0, 0);
  if (err)
    error(5, err, "%s", shell);

  exit(0);
}
