/*******************************************************************************
 *  rungetty.c
 *  This is rungetty, a small console getty that allows for execution of
 *  arbitrary programs.
 *
 *  Copyright (C) 1999 Jakob 'sparky' Kaivo
 *  jake@nodomainname.net
 *  
 *  Based on code from mingetty by Florian La Roche. See the
 *  file mingetty.comments for original text.
 *
 *  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 of the License, or (at your option) any later version.
 *
 ******************************************************************************/

#define DEBUG_THIS 0

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <unistd.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <grp.h>
#include <pwd.h>
#include <string.h>
#include <time.h>
#include <utmp.h>
#include <sys/file.h>
#include <sys/ioctl.h>
#include <sys/signal.h>
#include <sys/stat.h>
#include <sys/sysinfo.h>
#include <sys/types.h>
#include <sys/utsname.h>

#ifndef _PATH_LOGIN
#define _PATH_LOGIN "/bin/login"
#endif

#ifdef linux
#include <sys/param.h>
#define USE_SYSLOG
#endif

 /* If USE_SYSLOG is undefined all diagnostics go directly to /dev/console. */
#ifdef  USE_SYSLOG
#include <syslog.h>
#endif

#define ISSUE "/etc/issue"	/* displayed before the login prompt */
#define LOGIN " login: "	/* login prompt */
#define PROMPT_RETURN "Press return to activate "	/* when --prompt specified */

/* name of this program (argv[0]) */
static char *progname;

/* path of working directory for program */
static char *progpath = NULL;

/* 'nice' level of the program */
static int priority = 0;

/* on which tty line are we sitting? (e.g. tty1) */
static char *tty;

/* and what program do we run? */
static char *program_run;

/* some information about this host */
static struct utsname uts;

/* the hostname */
static char hn[MAXHOSTNAMELEN + 1];

/* process ID of this program */
static pid_t pid;

/* current time */
static time_t cur_time;

/* do not send a reset string to the terminal ? */
static int noclear = 0;
/* Print the whole string of gethostname() instead of just until the next "." */ static int longhostname = 0;

/* The user to run as */
static struct passwd *user;

/* The group */
static struct group *u_group;

/* set to wait for return before doing anything */
static int prompt_return = 0;

/* time to wait, seconds */
static int delay = 0;

/* AUTO_LAST points to a timestamp file used to limit autologins to
   immediately after system boot. */
#define AUTO_LAST "/var/log/autologin"
/* AUTO_TTY is the tty on which autologins will be accepted.  If set
   to an empty string, autologins will be accepted on any tty. */
#define AUTO_TTY "tty1"
/* If supplied, attempt an automatic login with this username. */
static char *autologin_name = NULL;

/* Prompt for use with --prompt */
static char *prompt;

/*
 * output error messages
 */
static void
error (const char *fmt,...)
{
  va_list va_alist;
  char buf[256], *bp;

#ifdef USE_SYSLOG
  buf[0] = '\0';
  bp = buf;
#else /* !USE_SYSLOG */
  int fd;

  strcpy (buf, progname);
  strcat (buf, ": ");
  bp = buf + strlen (buf);
#endif /* USE_SYSLOG */

  va_start (va_alist, fmt);
  vsprintf (bp, fmt, va_alist);
  va_end (va_alist);

#ifdef	USE_SYSLOG
  openlog (progname, LOG_PID, LOG_AUTH);
  syslog (LOG_ERR, buf);
  closelog ();
#else /* !USE_SYSLOG */
  strcat (bp, "\r\n");
  if ((fd = open ("/dev/console", 1)) >= 0)
    {
      write (fd, buf, strlen (buf));
      close (fd);
    }
#endif /* USE_SYSLOG */
  exit (1);
}

/*
 * update_utmp - update our utmp entry
 *
 * The utmp file holds miscellaneous information about things started by
 * /sbin/init and other system-related events. Our purpose is to update
 * the utmp entry for the current process, in particular the process
 * type and the tty line we are listening to. Return successfully only
 * if the utmp file can be opened for update, and if we are able to find
 * our entry in the utmp file.
 */
static void
update_utmp (void)
{
  struct utmp ut;
  int ut_fd;
  struct utmp *utp;
  void locktimeout (int);

  utmpname (_PATH_UTMP);
  setutent ();
  while ((utp = getutent ()))
    if (utp->ut_type == INIT_PROCESS && utp->ut_pid == pid)
      break;

  if (utp)
    {
      memcpy (&ut, utp, sizeof (ut));
    }
  else
    {
      /* some inits don't initialize utmp... */
      /* XXX we should print out a warning message */
      memset (&ut, 0, sizeof (ut));
      strncpy (ut.ut_id, tty + 3, sizeof (ut.ut_id));
    }
  endutent ();

  strncpy (ut.ut_user, "LOGIN", sizeof (ut.ut_user));
  strncpy (ut.ut_line, tty, sizeof (ut.ut_line));
  ut.ut_time = cur_time;
  ut.ut_type = LOGIN_PROCESS;
  ut.ut_pid = pid;

  pututline (&ut);
  endutent ();

  if ((ut_fd = open (_PATH_WTMP, O_APPEND | O_WRONLY)) >= 0)
    {
      (void) signal (SIGALRM, locktimeout);
      (void) alarm (3);
      flock (ut_fd, LOCK_EX);
      (void) alarm (0);
      write (ut_fd, &ut, sizeof (ut));
      flock (ut_fd, LOCK_UN);
      close (ut_fd);
    }
}

static void
output_special_char (unsigned char c)
{
  switch (c)
    {
    case 's':
      printf ("%s", uts.sysname);
      break;
    case 'n':
      printf ("%s", uts.nodename);
      break;
    case 'r':
      printf ("%s", uts.release);
      break;
    case 'v':
      printf ("%s", uts.version);
      break;
    case 'm':
      printf ("%s", uts.machine);
      break;
    case 'o':
#ifdef __USE_GNU
      printf ("%s", uts.domainname);
#else
      printf ("%s", uts.__domainname);
#endif
      break;
    case 'd':
    case 't':
      {
	char buff[20];
	struct tm *tm = localtime (&cur_time);

	strftime (buff, sizeof (buff), c == 'd' ? "%a %b %d %Y" : "%X", tm);
	fputs (buff, stdout);
	break;
      }
    case 'l':
      printf ("%s", tty);
      break;
    case 'u':
    case 'U':
      {
	int users = 0;
	struct utmp *ut;

	setutent ();
	while ((ut = getutent ()))
	  if (ut->ut_type == USER_PROCESS)
	    users++;
	endutent ();
	printf ("%d", users);
	if (c == 'U')
	  printf (" user%s", users == 1 ? "" : "s");
	break;
      }
    default:
      putchar (c);
    }
}

/* do_prompt - show login prompt, optionally preceded by /etc/issue contents */
static void
do_prompt (void)
{
  FILE *fd;
  char c;

  write (1, "\n", 1);		/* start a new line */
  if ((fd = fopen (ISSUE, "r")))
    {
      while ((c = getc (fd)) != EOF)
	{
	  if (c == '\\')
	    output_special_char (getc (fd));
	  else
	    putchar (c);
	}
      fflush (stdout);
      fclose (fd);
    }
  write (1, hn, strlen (hn));
  write (1, LOGIN, sizeof (LOGIN) - 1);
}

/* get_logname - get user name, establish speed, erase, kill, eol */
static char *
get_logname (void)
{
  static char logname[40];
  char *bp;
  unsigned char c;

  ioctl (0, TCFLSH, 0);		/* flush pending input */

  for (*logname = 0; *logname == 0;)
    {
      do_prompt ();
      for (bp = logname;;)
	{
	  if (read (0, &c, 1) < 1)
	    {
	      if (errno == EINTR || errno == EIO || errno == ENOENT)
		exit (0);
	      error ("%s: read: %s", tty, sys_errlist[errno]);
	    }
	  if (c == '\n' || c == '\r')
	    {
	      *bp = 0;
	      break;
	    }
	  else if (!isprint (c))
	    error ("%s: invalid character %c in login name", tty, c);
	  else if (bp - logname >= sizeof (logname) - 1)
	    error ("%s: too long login name", tty);
	  else
	    *bp++ = c;
	}
    }
  return logname;
}

static struct option const long_options[] =
{
  {"noclear", no_argument, &noclear, 1},
  {"long-hostname", no_argument, &longhostname, 1},
  {"autologin", required_argument, NULL, 'a'},
  {"prompt", optional_argument, NULL, 'p'},
  {"path", required_argument, NULL, 'w'},
  {"nice", required_argument, &priority, 'n'},
  {0, 0, 0, 0}
};

void
locktimeout (int dummy)
{
#ifdef USE_SYSLOG
  openlog (progname, LOG_PID, LOG_AUTH);
  syslog (LOG_ALERT, "Lock failed on wtmp");
  closelog ();
#else
  int fd;
  char buf[] = "Lock failed on wtmp\n";

  if ((fd = open ("/dev/console", 1)) >= 0)
    {
      write (fd, buf, strlen (buf));
      close (fd);
    }
#endif
}

/*
 * autologin_ok -- returns 1 if it's okay to auto-login when:
 *   this login is from /dev/tty1; and
 *   there was a login name passed with the --autologin option; and
 *   the autologin_name contains only "nice" characters; and
 *   this is the first autologin attempt since the last boot; 
 * return 0 otherwise.
 */
static int
autologin_ok (void)
{
  char c, *cp;
  int stat_err, fd;
  struct sysinfo info;
  struct stat sbuf;

  /* Autologins are restricted to AUTO_TTY if non-empty. */
  if (AUTO_TTY[0] && strcmp (tty, AUTO_TTY))
    return 0;

  /* An all-alphanumeric autologin name must be supplied. */
  if (autologin_name == NULL || autologin_name[0] == '\0')
    return 0;
  for (cp = autologin_name; (c = *cp); cp++)
    if (!isalnum (c) && c != '_')
      return 0;

  /* Get the uptime in info.uptime, and the last autologin time
     in sbuf.st_mtime. */
  sysinfo (&info);
  stat_err = stat (AUTO_LAST, &sbuf);

  /* If a stat error other than "no such file" occurs, I don't
     know what went wrong, so I'll proceed with caution by
     denying the autologin request. */
  if (stat_err && errno != ENOENT)
    return 0;

  /* If there's been an autologin granted since the last boot,
     deny this and any subsequent attempts.  Note that this test
     is skipped if the AUTO_LAST file doesn't exist. */
  if (!stat_err && time (NULL) - info.uptime < sbuf.st_mtime)
    return 0;

  /* Create the AUTO_LAST file.  The mtime of this file provides
     a persistent record of the last time that an autologin
     request was granted.  Deny the autologin request if either
     the file open or file close fails. */
  if ((fd = open (AUTO_LAST, O_WRONLY | O_CREAT | O_TRUNC, 0644)) < 0)
    return 0;
  if (close (fd) != 0)
    return 0;

  /* All tests are okay, so grant the autologin request. */
  return 1;
}

void
mingetty_login (char *logname, char *tty)
{
  if (autologin_ok ())
    {
      execl (_PATH_LOGIN, _PATH_LOGIN, "-f", autologin_name, NULL);
    }
  else
    {
      while ((logname = get_logname ()) == 0);
      execl (_PATH_LOGIN, _PATH_LOGIN, "--", logname, NULL);
    }
  error ("%s: can't exec " _PATH_LOGIN ": %s", tty, sys_errlist[errno]);
  exit (0);
}

/* open_tty - set up tty as standard { input, output, error } */
static void
open_tty (void)
{
  struct sigaction sa;
  char buf[20];
  int fd;

  /* Set up new standard input. */
  strcpy (buf, "/dev/");
  strcat (buf, tty);
  if (chown (buf, 0, 0) || chmod (buf, 0600))
    error ("%s: %s", buf, sys_errlist[errno]);

  sa.sa_handler = SIG_IGN;
  sa.sa_flags = 0;
  sigemptyset (&sa.sa_mask);
  sigaction (SIGHUP, &sa, NULL);

  /* vhangup() will replace all open file descriptors that point to our
     controlling tty by a dummy that will deny further reading/writing
     to our device. It will also reset the tty to sane defaults, so we
     don't have to modify the tty device for sane settings.
     We also get a SIGHUP/SIGCONT.
   */
  if ((fd = open (buf, O_RDWR, 0)) < 0
      || ioctl (fd, TIOCSCTTY, (void *) 1) == -1)
    error ("%s: cannot open tty: %s", buf, sys_errlist[errno]);
  if (!isatty (fd))
    error ("%s: not a tty", buf);

  vhangup ();
  /* Get rid of the present stdout/stderr. */
  close (2);
  close (1);
  close (0);
  close (fd);
  /* ioctl (0, TIOCNOTTY, (char *)1); */

  if (open (buf, O_RDWR, 0) != 0)
    error ("%s: cannot open as standard input: %s", buf,
	   sys_errlist[errno]);

  /* Set up standard output and standard error file descriptors. */
  if (dup (0) != 1 || dup (0) != 2)
    error ("%s: dup problem: %s", buf, sys_errlist[errno]);

  /* Write a reset string to the terminal. This is very linux-specific
     and should be checked for other systems. */
  if (!noclear)
    write (0, "\033c", 2);

  sa.sa_handler = SIG_DFL;
  sa.sa_flags = 0;
  sigemptyset (&sa.sa_mask);
  sigaction (SIGHUP, &sa, NULL);

  if (prompt_return)
    {
      write (1, prompt, strlen (prompt));
      if (program_run)
	{
	  write (1, "`", 1);
	  write (1, program_run, strlen (program_run));
	  write (1, "'", 1);
	}
      write (1, "\n", 1);
      read (0, buf, 1);
    }

#if	DEBUG_THIS
  printf ("session=%d, pid=%d, pgid=%d\n", getsid (0), getpid (), getpgid (0));
#endif
}

static void
usage (void)
{
  error ("usage: '%s tty program'", progname);
}


/*
 * main program
 */
int
main (int argc, char **argv)
{

  char *logname, *s;
  int c;

  progname = argv[0];
  uname (&uts);
  gethostname (hn, MAXHOSTNAMELEN);
  pid = getpid ();
  time (&cur_time);
  while ((c = getopt_long (argc, argv, "d:n:u:w:g:", long_options, (int *) 0)) != EOF)
    {
      switch (c)
	{
	case 0:
	  break;
	case 'a':
	  autologin_name = optarg;
	  break;
	case 'd':
	  delay = atoi (optarg);
	  break;
	case 'g':
	  u_group = getgrnam (optarg);
	  break;
	case 'n':
	  priority = atoi (optarg);
	  break;
	case 'p':
	  if (optarg)
	    prompt = optarg;
	  else
	    prompt = PROMPT_RETURN;
	  prompt_return = 1;
	  break;
	case 'u':
	  user = getpwnam (optarg);
	  break;
	case 'w':
	  progpath = optarg;
	  break;
	default:
	  usage ();
	}
    }
  if (!longhostname && (s = strchr (hn, '.')))
    *s = '\0';
  if (!user)
    user = getpwnam ("nobody");
  if (!u_group)
    u_group = getgrnam ("nobody");
  tty = argv[optind];
  if (!tty)
    usage ();
  program_run = argv[optind + 1];
  update_utmp ();

  if (delay)
    sleep (delay);

  open_tty ();

#ifdef linux
  putenv ("TERM=linux");
#else
  putenv ("TERM=vt100");
#endif
  putenv ("PATH=/bin:/usr/bin:/usr/ucb:/usr/local/bin");

  /* flush input and output queues, important for modems */
  ioctl (0, TCFLSH, 2);

  if (!program_run)
    mingetty_login (logname, tty);

  setgid (u_group->gr_gid);
  setuid (user->pw_uid);

  if (progpath)
    chdir (progpath);

  nice (priority);

  execvp (program_run, &argv[optind + 1]);
  error ("%s: can't exec %s : %s", tty, program_run, sys_errlist[errno]);
  exit (0);
}
