/* cpbidir.c */

/* Copyright 1999-2000 by Eberhard Mattes <em-gw@windhager.de>
   Donated to the public domain.  No warranty.

   1999-03-17 Initial version
   2000-05-31 Add FLAGS
   2000-07-07 <fcntl.h> */

#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <syslog.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/file.h>
#include <sys/socket.h>
#include "firewall.h"
#include "libemfw.h"
#include "emio.h"

/* Bidirectionally copy between two file descriptors without risking
   deadlock. */

#include <stdlib.h>
#include <string.h>
#include "libemfw.h"
#include "emio.h"

#define CHANNEL_BUFSIZ  4096

#define CB_SHUTDOWN	(CB_SHUTDOWN1 | CB_SHUTDOWN2)
#define CB_EOF		(CB_EOF1 | CB_EOF2)

#define CB1		(CB_SHUTDOWN1 | CB_EOF1)
#define CB2		(CB_SHUTDOWN2 | CB_EOF2)

struct channel
{
  int fdr, fdw;
  int count, eof;
  int emi_active;
  unsigned flags;
  EMI_FILE *emi;
  char buf[CHANNEL_BUFSIZ];
};

static int channel_init (struct channel *c, EMI_FILE *emi, int fdr, int fdw,
			 unsigned flags)
{
  int fl;

  c->fdr = fdr; c->fdw = fdw;
  c->count = 0; c->eof = 0;
  c->emi = emi;
  c->emi_active = (emi != NULL && emi->ptr != emi->end);
  c->flags = flags;
  fl = fcntl (fdw, F_GETFL, 0);
  if (fl != -1)
    fl = fcntl (fdw, F_SETFL, fl | O_NDELAY);
  if (fl == -1)
    {
      int e = errno;
      syslog (LLEV, "copy_bidir: fcntl: %s", strerror(errno));
      errno = e;
      return -1;
    }
  return 0;
}

static void channel_fdset (const struct channel *c, fd_set *rfds, fd_set *wfds)
{
  if (!c->eof && !c->emi_active && c->count < CHANNEL_BUFSIZ)
    FD_SET (c->fdr, rfds);
  if (c->count > 0 || c->emi_active)
    FD_SET (c->fdw, wfds);
}

static int channel_maybe_shutdown (struct channel *c)
{
  if (c->eof && c->count == 0 && !c->emi_active && (c->flags & CB_SHUTDOWN))
    {
      /* No further output */
      if (shutdown (c->fdw, 1) != 0 && errno != ENOTSOCK)
	{
	  int e = errno;
	  syslog (LLEV, "copy_bidir: shutdown: %s", strerror(errno));
	  errno = e;
	  return -1;
	}
    }
  return 0;
}

static int channel_read (struct channel *c, fd_set *rfds)
{
  int n;

  if (c->emi_active)
    {
      EMI_FILE *emi = c->emi;
      int buffered = (int) (emi->end - emi->ptr);
      int n = CHANNEL_BUFSIZ - c->count;

      DEBUG_ASSERT (buffered > 0); /* emi_active! */
      if (n > buffered)
	n = buffered;
      if (n > 0)
	{
	  memcpy (c->buf + c->count, emi->ptr, n);
	  emi->ptr += n; emi->amount += n;
	  c->count += n;
	  if (emi->ptr == emi->end)
	    c->emi_active = 0;
	}
    }
  else if (FD_ISSET (c->fdr, rfds))
    {
      n = read (c->fdr, c->buf + c->count, CHANNEL_BUFSIZ - c->count);
      if (n == -1)
	{
	  int e = errno;
	  syslog (LLEV, "copy_bidir: read: %s", strerror(errno));
	  errno = e;
	  return -1;
	}
      if (n == 0)
	{
	  c->eof = 1;
	  if (channel_maybe_shutdown (c) != 0)
	    return -1;
	}
      c->count += n;
      if (c->emi != NULL)
	c->emi->amount += n;
    }
  return 0;
}

static int channel_write (struct channel *c, fd_set *wfds)
{
  int n;

  if (FD_ISSET (c->fdw, wfds))
    {
      n = write (c->fdw, c->buf, c->count);
      if (n == -1)
        {
	  int e = errno;
          syslog (LLEV, "copy_bidir: write: %s", strerror(errno));
	  errno = e;
          return -1;
        }
      if (n < c->count)
        memmove (c->buf, c->buf + n, c->count - n);
      c->count -= n;
      if (channel_maybe_shutdown (c) != 0)
	return -1;
    }
  return 0;
}

/* Any more data to be processed?  (This function ignores CB_EOF.) */

static int channel_more (const struct channel *c)
{
  return !c->eof || c->count > 0 || c->emi_active;
}

/* Channel terminated?  (This function uses CB_EOF.) */

static int channel_eof (const struct channel *c)
{
  return (c->flags & CB_EOF) && !channel_more (c);
}

static int get_fd (int *fd, EMI_FILE *emi)
{
  if (emi != NULL)
    {
      if (emi->flags & EMI_FLAG_ERROR)
	{
	  syslog (LLEV, "copy_bidir: get_fd: EMI_FLAG_ERROR");
	  errno = EINVAL;
	  return -1;
	}
      if (*fd != -1 && *fd != emi->fd)
	{
	  syslog (LLEV, "copy_bidir: get_fd: file descriptor mismatch");
	  errno = EINVAL;
	  return -1;
	}
      *fd = emi->fd;
    }
  return 0;
}

int copy_bidir_emi (EMI_FILE *emi1, int fd1, EMI_FILE *emi2, int fd2,
		    int timeout, unsigned flags)
{
  struct channel c1, c2;
  fd_set rfds, wfds;
  struct timeval tv;
  int n, maxfd;

  if (get_fd (&fd1, emi1) != 0) return -1;
  if (get_fd (&fd2, emi2) != 0) return -1;
  maxfd = fd1 > fd2 ? fd1 : fd2;
  if (maxfd >= FD_SETSIZE)
    {
      syslog (LLEV, "copy_bidir: FD_SETSIZE exceeded");
      errno = EINVAL;
      return -1;
    }
  if (channel_init (&c1, emi1, fd1, fd2, flags & CB1) != 0) return -1;
  if (channel_init (&c2, emi2, fd2, fd1, flags & CB2) != 0) return -1;
  while (channel_more (&c1) || channel_more (&c2))
    {
      if (channel_eof (&c1) || channel_eof (&c2))
	break;			/* CB_EOF */
      FD_ZERO (&rfds); FD_ZERO (&wfds);
      channel_fdset (&c1, &rfds, &wfds);
      channel_fdset (&c2, &rfds, &wfds);
      tv.tv_sec = timeout;
      tv.tv_usec = 0;
      n = select (maxfd + 1, &rfds, &wfds, (fd_set *)0, &tv);
      if (n == 0)
	return 1;		/* Timeout */
      if (n < 0)
        {
	  int e = errno;
          syslog (LLEV, "copy_bidir: select: %s", strerror(errno));
	  errno = e;
          return -1;
        }
      if (channel_read (&c1, &rfds) != 0) return -1;
      if (channel_read (&c2, &rfds) != 0) return -1;
      if (channel_write (&c1, &wfds) != 0) return -1;
      if (channel_write (&c2, &wfds) != 0) return -1;
    }
  return 0;
}

int copy_bidir (int fd1, int fd2, int timeout, unsigned flags)
{
  return copy_bidir_emi (NULL, fd1, NULL, fd2, timeout, flags);
}
