/* main.c

   Main entry point for modemd... */

/*
 * Copyright (c) 1995 RadioMail Corporation.  All rights reserved.
 *
 * 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, this list of conditions and the following disclaimer.
 * 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. Neither the name of RadioMail Corporation nor the names of its
 *    contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY RADIOMAIL CORPORATION AND CONTRIBUTORS
 * ``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
 * RADIOMAIL CORPORATION OR CONTRIBUTORS 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.
 *
 * This software was written for RadioMail Corporation by Ted Lemon
 * under a contract with Vixie Enterprises, and is based on an earlier
 * design by Paul Vixie.
 */

#ifndef lint
static char copyright[] =
"@(#) Copyright (c) 1995 RadioMail Corporation.  All rights reserved.\n";
#endif /* not lint */

#include "cdefs.h"
#include "osdep.h"
#include "global.h"
#include "mcap.h"
#include <syslog.h>
#include <pwd.h>
#include <stdio.h>

static char *ttyName;
static char progName [1024];

char **Argv;

int main (argc, argv, envp)
     int argc;
     char **argv, **envp;
{
  char *noName = "<noname>";
  char *capName, *ttyNameP;
  int ttyfd;
  int pstatus, pipefd [2], spare;

  /* Export argument list... */
  Argv = argv;

  /* If we were called by init, we have no file descriptors open yet.
     Syslog can't be on descriptor 0, 1 or 2, since those will be the
     terminal descriptors, so use up some descriptors on nonsense before
     calling openlog. */
  pstatus = pipe (pipefd);
  spare = dup (pipefd [0]);

  if (argc == 2)
    capName = ttyNameP = argv [1];
  else if (argc == 3)
    {
      capName = argv [1];
      ttyNameP = argv [2];
    }
  else
    {
      ttyNameP = noName;
    }
  sprintf (progName, "modemd %s", ttyNameP);
  openlog (progName, LOG_NDELAY, LOG_LOCAL3);
  /* setlogmask(LOG_UPTO (LOG_INFO)); /* */
  syslog (LOG_DEBUG, "Log Mask is %x", LOG_UPTO (LOG_INFO));

  /* If the arguments were bogus... */
  if (ttyNameP == noName)
    error ("Usage: %s [<modem type>] <modem device>", argv [0]);

  /* Save ttyName in a global for future reference... */
  ttyName = ttyNameP;

  /* If we couldn't make dummy descriptors... */
  if (pstatus < 0 || spare < 0)
    warn ("Unable to make dummy descriptors - log may be incomplete.");

  /* We're done with the dummies... */
  close (pipefd [0]);
  close (pipefd [1]);
  close (spare);

  /* If the ttyname and the capname are the same, the capname might have
     a leading /dev or something - strip off all but the last path element. */
  if (capName [0] == '/')
    capName = strrchr (capName, '/') + 1;

  /* Get the modemcap entry specified on the command line. */
  init_mcap (capName);

  if (modemcap.init_string)
    atparse (modemcap.init_string, 0);

  /* Open the terminal... */
  ttyfd = ttsetup (ttyNameP);

  /* Lock the terminal... */
  ttnewlock (ttyNameP, 1);

  /* Initialize the modem... */
  if (!modemAttn (ttyfd) &&
      !modemAttn (ttyfd))
    error ("Can't get modem's attention on startup.");
  modemInit (ttyfd);

  /* Release the lock so that tip, et al, can get in... */
  ttunlock ();

  /* Wait for it to ring, then answer it... */
  modemAnswer (ttyfd);
}
  
/* Wait for the modem to ring.   When it does, answer it. */
void modemAnswer (tty)
     int tty;
{
  int matchID;
  extern char *atterr;	/* Attention deficit error message. */

  syslog (LOG_INFO, "Waiting for modem to ring.");

  /* Wait for input to be available on tty... */
  ttwait (tty);

  /* Get a lock on the tty... */
  /* XXX This is of course one place where disaster could strike:
     if somebody tips to the modem while we're waiting for it to ring,
     and we don't wake up before they release the lock, the modem
     could wind up being initialized to a bad state.   I know of no 
     reliable means of detecting this situation, though.  :'(
     The good news is that I'm pretty sure that at least BSD kernels
     wake up *everybody* who's waiting for I/O on a descriptor and then
     let them sort it out. */
  /* XXX It's also possible that somebody could dial in at exactly
     the same moment that somebody else tips to the modem, so that
     we wake up on the RING just as tip grabs the lock.   This leaves
     tip talking to a ringing modem - very bad.   Again, I don't see
     an obvious fix using the existing locking mechanism.   Unfortunately,
     on a busy modem rack, this isn't an entirely unlikely scenario.
     It would be nice if there were some way for tip to say to the switch,
     ``don't answer on this line,'' or even for it to find out which
     modem is next in line in the hunt group and pick a different one.
     It may be possible to determine this based on some kind of dialin log,
     if enough is known about the operation of the hunt group. */
  ttgetlock (1);

  /* See what it is... */
  if (ttmatch (tty, 0, "RING", (char *)0) <= 0)
    error ("ttmatch returned without RING.");
  ttdrain (tty);
  ttwriteslow (tty, "ATA\r");
  if (ttmatch (tty, 1000000, "ATA", (char *)0) <= 0)
    error (atterr, "ATA");
  matchID = ttmatch (tty, 65000000, "CONNECT", "NO CARRIER", (char *)0);
  if (matchID == 1)
    {
      /* Match DTE speed to DCE speed if requested. */
      if (modemcap.match_speed && !ttread_connect_speed (tty, 1000000))
	error ("Unable to get modem speed.");
	  
      /* Drain input buffer up to \r\n, which should be the last
	 thing the modem prints before we're connected to the caller. */
      if (ttmatch (tty, 1000000, "\r\n", (char *)0) == -1)
	error ("No CR/LF after CONNECT\n");
      connected (tty);
      /*NOTREACHED*/
    }
  else if (matchID == 2)
    error ("Phone rang, but no carrier was detected.");
  else if (matchID == -1)
    error ("No response from modem within connect timeout.");
  else
    error ("Unknown result %s in answer attempt.", ttstring (tty));
}

/* Be ready to catch a SIGHUP when the modem hangs up... */
int hung_up = 0;
void hup (sig)
     int sig;
{
  hung_up = 1;
}

/* Be ready to catch a SIGCHLD when the child exits.... */
int exited = 0;
void chld (sig)
     int sig;
{
  exited = 1;
}

/* Called once CONNECT is heard from the modem.   Sets up child process's
   I/O, logs user in (if not calling login), forks and execs login
   program.   Waits for SIGHUP and/or program exit. */

void connected (tty)
     int tty;
{
  int pid, wpid, status;
  struct passwd *pw;
  sigset_t signal_mask;
  char *s, *e;
  char buf [512];
  int fds [2];
  FILE *cstderr;
  size_t len;

  syslog (LOG_INFO, "Connected.");
  signal (SIGHUP, hup);
  signal (SIGCHLD, chld);

  /* If we're supposed to connect the modem directly to a TCP port,
     do it and exit when it's done. */
  if (modemcap.connect_addr)
    exit (tcp_connect (tty));

  s = modemcap.banner;
  do {
    if (*s == '%')
      {
	switch (s [1])
	  {
	  case '%':
	    ttwrite (tty, "%");
	    break;
	  case 'h':
	    gethostname (buf, sizeof buf);
	    ttwrite (tty, buf);
	    break;
	  case 't':
	    ttwrite (tty, ttyName);
	    break;
	  case 's':
#if defined (CTL_KERN) && defined (KERN_OSTYPE)
	    fds [0] = CTL_KERN;
	    fds [1] = KERN_OSTYPE;
	    len = (sizeof buf) - 1;
	    if (sysctl (fds, 2, buf, &len, (char *)0, 0) >= 0)
	      buf [len] = 0;
	    else
#endif
	      strcpy (buf, "UnknownOS");
	    ttwrite (tty, buf);
	    break;

	  case 'v':
#if defined (CTL_KERN) && defined (KERN_OSRELEASE)
	    fds [0] = CTL_KERN;
	    fds [1] = KERN_OSRELEASE;
	    len = (sizeof buf) - 1;
	    if (sysctl (fds, 2, buf, &len, (char *)0, 0) >= 0)
	      buf [len] = 0;
	    else
#endif
	      strcpy (buf, "0.0");
	    ttwrite (tty, buf);
	    break;

	  case 0:
	    s--;
	    break;
	  }
	s += 2;
      }
    e = strchr (s, '%');
    if (!e)
      e = s + strlen (s);
    if (e - s >= (sizeof buf) - 1)
      e = s + (sizeof buf) - 1;
    strncpy (buf, s, e - s);
    buf [e - s] = 0;
    ttwrite (tty, buf);
    s = e;
  } while (s [0]);
    
  /* If we have been asked to detect an incoming HDLC-encapsulated
     PPP packet and run PPP if we see one, but detect a normal login
     otherwise, go do that. */

  if (modemcap.ppp_prog)
    {
      ppp_detect (tty, ttyName);
    }
  if (!modemcap.raw_tty)
    ttnormal (tty);
  redirect_std (tty);
  if (modemcap.do_login)
    {
      if (!modemcap.luser)
	modemcap.luser = "nobody";
      pw = getpwnam (modemcap.luser);
#if 0
      if (!pw)
	error ("Unknown user %s in modemcap entry %s\n",
	       modemcap.luser, modemcap.name);
#endif

      do_login(modemcap.luser, ttyName);
    }
  pid = fork ();
  if (pid < 0)
    error ("Can't fork: %m");
  else if (pid == 0)
    {
      char *s, *t;
      char *ev [5], **av;
      char *lp, *unb;
      int argc = 0;
      static char home [PATH_MAX + 6 /* HOME=\0 */];
      static char user [USER_MAX + 6 /* USER=\0 */];
      static char shell [PATH_MAX + 7 /* SHELL=\0 */];
      static char path [sizeof _PATH_DEFPATH + 6 /* PATH=\0 */];

      /* Capture child's stderr output and log it? */
      if (modemcap.logstderr)
	{
	  if (pipe (fds) < 0)
	    error ("can't capture child stderr: %m");

	  pid = fork ();
	  if (pid < 0)
	    error ("can't fork: %m");
	  else if (pid == 0) 
	    {
	      close (fds [1]);
	      if ((cstderr = fdopen (fds [0], "r")) == NULL)
		error ("Can't fdopen: %m");
	      while (fgets (buf, sizeof buf, cstderr) != NULL)
		syslog (LOG_ERR, "%s", buf);
	      fclose (cstderr);
	      exit (0);
	    }
		
	  close (fds [0]);
	  close (2);
	  dup2 (fds [1], 2);
	  if (fds [1] != 2)
	    close (fds [1]);
	}

      for (s = modemcap.program; *s; s++)
	if (s [0] == ' ' && s [1] != ' ')
	  ++argc;
      av = (char **)malloc ((argc + 1) * sizeof (char *));
      if (modemcap.luser)
	unb = (char *)malloc (strlen (modemcap.luser) + 2);
      else
	unb = "";
      if (!av || !unb)
	error ("No memory to build argument list (%d %d).",
	       (argc + 1) * sizeof (char *), (modemcap.luser
					      ? strlen (modemcap.luser + 2)
					      : 0));
      s = modemcap.program;
      argc = 0;
      do {
	  while (*s && *s == ' ')
	    ++s;
	  if (*s)
	    {
	      t = s;
	      while (*t && *t != ' ')
		++t;
	      av [argc++] = s;
	      if (*t == ' ')
		s = t + 1;
	      else
		s = t;
	      *t = 0;
	      syslog (LOG_DEBUG, "argv [%d] = %s\n", argc - 1, av [argc - 1]);
	    }
	} while (*s);
      av [argc] = (char *)0;
      lp = av [0];
      if (modemcap.luser)
	{
	  unb [0] = '-';
	  strcpy (unb + 1, modemcap.luser);
	  av [0] = unb;
	}
      syslog (LOG_DEBUG, "lp = %s  av [0] = %s\n", lp, av [0]);

      /* If we're supposed to do the login, set the uid and gid now... */
      if (modemcap.do_login)
	{
	  if (SETGID (pw -> pw_gid) < 0)
	    error ("Can't setgid to %d: %m", pw -> pw_gid);
	  if (SETUID (pw -> pw_uid) < 0)
	    error ("Can't setuid to %s (%d): %m",
		   modemcap.luser, pw -> pw_gid);

	  /* Set up the environment... */
	  strcpy (home, "HOME=");
	  strncpy (&home [5], pw -> pw_dir, sizeof home - 5);
	  ev [0] = home;
	  strcpy (user, "USER=");
	  strncpy (&user [5], pw -> pw_name, sizeof user - 5);
	  ev [1] = user;
	  strcpy (shell, "SHELL=");
	  strncpy (&shell [6], pw -> pw_shell, sizeof shell - 6);
	  ev [2] = shell;
	  strcpy (path, "PATH=");
	  strcat (path, _PATH_DEFPATH);
	  ev [3] = path;
	  ev [4] = (char *)0;
	}
      else
	ev [0] = (char *)0;

      closelog ();

      execve (lp, av, ev);

      openlog (progName, LOG_NDELAY, LOG_DAEMON);
      error ("Can't execve %s: %m", lp);
    }

 badsig:
  /* Wait for SIGHUP or Child process exit. */
  sigemptyset (&signal_mask);
  sigsuspend (&signal_mask);

  /* Reap child process... */
  if (exited)
    wpid = wait (&status);

  /* If we get an error and nothing happened, the child has
     already (somehow) been reaped.   This shouldn't happen. */
  if (!exited)
    {
      if (!hung_up)
	{
	  warn ("Strange signal received while waiting for child.");
	  goto badsig;
	}
      /* Now that we've received the SIGHUP, the child should exit.   We'll
	 let init reap it.    Init should also vhangup the tty, depriving
	 the process of access to it if it's caught SIGHUP. */
      syslog (LOG_INFO, "Received hangup signal.");
    }
  else
    {
      if (WIFEXITED (status))
	syslog (LOG_INFO,
		"Child process exited with code %d", WEXITSTATUS (status));
      else if (WIFSTOPPED (status))
	{
	  syslog (LOG_ERR,
		  "Child process stopped on signal %d", WSTOPSIG (status));
	  goto badsig;
	}
      else
	syslog (LOG_INFO,
		"Child processes exited on signal %d", WTERMSIG (status));
    }
  do_logout ();
  exit (0);
}

/* Clean up loose ends before exiting... */

void cleanup ()
{
  do_logout ();
  ttunlock ();
}
