/*
 * @(#) txfr.c  RCS: $Revision: 1.8 $ $Date: 95/03/08 15:46:54 $
 */
#if defined(_POSIX_SOURCE)
#undef _POSIX_SOURCE	/* otherwise <sys/types.h> won't define fd_set */
#endif
#include <sys/types.h>		/* BSD type fd_set FD_SET macros */
#include <sys/time.h>		/* BSD struct timeval */
#if !defined(_POSIX_SOURCE)	/* in case the above include files defined it */
#define _POSIX_SOURCE		/* yes, I know posix doesn't have select */
#endif
#include <unistd.h>		/* alarm read write */
#include <stdlib.h>		/* exit malloc */
#include <stdio.h>
#include <errno.h>		/* EINTR EAGAIN */
#include <signal.h>		/* SIGCHLD */
#include <string.h>		/* memset */
#include <fcntl.h>		/* O_NONBLOCK */
#include <sys/time.h>		/* select */
/*
 * 25-Jan-95: gl8f@fermi.clas.virginia.edu (Greg Lindahl)
 * 25-Jan-95: skrenta@pbm.com (Rich Skrent)
 */
#if defined(AIX) || defined(__DYNIX__) 	/* { */
#include <sys/select.h>		/* AIX 3.2.5 mainframe */
#endif 	/* } */
#include "posignal.h"
#include "log.h"
#include "auth.h"
#include "txfr.h"
#include "deslogin.h"

extern unsigned long inBytes, outBytes;

#if !defined(WRITE_RETRY_TIME)
#define WRITE_RETRY_TIME	1000	/* ms retry time on EAGAIN for write */
#endif

#if !defined(sparc) && !defined(sun3)
volatile 
#endif
unsigned gotalarm = 0;

/*
 * In POSIX, the read or write call will be interrupted by a signal.  If so, 
 * the read may return with -1 and errno=EINTR, or, if multi-byte, may return 
 * a result of the number of bytes read.
 */
void alarmCatcher(sig)
   int sig;
{
   gotalarm++;
}

#if !defined(sparc) && !defined(sun3)
volatile 
#endif
unsigned txfrChld = 0;

void chldCatcher(sig)
   int sig;
{
   txfrChld++;
   if (debug) {
      log("%s: (txfr) SIGCHLD #%d\n", progName, txfrChld);
   }
}


int recvData(fd, buf, size, cbuf, cs)
   int fd;
   char *buf, *cbuf;
   unsigned size;
   register cipherKey cs;
{
   register int rcount = read(fd, cbuf, size);

   if (debug > 2) {
      log("read(%d,%d)=%d(%d)\n", fd, size, rcount, errno);
   }
   if (rcount <= 0) {
      /*
       * Linux (and some other machines) always emit this message with
       * errno "I/O error" when the connection is shut down correctly.
       */
      if ((rcount < 0) && ((verbose) || (debug))) {
	 log("%s: (recv) read %d bytes fd %d failed--%s\n",
	     progName, size, fd, ERRMSG);
      }
      return rcount;
   }
   cipherData(buf, cbuf, rcount, cs);
   return rcount;
}

int sendData(fd, buf, size)
   int fd;
   char *buf;
   unsigned size;
{
   int wcount;

   wcount = write(fd, buf, size);
   if (debug > 2) {
      log("write(%d,%d)=%d(%d)\n", fd, size, wcount, errno);
   }
   /* 
    * We could get -1 and EAGAIN if a write is attempted for less than 
    * fpathconf(_PC_PIPE_BUF) and there isn't space yet.  In this case,
    * if we just returned immediately, we could chew up a lot of CPU time
    * with select continually returning true. Gag! Just pause for a 
    * WRITE_RETRY_TIME and return.
    */
   if (wcount == 0) {
      log("%s: (send) write %d bytes fd %d returned 0\n", progName, size, fd);
      exit(1);
   }
   if (wcount < 0) {
      if (errno != EAGAIN) {
	 log("%s: (send) write %d bytes fd %d failed--%s\n",
	    progName, size, fd, ERRMSG);
	 exit(1);
      }
      if (debug || verbose) {
	 log("%s: (send) write %d bytes fd %d busy; Retry in %lu msec\n",
	    progName, size, fd, WRITE_RETRY_TIME);
      }
      wcount = nap(WRITE_RETRY_TIME);
      if (wcount != 0) {
	 log("%s: (send) nap(%lu) failed--%s\n",
	    progName, WRITE_RETRY_TIME, ERRMSG);
	 exit(1);
      }
   }
   return wcount;
}

/*
 * Set the nonblocking flag for the given fd with error report.
 * returns the previous nonblock flag or -1 for falure (exits).
 */
int setnonblock(fd, flag)
   int fd, flag;
{
   int res, oldflag;

   oldflag = fcntl(fd, F_GETFL);
   if (oldflag < 0) {
      log("%s: (setnonblock) fcntl(%d) F_GETFL failed--%s\n", 
	 progName, fd, ERRMSG);
      exit(1);
   }
   if (flag) {
      res = fcntl(fd, F_SETFL, oldflag | O_NONBLOCK);
   } else {
      res = fcntl(fd, F_SETFL, oldflag & ~O_NONBLOCK);
   }
   if (res < 0) {
      log("%s: (setnonblock) fcntl(%d) F_SETFL failed--%s\n", 
	 progName, fd, ERRMSG);
      exit(1);
   }
   return ((oldflag & O_NONBLOCK) == O_NONBLOCK);
}

/*
 * Sleep for a specified number of milliseconds
 *
 * Returns: 0 if successful, -1 if unsuccessful
 */
int nap(ms)
   unsigned long ms;
{
   struct timeval to;
   register int res;

   to.tv_sec  = ms / 1000;
   to.tv_usec = (ms % 1000) * 1000;
   res = select(0, 0, 0, 0, &to);		/* wait a short while */
   return res;
}

/* 
 * Select on the master pty before the slave has a successful open behaves
 * totally differently on every machine I tried.  There appears to be no
 * machine independent way to do both of the following:
 *
 *  1) Determine when the slave has been opened
 *  2) Prevent multiple processes from opening the slave.
 *
 * If there was a way to do 2, what we should *really* do is to do a full
 * bore authentication protocal between the slave and tha master.  Since
 * there is no way to achieve 2 except file permissions on the slave pty,
 * there isn't any point; anyone that can break the file permissions can
 * intercept data between the slave pty and the process using it (which
 * will be the user shell, so we can't help here.
 * 
 * On DEC Alpha, select lies and returns true for read on the master pty when 
 * the slave hasn't opened it yet.  A subsequent read will fail errno=EIO.
 * It fails to return true for write at all.
 * It fails to return true for exception.
 *
 * On DEC Ultrix 4.3, select returns true for read on the master pty when 
 * the slave hasn't opened it yet.  A subsequent read will fail errno=EIO.
 * It also returns true for write. 
 * It also returns true for exception.
 *
 * Do whatever it takes to make sure that txfr won't abort before the slave
 * side of the pty has been opened.  Because select is so unpredicable before
 * the slave is open, we have to throw up our hands and poll until we don't
 * get an I/O error.  Since we can't poll transparently we have to have
 * the process on the slave side send us something with mkReady.
 *
 * Returns: 0 - if timeout
 *          1 - success
 *         <0 - failure (Could be from SIGCHLD)
 */
int waitReady(fd, buf, size, timeout) 
   int fd; 
   char *buf;
   unsigned size;
   unsigned timeout;
{
   pfv oldHandler;
   struct timeval to;
   int res = 0;

   gotalarm = 0;
   oldHandler = posignal(SIGALRM, alarmCatcher);
   alarm(timeout / 1000);
   while (!res && !gotalarm) {
      res = read(fd, buf, size);
      if (debug) {
	 log("%s: (waitReady) read=%d gotalarm=%d\n", progName, res, gotalarm);
      }
      if (res < 0) {
	 if (errno != EIO) {
	    break;
	 }
	 to.tv_sec  = 0;		/* some selects update to on return */
	 to.tv_usec = 500000;		/* 500 ms polling interval */
	 res = select(0, 0, 0, 0, &to);		/* wait a short while */
	 if (gotalarm) {
	    res = 0;	/* it was -1 from select being interrupted by signal */
	 } /* res could still be -1 if SIGCHLD */
      }
   }
   alarm(0);
   posignal(SIGALRM, oldHandler);
   return res;		/* res > 0 for success, < 0 failure, 0 timeout */
}

int mkReady(fd, buf, size, timeout) 
   int fd; 
   char *buf;
   unsigned size;
   unsigned timeout;
{
   pfv oldHandler;
   struct timeval to;
   int res = 0;

   gotalarm = 0;
   oldHandler = posignal(SIGALRM, alarmCatcher);
   alarm(timeout / 1000);
   while (!res && !gotalarm) {
      res = write(fd, buf, size);
      if (debug > 1) {
	 log("%s: (mkReady) write=%d gotalarm=%d\n", progName, res, gotalarm);
      }
      if (res < 0) {
	 if (errno != EIO) {
	    break;
	 }
	 to.tv_sec  = 0;		/* some selects update to on return */
	 to.tv_usec = 500000;		/* 500 ms polling interval */
	 res = select(0, 0, 0, 0, &to);		/* wait a short while */
	 if (gotalarm) {
	    res = 0;	/* it was -1 from select being interrupted by signal */
	 } 
      }
   }
   alarm(0);
   posignal(SIGALRM, oldHandler);
   return res;		/* res > 0 for success, < 0 failure, 0 timeout */
}

/*
 * Transfer data bidirectionally using a buffer of given size.
 * Returns when timeout (ms) expires (0 = timeout)
 *         or when eof (res = 1)
 *         or error (res = -1);
 *         or interrupt (res = -2) (or exception, slave closed pty)
 *
 * NOTE: there appears to be a bug in HP-UX where a read can return EAGAIN
 *       even if select returns true for both read and write on a tty fd.
 * On the DEC Alpha:
 *       If the slave side of a pty isn't open yet, the master side's select
 *       will return true for read, and the read call will fail with 
 *       EIO.
 */
int txfr(fd1, fdin, fdout, bufSize, timeout, key)
   int fd1, fdout, fdin;
   unsigned bufSize;
   unsigned timeout;
   keyType key;
{
   char 	*cbuf = (char *) malloc(bufSize);
   char 	*buf1 = (char *) malloc(bufSize); 
   char 	*bufin = (char *) malloc(bufSize); 
   struct timeval to, tov, *top = (struct timeval *) 0;	/* Not POSIX */
   unsigned     max_fd = fd1;
   fd_set       rmask, wmask, emask;
   int 	        result = -1, oldblockin = 0, oldblockout = 0, oldblock1 = 0;
   int		fdinEof = 0;
   cipherKey    cs1 = (cipherKey) 0, csin = (cipherKey) 0;
   pfv		oldChld;
   register int res, rcount1 = 0, rcountin = 0, wcount;
   register char *bufp1, *bufpin;

   if (fdout > max_fd) max_fd = fdout;
   if (fdin > max_fd) max_fd = fdin;

   if (buf1  == (char *) 0) goto txfr_done;
   if (bufin == (char *) 0) goto txfr_done;
   if (cbuf  == (char *) 0) goto txfr_done;

#if 0
   key = (keyType) 0;			/* for debug! */
#endif

   cs1  = mkCipherKey(key, 1);			/* decrypt */
   csin = mkCipherKey(key, 0);			/* encrypt */
   if (cs1   == (cipherKey) 0) goto txfr_done;
   if (csin  == (cipherKey) 0) goto txfr_done;

   bufp1 = buf1;
   bufpin = bufin;

   if (timeout != 0) {
      to.tv_sec  = (timeout / 1000);
      to.tv_usec = (timeout % 1000) * 1000;
      top = &tov;
   }

   /*
    * We must use non-blocking IO to ensure that write calls won't block.
    * Read calls are safe anyway.
    */
   oldblock1   = setnonblock(fd1, 1);
   oldblockin  = setnonblock(fdin, 1);		/* must be before fdout */
   oldblockout = setnonblock(fdout, 1);

   txfrChld = 0;
   oldChld = posignal(SIGCHLD, chldCatcher);
   if (oldChld == (pfv) -1) {
      log("%s: (txfr) sigaction SIGCHLD failed--%s\n", 
	 progName, ERRMSG);
      exit(1);
   }

   FD_ZERO(&emask); 
   do {
      FD_ZERO(&rmask); 	   FD_ZERO(&wmask); 	
      FD_SET(fd1, &emask); 
#if 0		/* we don't want exception checking on tty or files */
      FD_SET(fdout, &emask); FD_SET(fdin, &emask);
#endif
      if (rcount1  != 0) FD_SET(fdout, &wmask); else {
	 FD_SET(fd1,  &rmask); 
      }
      if (rcountin != 0) FD_SET(fd1,   &wmask); else if (!fdinEof) {
	 FD_SET(fdin, &rmask);
      }

      if (top != (struct timeval *) 0) {
	 tov.tv_sec  = to.tv_sec;
	 tov.tv_usec = to.tv_usec;
      }

      if (debug > 2) {
	 log("select(%u,%u,%u,%u)\n", max_fd+1,
	    *(unsigned *)&rmask, *(unsigned *)&wmask, *(unsigned *)&emask);
      }
      if (txfrChld != 0) {		/* must be right next to select */
	 break;
      }	/* race condition right here */
      /*
       * HP-UX incorrectly declares select with int * args instead of fd_set
       * Ignore the warnings for select for HP-UX.
       */
      res = select(max_fd+1, &rmask, &wmask, &emask, top);
      if (debug > 2) {
	 log("select returned: %u(%u,%u,%u)\n", res,
	    *(unsigned *)&rmask, *(unsigned *)&wmask, *(unsigned *)&emask);
      }
      if (res == 0) {			/* timeout */
	 result = 0;
	 break;
      }
      if (res < 0) {			/* error */
	 if (errno == EINTR) {		/* signal (SIGCHLD occurred) */
	    result = -2;
	    break;
	 } else {
	    log("%s: select failed--%s\n", progName, ERRMSG);
	    break;
	 }
      }

      /*
       * This block will never be triggered because there is no way for a 
       * close on a slave pty to be detected on non-hpux systems.
       */
      if (FD_ISSET(fd1, &emask) 
#if 0
      || FD_ISSET(fdout, &emask) 
      || FD_ISSET(fdin, &emask)
#endif
      ) {	/* close */
	 result = -2;
	 break;
      }

      if (FD_ISSET(fd1, &rmask)) {
	 rcount1 = recvData(fd1, buf1, bufSize, cbuf, cs1);
	 if (rcount1 <= 0) {
	    if (rcount1 == 0) {
	       result = 1;
	       break;
	    } else {
	       result = -1;
	       break;
	    }
	 }
	 bufp1    = buf1;
	 inBytes += rcount1;
      }
      if (FD_ISSET(fdin, &rmask)) {
	 rcountin = recvData(fdin, bufin, bufSize, cbuf, csin);
	 if (rcountin <= 0) {
	    if (rcountin == 0) {
	       fdinEof = 1;
	    } else {
	       result = -1;
	       break;
	    }
	 }
	 bufpin    = bufin;
	 outBytes += rcountin;
      }

      if (FD_ISSET(fd1, &wmask)) {
	 wcount    = sendData(fd1, bufpin, rcountin);
	 rcountin -= wcount;
	 bufpin   += wcount;
      }
      if (FD_ISSET(fdout, &wmask)) {
	 wcount   = sendData(fdout, bufp1, rcount1);
	 rcount1 -= wcount;
	 bufp1   += wcount;
      }
   } while (txfrChld == 0);
   posignal(SIGCHLD, oldChld);
   setnonblock(fd1, oldblock1);
   setnonblock(fdin, oldblockin);
   /*
    * We must blindly assume that we don't want non-blocking I/O when whe
    * leave.  The reason is that two fd's may actually be connected to the
    * same device.  If this is the case, then it's possible to switch the
    * nonblocking mode incorrectly.  (e.g. setting NON_BLOCK in fdin also
    * changes it on fdout)
    */
   setnonblock(fdout, oldblockin);	/* oldblockin is not a bug */
txfr_done:
   destroyCipherKey(&csin);
   destroyCipherKey(&cs1);
   free(bufin);
   free(buf1);
   free(cbuf);
   return result;
}
/*
 * Input a string of upto size chars with timout terminated by CR LF or NULL
 * Input:
 *     size - the maximum number of characters to read
 *
 * Output:
 *     *buf - The characters read not including terminator
 *            A '\0' character is always added to the end of buf
 *
 * Returns: the number of characters read (not including terminator)
 *          This is true even if we were interrupted by an alarm.
 */
int getString(fd, buf, size, timeout)
   int fd;
   char *buf;
   unsigned size;
   unsigned timeout;
{
   register unsigned count = size;
   register char *chp = buf;
   char ch;
   register unsigned seconds = (unsigned) ((timeout + (1000L-1)) / 1000L);
   register int rcount;
   pfv oldHandler;

   gotalarm = 0;
   oldHandler = posignal(SIGALRM, alarmCatcher);
   alarm(seconds);
   while (count != 0) {
      rcount = read(fd, &ch, 1);
      if (rcount <= 0 || gotalarm != 0) break;
      if (ch == '\r' || ch == '\n' || ch == '\0') break;
      *chp   = ch;
      count -= rcount;
      chp   += rcount;
   }
   alarm(0);
   posignal(SIGALRM, oldHandler);

   *chp = '\0';
   return size - count;
}

/*
 * Read a block of binary data of the given size.  
 * Return the number of bytes read (0 if timeout, or read error).
 */
int getBlock(fd, buf, size, timeout)
   int fd;
   char *buf;
   unsigned size;
   unsigned timeout;
{
   register unsigned seconds = (unsigned) ((timeout + (1000-1)) / 1000);
   register int rcount;
   pfv oldHandler;

   gotalarm = 0;
   oldHandler = posignal(SIGALRM, alarmCatcher);
   alarm(seconds);
   rcount = read(fd, buf, size);
   alarm(0);
   posignal(SIGALRM, oldHandler);

   /*
    * No error checking is reported here. If we got a a signal, we could get
    * either -1/errno=EINTR, or a short read, depending upon implementation.
    */
   if (rcount < 0) {
      rcount = 0;
   }
   return rcount;
}

/*
 * Encipher all the data from sfd to dfd.  Send the entire shmeel in timeout
 * seconds, abort if longer.  This routine is used for sending a nologin
 * message contained in the file /etc/nologin.
 */
int cipherCopy(dfd, sfd, timeout, key)
   int dfd, sfd;
   unsigned timeout;
   keyType key;
{
   int 		rcount, wcount = 0;
   char 	buf[128], cbuf[128]; 
   register char *bufp = buf;
   pfv 		oldHandler;
   cipherKey 	cs = mkCipherKey(key, 0);			/* encrypt */

   if (cs == (cipherKey) 0) return -1;

   gotalarm = 0;
   oldHandler = posignal(SIGALRM, alarmCatcher);
   alarm(timeout);

   do {
      while ((rcount = read(sfd, bufp, 1)) > 0) {
	 if (*bufp == '\n') {
	    *bufp++ = '\r';
	    *bufp   = '\n';
	 }
	 bufp++;
	 if (bufp >= buf + 127 || gotalarm != 0) break;
      }
      if (gotalarm != 0) break;
      rcount = bufp - buf;
      bufp   = buf;
      cipherData(cbuf, buf, rcount, cs);
      wcount = write(dfd, cbuf, rcount);
      if (wcount <= 0) break;
   } while (gotalarm == 0);

   alarm(0);
   posignal(SIGALRM, oldHandler);

   destroyCipherKey(&cs);

   if (rcount < 0 || wcount < 0) return -1;
   return 0;
}
