/* 	$Id: newstartd.c,v 1.6 1999/02/03 01:46:23 cameron Exp $	 */

#ifndef lint
static char vcid[] = "$Id: newstartd.c,v 1.6 1999/02/03 01:46:23 cameron Exp $";
#endif /* lint */

/*
 *   There's a really bad hack in newstartd.c that refuses more than 1
 *   connection from any hostname.  It's bad because it uses /usr/ucb/netstat
 *   to figure out who's connected.
 *
 *   At this point, only the hostname of the caller is available.  The
 *   daemon in theory has access to both user name and (partial) hostname,
 *   so that's where multiple logins should be denied.
 *
 *   To bypass this check, create the file defined by NoCount_File in
 *   newaccess.c.  Usually .nocount in LIBDIR.  Set by default normally.
 *
 *   4/13/92
 *
 *   Derived from distribution startd.c and xtrekII.sock.c (as of ~1/14/91).
 *
 *   Installation:  update the Prog and LogFile variables below, and
 *     set the default port appropriately (presently 2592).  Set debug=1
 *     to run this under dbx.  Link this with access.o and subnet.o.
 *  
 *   Summary of changes:
 *   - subsumes xtrekII.sock into startd (newstartd calls ntserv directly to
 *       reduce OS overhead)
 *   - some debugger support code added
 *   - uses reaper() to keep process table clean (removes <exiting> procs)
 *
 *   8/16/91 Terence Chang
 *
 */

#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <netdb.h>
#include <sys/ioctl.h>
#include <sys/file.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/stat.h>

#include "../ntserv/defs.h"
#include INC_STRINGS
#include INC_FCNTL
#include "../ntserv/data.h"

int restart;		/* global flag, set by SIGHUP, cleared by read	*/
int debug = 0;		/* programmers' debugging flag			*/

int get_connection();
int read_portfile();
void deny();
void statistics (int, int);
void process(int);
void reaper(int);
void hangup(int);
void putpid();
 
#define SP_BADVERSION 21
struct badversion_spacket {
  char type;
  char why;
  char pad2;
  char pad3;
};

#define PORTFILE LIBDIR"/.ports"	/* default .ports file path	*/
#define MAXPROG 16			/* maximum ports to listen to	*/

static
struct progrecord {			/* array of data read from file	*/
  unsigned short port;			/* port number to listen on	*/
  int sock;				/* socket created on port or -1	*/
  int nargs;				/* number of arguments to prog	*/
  char prog[512];			/* program to start on connect	*/
  char progname[512];			/* more stuff for exec()	*/
  char arg[4][64];			/* arguments for program	*/
  int internal;				/* ports handled without fork()	*/
  int accepts;				/* count of accept() calls	*/
  int denials;				/* count of deny() calls	*/
  int forks;				/* count of fork() calls	*/
} prog[MAXPROG];

extern char peerhostname[];		/* defined in newaccess.c	*/
int fd;					/* log file file descriptor	*/

int main (int argc, char *argv[])
{
  char *portfile = PORTFILE;
  int port_idx = -1;
  int num_progs = 0;
  int i;
  
  getpath ();

  /* if someone tries to ask for help, give 'em it */
  if (argc == 2 && argv[1][0] == '-') {
    fprintf (stderr, "Usage: %s [portfile]\n", argv[0] );
    exit (1);
  }

  /* allow user to specify a port file to use */
  if (argc == 2) portfile = argv[1];

  /* set up handlers for signals */
  SIGNAL (SIGCHLD, reaper);
  SIGNAL (SIGHUP, hangup);

  /* detach from terminal */
  if (!debug) {
    DETACH
  }

  /* open the connection log file */
  if ((fd = open (LogFile, O_CREAT | O_WRONLY | O_APPEND, 0600)) < 0) {
    perror ("open log");
  }

  /* record our process id and announce ourselves */
  putpid ();
  fprintf (stderr, "%s\n", vcid );
  fprintf (stderr, "using %s\n", portfile );

  /* run forever */
  while (1) {

    /* [re-]read the port file */
    num_progs = read_portfile (portfile);
    restart = 0;

    /* run until restart requested */
    while (!restart) {

      /* wait for one connection */
      port_idx = get_connection (num_progs);
      if (port_idx < 0) continue;
      prog[port_idx].accepts++;

      /* check this client against denied parties */
      if (host_access (NULL) == 0) {
        prog[port_idx].denials++;
	deny ();
	close (0);
	continue;
      }

      /* check for internals */
      if (prog[port_idx].internal) {
	statistics (port_idx, num_progs);
	continue;
      }

      prog[port_idx].forks++;
      /* fork to execute the program specified by .ports file */
      if (fork () == 0) {
	process (port_idx);		/* returns only on error */
	exit (0);
      }

      /* main program continues after fork, close socket to client */
      close (0);

    } /* while (!restart) */
    
    /* SIGHUP restart requested, close all listening sockets */
    fprintf (stderr, "newstartd restarting on SIGHUP\n");

    for (i = 0; i < num_progs; i++) {
      if (prog[i].sock >= 0) close(prog[i].sock);
    }

    /* between closing the listening sockets and commencing the new ones
       there is a time during which incoming client connections are refused */
  }
}

int get_connection (int num_progs)
{
  struct sockaddr_in addr;
  struct sockaddr_in naddr;
  struct timeval timeout;
  int readfds;
  fd_set accept_fds;
  int len, i, st, newsock;
  
  char *opt;
  int foo = 1;
  
  /* stage one; for each port not yet open, set up listening socket */

  /* check all ports */
  for (i = 0; i < num_progs; i++) {
    sock = prog[i].sock;
    if (sock < 0) {
      if ((sock = socket (AF_INET, SOCK_STREAM, 0)) < 0) {
	perror ("socket");
	sleep (1);
	return -1;
      }
      
      /* set the listening socket to close on exec so that children
	 don't hold open the file descriptor, thus preventing us from
	 restarting newstartd. */
      
      if (fcntl(sock, F_SETFD, FD_CLOEXEC) < 0) {
	perror ("fcntl F_SETFD FD_CLOEXEC failure");
      }

      addr.sin_family = AF_INET;
      addr.sin_addr.s_addr = INADDR_ANY;
      addr.sin_port = htons (prog[i].port);
      
      if (setsockopt (sock, SOL_SOCKET, SO_REUSEADDR,
		      (char *) &foo, sizeof (int)) < 0) {
	perror ("setsockopt REUSEADDR");
	return -1;
      }
      
      if (bind (sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
	if (errno == ENOTSOCK) {
	  perror ("bind");
	  close (sock);
	  prog[i].sock = -1;
	  continue;
	}
	if (debug)
	  fprintf (stderr, "Taking 10...\n");
	sleep (10);
	if (bind (sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
	  perror ("bind");
	  if (debug)
	    fprintf (stderr, "Taking 10...\n");
	  sleep (10);
	  if (bind (sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
	    perror ("bind");
	    close (sock);
	    prog[i].sock = -1;
	    continue;
	  }
	}
      }
      if (listen (sock, 1) < 0) {
	perror ("listen");
	sock = -1;
      }
      prog[i].sock = sock;
      fprintf (stderr,
	       "listening on %d, connection to start %s \"%s\" %s %s %s %s\n",
	       prog[i].port, prog[i].prog, prog[i].progname, prog[i].arg[0],
	       prog[i].arg[1], prog[i].arg[2], prog[i].arg[3]);
      fflush (stderr);
    }
  }

  /* stage two; wait for a connection on the listening sockets */
  
  while (1) {				/* wait for a connection */
    FD_ZERO (&accept_fds);		/* clear the file descriptor mask */
    for (i = 0; i < num_progs; i++) {	/* set bits in mask for each sock */
      sock = prog[i].sock;
      if (sock < 0) continue;
      FD_SET (sock, &accept_fds);
    }
    
    /* block until select() indicates accept() will complete */
    st = select (32, &accept_fds, 0, 0, 0);
    if (st > 0) break;
    if (restart) return -1;
  }

  /* find socket in mask that shows activity */
  for (i = 0; i < num_progs; i++) {
    sock = prog[i].sock;
    if (sock < 0) continue;
    if (FD_ISSET (sock, &accept_fds)) break;
  }

  /* none of the sockets showed activity? erk */
  if (i >= num_progs) return -1;

  len = sizeof(naddr);
    
  /* hack: to support reaper(), must ignore EINTR errors (TC) */

nointr:
  newsock = accept (sock, (struct sockaddr*)&naddr, &len);
  if ((newsock < 0) && (errno == ECONNRESET)) {
    fprintf (stderr,"Connection reset! Socket %d (%d)\n", sock, newsock);
    sleep (1);
    goto nointr;
  }
  if ((newsock < 0) && (errno == ETIMEDOUT)) {
    fprintf (stderr,"Connection timeout! Socket is %d (%d)\n", sock, newsock);
    sleep (1);
    goto nointr;
  }
  if ((newsock < 0) && (errno == EINTR))
    goto nointr;

  if (newsock < 0) {
    perror ("accept");
    shutdown (sock, 2);
    close (sock);
    prog[i].sock = -1;
    return -1;
  } else {
    if (newsock != 0) {
      if (dup2 (newsock, 0) == -1) {
	perror ("dup2");
      }
      close (newsock);
    }
    return i;
  }
}

void deny ()
{
  struct badversion_spacket packet;

  /* if log is open, write an entry to it */
  if (fd != -1) {
    time_t curtime;
    char logname[BUFSIZ];

    time (&curtime);
    sprintf (logname, "Denied %-32.32s %s",
	     peerhostname,
	     ctime(&curtime));
    write (fd, logname, strlen(logname));
  }

  /* issue a bad version packet to the client */
  packet.type = SP_BADVERSION;
  packet.why = 1;
  write (0, (char *) &packet, sizeof(packet));

  sleep (2);
}

void statistics (int port_idx, int num_progs)
{
  FILE *client;
  int i;

  if (fork() == 0) {
    client = fdopen (0, "w");
    if (client != NULL) {
      if (!strcmp (peerhostname, "localhost")) {
	fprintf (client, "port\taccepts\tdenials\tforks\n" );
	for (i=0; i<num_progs; i++) {
	  fprintf (client,
		   "%d\t%d\t%d\t%d\n",
		   prog[i].port,
		   prog[i].accepts,
		   prog[i].denials,
		   prog[i].forks );
	}
      }
      fprintf (client, "\n%s\n", vcid);
      fflush (client);
      if (fclose (client) < 0) perror("fclose");
    } else {
      close (0);
    }
    exit (1);
  } else {
    close (0);
  }
}

void process (int port_idx)
{
  struct progrecord *pr;

  /* note: we are a clone */

  /* if log is open, write an entry to it, and close it */
  if (fd != -1) {
    time_t curtime;
    char logname[BUFSIZ];

    time (&curtime);
    sprintf (logname, "       %-32.32s %s",
	     peerhostname,
	     ctime(&curtime));
    write (fd, logname, strlen(logname));
    close (fd);
  }
  
  /* execute the program required */
  pr = &(prog[port_idx]);
  switch (pr->nargs) {
  case 0:
    execl (pr->prog, pr->progname, peerhostname, 0);
    break;
  case 1:
    execl (pr->prog, pr->progname, pr->arg[0], peerhostname, 0);
    break;
  case 2:
    execl (pr->prog, pr->progname, pr->arg[0], pr->arg[1], 
	   peerhostname, 0);
    break;
  case 3:
    execl (pr->prog, pr->progname, pr->arg[0], pr->arg[1], 
	   pr->arg[2], peerhostname, 0);
    break;
  case 4:
    execl (pr->prog, pr->progname, pr->arg[0], pr->arg[1], 
	   pr->arg[2], pr->arg[3], peerhostname, 0);
    break;
  default: ;
  }
  fprintf (stderr, "Error in execl! -- %s\n", pr->prog);
  perror ("execl");
  close (0);
  exit (1);
}

/* set flag requesting restart, select() will get EINTR */
void hangup (int sig)
{
  restart++;
  HANDLE_SIG (SIGHUP, hangup);
}

/* accept termination of any child processes */
void reaper (int sig)
{
  int stat;
  if (debug) fprintf (stderr, "Reaping...\n");
  while (WAIT3 (&stat, WNOHANG, 0) > 0)
    ;
  HANDLE_SIG (SIGCHLD, reaper);
}

/* write process id to file to assist with automatic signalling */
void putpid ()
{
  FILE *file;

  file = fopen(LIBDIR"/.newstartd.pid", "w");
  if (file == NULL) return;
  fprintf (file, "%d\n", (int) getpid());
  fclose (file);
}

int read_portfile (char *portfile)
{
  FILE *fi;
  char buf[BUFSIZ];
  int i = 0, p, n;
  struct stat sbuf;

  fi = fopen (portfile, "r");
  if (fi) {
    while (fgets (buf, BUFSIZ - 1, fi)) {
      if ((n = sscanf (buf, "%d %s \"%[^\"]\" %s %s %s %s", &p,
		       prog[i].prog,
		       prog[i].progname,
		       prog[i].arg[0],
		       prog[i].arg[1],
		       prog[i].arg[2],
		       prog[i].arg[3])) >= 3) {
	prog[i].nargs = n-3;
	prog[i].port = (unsigned short) p;
	prog[i].sock = -1;
	prog[i].internal = (!strcmp (prog[i].prog, "special"));
	prog[i].accepts = 0;
	prog[i].denials = 0;
	prog[i].forks = 0;

	/* check normal entries to make sure program mode is ok */
	if ((!prog[i].internal) &&
	    (stat (prog[i].prog, &sbuf) < 0 || !(sbuf.st_mode & S_IRWXU))) {
	  fprintf (stderr, "\"%s\" not accessible or not executable.\n",
		   prog[i].prog);
	  fflush (stderr);
	} else {
	  i++;
	}
      }
    }
    fclose (fi);
  } else {
    perror (portfile);
  }
  
  /* if the .ports file is empty, at least open the standard port */
  if (!i) {
    strcpy (prog[0].prog, Prog);
    strcpy (prog[0].progname, Prog);
    prog[0].port = PORT;
    prog[0].nargs = 0;
    prog[0].sock = -1;
    prog[0].accepts = 0;
    prog[0].denials = 0;
    prog[0].forks = 0;
    i = 1;
  }

  return i;
}
