/* 
 * pmwho.c - Show logged-on users on Livingston Portmaster servers.
 *
 * General information
 * -------------------
 * This is a utility that serves the purpose of showing the status on the 
 * available ports on a Livingston Portmaster server. It takes care of logging 
 * on and issuing the commands to retrieve the information. The main advantage
 * with this utility is that it can be run by any user and only the program 
 * itself knows about the password. When Livingston incorporates a finger-
 * daemon of some sort into the Portmaster server this utility will no longer
 * be needed. An ANSI-C compiler is required to compile the program. The 
 * program is completely free, do what you like with it.
 *
 * Usage
 * -----
 * - If the program is named 'PMWHO' (constant defined below) and it is started
 *   like 'PMWHO pm1 pm2 pm3 ...' it will use 'pm1' ... as the hostnames to
 *   connect to (yes, it will connect to as many hosts as you specify, good if
 *   you have several Portmaster server's at your site).
 * - If the program is named 'PMWHO' and it is started like 'PMWHO' it will 
 *   use 'PMWHO_DEFAULT' (constant defined below) as the hostname to connect 
 *   to.
 * - If the program is named 'pm1' and it is started like 'pm1' it will use 
 *   'pm1' as the hostname to connect to.
 * - The program has two options with the following meaning attached to them:
 *   'h' - (it is started like 'PMWHO -h'), show usage.
 *   'a' - (it is started like 'PMWHO -a hosts ...'), show all serial ports 
 *         (including ports that are at present IDLE).
 *
 * Installation
 * ------------
 * The program should be made protection mode 111 ("--x--x--x", executable 
 * only) and the source-code should be made protection mode 400 ("r--------", 
 * read-only to owner (user 'root')) since the password could otherwise easily 
 * be found. You might even consider not having the source-code online after 
 * it has been compiled, and please do not set the password on your other 
 * systems to the same as that of the Portmaster server! There are so many 
 * ways of breaching security that the possibility of someone being able to 
 * copy the program and finding out the password to the Portmaster server must
 * be considered (one way would be to make the executable file readable and 
 * running 'trace' (SunOS 4.x) or 'truss' (Solaris 2.x) on it and watch the 
 * output).
 *
 * How it actually works
 * ---------------------
 * It starts a 'telnet' session to the Portmaster server, issues the commands 
 * to login, show the information and then logs out. The information is passed
 * to the parent process through pipes (the parent process has a pipe through 
 * which it can send commands to the 'stdin' of the 'telnet process, and 
 * another pipe through which it receives the output from the 'stdout' (and
 * 'stderr') of the 'telnet' process).
 *
 * Configuration options
 * ---------------------
 * Before building the program you should check out (and modify if necessary) 
 * the values of the constants to match your own configuration (good default 
 * values are already provided for SunOS 4.x, Solaris 2.x, DNIX and Linux).
 *
 * PMWHO - a string, default name of this utility.
 * PMWHO_DEFAULT - a string, default host to connect to.
 * PASSWORD - a string, the default password to use for user '!root' if
 *            the hostname is not defined in the 'struct pwtable'.
 * TELNET_EXEC - a string, the path to the executable 'telnet'.
 * TELNET_CLOSE - a string, the leading char's in the string that 'telnet' 
 *                sends when a connection is closed.
 * TELNET_PORT - a string, the port to use when starting 'telnet'.
 * TELNET_PROMPT - a string, the prompt of telnet.
 * SHOW_PRINTER - #define this if you want to see the information
 *                on the parallel port as well (it is otherwise ignored).
 * HAVE_SYS_WAIT, HAVE_WAIT - #define only one of these to indicate which
 *                            headerfile you have (<sys/wait.h> _or_ <wait.h>,
 *                            in that order), if none of them is #define-d a 
 *                            generic definition be used.
 *
 * Building the utility
 * --------------------
 * - Using SunOS 4.x:
 *   $ gcc -DSUNOS4 pmwho.c -o pmwho
 *   $ chmod 111 pmwho
 *
 * - Using Solaris 2.x:
 *   $ gcc -DSOLARIS pmwho.c -o pmwho
 *   $ chmod 111 pmwho
 *
 * - Using DNIX:
 *   $ cc -DDNIX pmwho.c -o pmwho
 *   $ chmod 111 pmwho
 *
 * - Using Linux:
 *   $ gcc -DLINUX pmwho.c -o pmwho
 *   $ chmod 111 pmwho
 *
 * - Using HP-UX 9.0x
 *   $ cc -DHPUX -D_INCLUDE_POSIX_SOURCE pmwho.c -o pmwho -O -Aa
 *   $ chmod 111 pmwho
 *
 * Version   Date   Comments
 * -------  ------  ----------------------------------------------------------
 *   1.0    950531  First release.
 *                  Bugs: 1. Only one password is used.
 *                        2. If "dup" or "execl" fails (in the child) the
 *                           'stderr' is closed.
 *
 *   1.1    950604  - Added PROMPT constant for those not using "Command> " as
 *                    prompt. 
 *                  - The information shown is changed, now only the active 
 *                    ports are shown unless the "-a" (show all ports 
 *                    regardless of state) option is given.
 *                  - program uses 'getopt' to check for option flags.
 *
 *   1.2    950706  - Portmaster server's with only 10 ports do not get the
 *                    "-- Press Return for More -- " message, solved in a 
 *                    generic way.
 *                  - An attempt to fix bug #2 (stderr).
 *                  - Added support for Linux.
 *                  - PROMPT replaced by "> " since we have to handle both 
 *                    a prompt of type "Command> " and "$hostname> ", this
 *                    will probably mean that we will run into trouble when
 *                    someone enters a username with "> " in it...
 *
 *   1.3    950904  - Improved the description part.
 *                  - Connects to multiple hosts.
 *
 *   1.4    950930  - Takes care of signal handling (v1.3 could leave a 
 *                    'telnet' process behind...).
 *
 *   1.5    960404  - Fixed bug #1 above. 
 *                  - Have #defines for compile on HP systems.
 *                  - Signal-handling bug fixed.
 *
 * Comments on how to improve this simple utility are very much welcome.
 *
 * /jp, Johan Persson, jp@abc.se
 *
 */

/* configuration constants - make your individual settings here */
#define PMWHO           "pmwho"
#define PMWHO_DEFAULT   "pm1"
/* #define SHOW_PRINTER */

/* configuration constants - make your individual settings here */

#define PASSWORD        "igotap3" /* the default password to use */

/* 
 * a password table structure with the purpose of being able to have different
 * passwords on different servers. If you only want to have one single password
 * you do not have to change anything, it will work as expected by default 
 * (by using the password PASSWORD as defined by the constant a few lines up).
 *
 * If you do however want to be able to have different passwords, then you must
 * enter the appropriate values below (I have added an example for the test-
 * server by the name of "pm1", uncomment and change as appropriate). Don't
 * forget that the constant PASSWORD is still the default if the hostname is
 * not found in the table.
 *
 */
   
struct {
  char hostname[32];
  char password[32];
} pwtable[] = {
  /* { "pm1", "topsecret" } */
};

#if defined(SUNOS4) || defined(SOLARIS)
#define TELNET_EXEC     "/usr/ucb/telnet"
#define TELNET_CLOSE    "Connection closed"
#define TELNET_PORT     "23"
#define TELNET_PROMPT   "telnet> "
#define HAVE_SYS_WAIT
#elif defined(DNIX) || defined(HPUX)
#define TELNET_EXEC     "/usr/bin/telnet"
#define TELNET_CLOSE    "Trying"
#define TELNET_PORT     "23"
#define TELNET_PROMPT   "telnet> "
#define HAVE_SYS_WAIT
#elif defined(LINUX)
#define TELNET_EXEC     "/bin/telnet"
#define TELNET_CLOSE    "Connection closed"
#define TELNET_PORT     "23"
#define TELNET_PROMPT   "telnet> "
#define HAVE_SYS_WAIT
#else /* generic entry */
#define TELNET_EXEC     "/usr/bin/telnet"
#define TELNET_CLOSE    "Connection closed"
#define TELNET_PORT     "23"
#define TELNET_PROMPT   "telnet> "
/* #define HAVE_SYS_WAIT */
/* #define HAVE_WAIT */
#endif

/* header files */
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>

#if defined(HPUX)
#include <unistd.h>
#define NSIG _NSIG
#endif

#include <sys/types.h>

#if defined(HAVE_SYS_WAIT)
#include <sys/wait.h>
#elif defined(HAVE_WAIT)
#include <wait.h>
#else
extern pid_t wait(int *status);
#endif

#include <errno.h>
#include <signal.h>

/* program constants and global vars */
char  *progname;
char  *hostname;
pid_t pid;
int   showall;
int   got_alarm;

#define LINELEN  256
#define IDLE_POS  50

/* program declarations */
void main(int argc, char *argv[]);
void usage(void);
void pmwho(void);
int  do_read(int fd, char *text1, char *text2, int show, int timeout);
int  do_write(int fd, char *text, int timeout);
void sigalrm(int signo);
void sigfunc(int signo);
void err_msg(const char *format, ...);
void err_exit(const char *format, ...);
void stop_telnet(void);

/* the program */

/* main procedure, find out program name and the hostname to use */
void main(int argc, char *argv[])
{
  char *t;
  int ch, n;

  extern int optind;

  t = strrchr(argv[0], '/');
  progname = (t != NULL) ? t+1 : argv[0];
  hostname = NULL;
  pid = -1;
  showall = 0;

  while ((ch = getopt(argc, argv, "ah")) != -1) {
    switch (ch) {
      case 'a':
	showall = 1;
	break;
      case 'h':
	/* fall-through */
      default:
	usage();
    }
  }

  /* setup signals properly */
  for(n = 1; n < NSIG; n++) signal(n, sigfunc);
#if defined(SIGCHLD)
  signal(SIGCHLD, SIG_IGN);
#endif
#if defined(SIGCLD)
  signal(SIGCLD, SIG_IGN);
#endif

  /* take different action according to usage */
  if (strcmp(progname, PMWHO) == 0) { /* PMWHO */
    if (optind >= argc) { /* usage: 'PMWHO' */
      hostname = PMWHO_DEFAULT;
      pmwho();
    }
    else if (optind == argc-1) { /* usage: 'PMWHO hostname' */
      hostname = argv[optind];
      pmwho();
    }
    else { /* usage: 'PMWHO hostname1 hostname2 ...' */
      for(; optind < argc; optind++) {
	hostname = argv[optind];
	fprintf(stdout, "[%s]\n", hostname);
	fflush(stdout);
	pmwho();
	if (optind < argc-1) {
	  fprintf(stdout, "\n");
	  fflush(stdout);
	}
      }
    }
  }
  else { /* usage: 'hostname' */
    hostname = progname;
    progname = PMWHO; /* for the messages */
    pmwho();
  }
  exit(0);
}

/* show usage info */
void usage(void)
{
  fprintf(stderr, "usage: %s [-ah]\n", progname);
  exit(0);
}

/* retrieve the information from the host by logging on, giving the command */
/* "show session" and logging out from 'hostname'                           */
void pmwho()
{
  int fdpipe[2][2], fdin, fdout, fderr, err, n;

  got_alarm = 0;

  /* we need pipes to communicate between the programs */
  if (pipe(fdpipe[0]) < 0) {
    err_msg("%s: pipe failed: errno=%d\n", progname, errno);
    return;
  }
  if (pipe(fdpipe[1]) < 0) {
    err_msg("%s: pipe failed: errno=%d\n", progname, errno);
    close(fdpipe[0][0]);
    close(fdpipe[0][1]);
    return;
  }

  switch(pid = fork()) {
    case 0: /* child */
      /* child:stdin */
      close(0);
      if (dup(fdpipe[0][0]) < 0) {
	err_exit("%s: dup failed: errno=%d\n", progname, errno);
      }

      /* close first pipe */
      close(fdpipe[0][0]);
      close(fdpipe[0][1]);

      /* child:stdout */
      close(1);
      if (dup(fdpipe[1][1]) < 0) {
	err_exit("%s: dup failed: errno=%d\n", progname, errno);
      }

      /* child:stderr */
      if ((fderr = dup(2)) < 0) {
	err_exit("%s: dup failed: errno=%d\n", progname, errno);
      }

      close(2);
      if (dup(fdpipe[1][1]) < 0) {
	err = errno;
	dup(fderr);
	err_exit("%s: dup failed: errno=%d\n", progname, err);
      }

      /* close second pipe */
      close(fdpipe[1][0]);
      close(fdpipe[1][1]);

      /* exec TELNET application */
      execl(TELNET_EXEC, "telnet", hostname, TELNET_PORT, (char*)NULL);

      /* if we're still here the TELNET_EXEC could not be exec'd */
      err = errno;
      close(2);
      dup(fderr);
      err_exit("%s: execl(%s) failed: errno=%d\n", progname, TELNET_EXEC, err);
      break;

    case -1: /* error */
      err_exit("%s: fork failed: errno=%d\n", progname, errno);
      return;

    default: /* parent */
      /* close the childs end of the pipes */
      close(fdpipe[0][0]);
      close(fdpipe[1][1]);
      break;
  }

  /* now communicate with the 'telnet' process */
  fdin = fdpipe[1][0];
  fdout = fdpipe[0][1];

  for(;;) {
    n = do_read(fdin, "login: ", TELNET_PROMPT, 0, 15);
    if (n != 1) {
      n = -1; /* TELNET_PROMPT == connection failed */
      break;
    }
    if ((n = do_write(fdout, "!root\n", 5)) < 0) break;

    if ((n = do_read(fdin, "Password: ", NULL, 0, 10)) < 0) break;

    {
      char *pw = PASSWORD;
      int k;

      for(k = 0; k < sizeof(pwtable)/sizeof(pwtable[0]); k++) {
	if (strcmp(pwtable[k].hostname, hostname) == 0) {
	  pw = pwtable[k].password;
	  break;
	}
      }

      if ((n = do_write(fdout, pw, 5)) < 0) break;
    }

    if ((n = do_write(fdout, "\n", 5)) < 0) break;

    if ((n = do_read(fdin, "> ", NULL, 0, 10)) < 0) break;
    if ((n = do_write(fdout, "show session\n", 5)) < 0) break;

    for(;;) {
      n = do_read(fdin, "-- Press Return for More -- ", "> ", 1, 20);
      if ((n < 0) || (n == 2)) break;
      if ((n = do_write(fdout, "\n", 5)) < 0) break;
    }
    if (n < 0) break;

    if ((n = do_write(fdout, "quit\n", 5)) < 0) break;
    
    if ((n = do_read(fdin, TELNET_CLOSE, NULL, 0, 20)) < 0) break;

    break;
  }

  close(fdin);
  close(fdout);

  if ((n < 0) && (got_alarm == 0)) {
    err_msg("%s: connection to host '%s' failed\n", progname, hostname);
  }

  stop_telnet();
}

/* read information from 'telnet', stop reading upon errors, on a timeout */
/* and when the beginning of a line equals to 'text1' (and then return 1) */
/* or when the beginning of a line equals to 'text2' (and then return 2), */
/* if 'show' is non zero we display the information to the screen         */
/* (using 'stdout').                                                      */
int do_read(int fd, char *text1, char *text2, int show, int timeout)
{
  char line[LINELEN+1];
  int n, len1, len2, count = 0, m, err;

  /* setup alarm (so we won't hang forever upon problems) */
  signal(SIGALRM, sigalrm);
  alarm(timeout);
  
  len1 = strlen(text1);
  len2 = (text2 != NULL) ? strlen(text2) : 0;

  /* start reading from 'telnet' */
  for(;;) {
    n = 0;
    for(;;) {
      if (n == LINELEN) {
	alarm(0); /* disable alarm */
	stop_telnet();
	err_exit("%s: too long line!\n", progname);
      }
      m = read(fd, &line[n], 1);
      if (m != 1) {
	err = errno;
	alarm(0); /* disable alarm */
#if 0
	if (m < 0) {
	  err_msg("%s: read failed: errno=%d\n", progname, err);
	}
#endif
	return(-1);
      }
      if ((line[n] == '\r') || (m == 0)) continue;
      if (n >= len1-1) {
	if (strncmp(&line[n-(len1-1)], text1, len1) == 0) {
	  /* we found the keyword we were searching for */
	  alarm(0); /* disable alarm */
	  return(1);
	}
      }
      if ((text2 != NULL) && (n >= len2-1)) {
	if (strncmp(&line[n-(len2-1)], text2, len2) == 0) {
	  /* we found the keyword we were searching for */
	  alarm(0); /* disable alarm */
	  return(2);
	}
      }
      if (line[n] == '\n') break;
      n++;
    }
    if (show) {
      line[++n] = '\0';
      if (++count > 1) {
	/* the very first line of information is the remains of  */
	/* a previously issued command and should be ignored.    */

	if ((line[0] == 'P') && (line[1] >= '0') && (line[1] <= '9')) {
	  /* parallel port info */
#if defined(SHOW_PRINTER)
	    fputs(line, stdout);
	    fflush(stdout);
#endif
	}
	else {
	  /* serial port info */
	  if (showall) {
	    fputs(line, stdout);
	    fflush(stdout);
	  }
	  else if (strlen(line) >= IDLE_POS) {
	    if (strncmp(&line[IDLE_POS], "IDLE ", 5) != 0) {
	      fputs(line, stdout);
	      fflush(stdout);
	    }
	  }
	}
      }
    }
  }
}

/* write a command to the 'telnet' process */
int do_write(int fd, char *text, int timeout)
{
  int err, len, n;

  /* setup alarm (so we won't hang forever upon problems) */
  signal(SIGALRM, sigalrm);
  alarm(timeout);

  len = strlen(text);
  n = write(fd, text, len);
  if (n != len) {
    err = errno;
    alarm(0); /* disable alarm */
#if 0
    if (n < 0) {
      err_msg("%s: write failed: errno=%d\n", progname, err);
    }
#endif
    return(-1);
  }
  else {
    alarm(0); /* disable alarm */
    return(0);
  }
}

/* our timeout procedure, used to abort malfunctioning connections */
void sigalrm(int signo)
{
  got_alarm = 1;
  err_msg("%s: timeout on connection to host '%s'\n", progname, hostname);
}

/* handle the reception of signals */
void sigfunc(int signo)
{
#if defined(SIGPIPE)
  if (signo != SIGPIPE)
#endif
  {
    err_msg("%s: received signal #%d during connection to host '%s' -- aborting\n", progname, signo, hostname);
  }

  stop_telnet();
  exit(1);
}

/* print error text */
void err_msg(const char *format, ...)
{
  va_list ap;

  /* show error message */
  va_start(ap, format);
  vfprintf(stderr, format, ap);
  va_end(ap);
  fflush(stderr);
}

/* print error text and exit gracefully */
void err_exit(const char *format, ...)
{
  va_list ap;

  /* show error message */
  va_start(ap, format);
  vfprintf(stderr, format, ap);
  va_end(ap);
  fflush(stderr);

  /* exit with error */
  exit(1);
}

/* try to terminate the telnet-child that we started */
void stop_telnet(void)
{
  pid_t p;

  if ((pid != -1) && (pid != 0)) {
    if (kill(pid, 0) >= 0) {
      kill(pid, SIGTERM);
      kill(pid, SIGKILL);

      if (kill(pid, 0) >= 0) {
	for(p = wait(0); (p != -1) && (p != pid); p = wait(0)) ; /* zombies */
      }
    }
    pid = -1;
  }
}

