// Copyright (C) 2008 Zack Weinberg <zackw@panix.com>
//
// This program is made available under the GNU GPL version 2.0 or
// greater. See the accompanying file COPYING for details.
//
// This program is distributed WITHOUT ANY WARRANTY; without even the
// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE.

/* The test suite runs this program to decide whether or not to include
   network tests.  It determines whether it is possible to create a
   listening socket on a randomly chosen port on the loopback interface,
   connect to that socket from another process, and ping-pong a byte.

   Will exit successfully, with no output, if everything works; otherwise,
   will exit unsuccessfully and produce diagnostics on stderr.  */

#include "config.h"

#include <cstdio>

#if defined HAVE_SOCKET && defined HAVE_NETINET_IN_H

#include <cstdlib>
#include <cstring>
#include <csignal>
#include <cerrno>
#include <ctime>

#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#include <sys/socket.h>
#include <netinet/in.h>

static int synchronizer[2];
static const char *who;
static unsigned short port;

static void sigalrm(int unused)
{
  fprintf(stderr, "%s: timeout\n", who);
  exit(1);
}

static void prep_timeout(const char *w)
{
  who = w;
  signal(SIGALRM, sigalrm);
  alarm(5);
}

/* "b_or_c" should be either "bind" or "connect".  Conveniently, they have
   the same signature.  */
static int get_socket(int (*b_or_c)(int, const struct sockaddr *, socklen_t))
{
  int sfd;

  /* try IPv4 first */
  {
    struct sockaddr_in sin;
    memset(&sin, 0, sizeof sin);
    sin.sin_family = AF_INET;
    sin.sin_port = htons(port);
    sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);

    sfd = socket(PF_INET, SOCK_STREAM, 0);
    if (sfd >= 0)
      {
        if (b_or_c(sfd, (struct sockaddr *)&sin, sizeof sin) == 0)
          return sfd;
        close(sfd);
      }
  }

  /* if that didn't work, and we have library support for it, try IPv6 too */
#ifdef AF_INET6
  {
    struct sockaddr_in6 sin6;
    memset(&sin6, 0, sizeof sin6);
    sin6.sin6_family = AF_INET6;
    sin6.sin6_port = htons(port);
    sin6.sin6_addr = in6addr_loopback;

    sfd = socket(PF_INET6, SOCK_STREAM, 0);
    if (sfd >= 0)
      {
        if (b_or_c(sfd, (struct sockaddr *)&sin6, sizeof sin6) == 0)
          return sfd;
        close(sfd);
      }
  }
#endif

  fprintf(stderr, "socket/connect/bind: %s\n", strerror(errno));
  return -1;
}

static int server(void)
{
  int sfd, cfd, n;
  char buf;

  prep_timeout("server");

  sfd = get_socket(bind);
  if (sfd < 0)
    return 1;

  if (listen(sfd, 1))
    {
      fprintf(stderr, "server: listen: %s\n", strerror(errno));
      close(sfd);
      return 1;
    }

  /* Client process may proceed.  */
  n = write(synchronizer[1], "x", 1);
  if (n != 1)
    {
      fprintf(stderr, "server: semaphore write: %s\n",
              n == 0 ? "unexpected EOF" : strerror(errno));
      close(sfd);
      return 1;
    }

  cfd = accept(sfd, 0, 0);  /* don't care _who_ connects */
  if (cfd < 0)
    {
      fprintf(stderr, "server: accept: %s\n", strerror(errno));
      close(sfd);
      return 1;
    }

  n = read(cfd, &buf, 1);
  if (n != 1)
    {
      fprintf(stderr, "server: socket read: %s\n",
              n == 0 ? "unexpected EOF" : strerror(errno));
      close(cfd);
      close(sfd);
      return 1;
    }
  if (buf != 'x')
    {
      fprintf(stderr, "server: socket read: got '%c' exp 'x'\n", buf);
      close(cfd);
      close(sfd);
      return 1;
    }
  n = write(cfd, "x", 1);
  if (n != 1)
    {
      fprintf(stderr, "server: socket write: %s\n",
              n == 0 ? "unexpected EOF" : strerror(errno));
      close(cfd);
      close(sfd);
      return 1;
    }

  close(cfd);
  close(sfd);
  return 0;
}

static int client(void)
{
  int sfd, n;
  char buf;

  prep_timeout("client");

  /* wait for server setup */
  n = read(synchronizer[0], &buf, 1);
  if (n != 1)
    {
      fprintf(stderr, "client: semaphore read: %s\n",
              n == 0 ? "unexpected EOF" : strerror(errno));
      return 1;
    }

  sfd = get_socket(connect);
  if (sfd < 0)
    return 1;

  n = write(sfd, "x", 1);
  if (n != 1)
    {
      fprintf(stderr, "client: socket write: %s\n",
              n == 0 ? "unexpected EOF" : strerror(errno));
      close(sfd);
      return 1;
    }

  n = read(sfd, &buf, 1);
  if (n != 1)
    {
      fprintf(stderr, "client: socket read: %s\n",
              n == 0 ? "unexpected EOF" : strerror(errno));
      close(sfd);
      return 1;
    }
  if (buf != 'x')
    {
      fprintf(stderr, "client: socket read: got '%c' exp 'x'\n", buf);
      close(sfd);
      return 1;
    }

  close(sfd);
  return 0;
}

int main(void)
{
  pid_t child, p;
  int status;

  /* Pick a random port in the high half of the range, thus
     unlikely to be used for anything.  */
  srand(time(0));
  do
    {
      port = rand();
    }
  while (port < 32767);

  if (pipe(synchronizer))
    {
      fprintf(stderr, "setup: pipe: %s\n", strerror(errno));
      return 2;
    }

  child = fork();
  if (child < 0)
    {
      fprintf(stderr, "setup: fork: %s\n", strerror(errno));
      return 2;
    }

  if (child == 0)
    return client();

  if (server())
    return 1;

  p = wait(&status);
  if (p < 0)
    {
      fprintf(stderr, "teardown: wait: %s\n", strerror(errno));
      return 2;
    }
  if (p != child)
    {
      fprintf(stderr, "teardown: unexpected child %ld != %ld\n",
              (long int) p, (long int) child);
      return 2;
    }
  if (!WIFEXITED(status))
    {
      fprintf(stderr, "teardown: child crash, status %d\n", status);
      return 2;
    }

  return WEXITSTATUS(status);
}

#else /* no socket, or no netinet/in.h */

int main(void)
{
  fprintf(stderr, "socket headers are missing, cannot test networking\n");
  return 1;
}

#endif

// Local Variables:
// mode: C++
// fill-column: 76
// c-file-style: "gnu"
// indent-tabs-mode: nil
// End:
// vim: et:sw=2:sts=2:ts=2:cino=>2s,{s,\:s,+s,t0,g0,^-2,e-2,n-2,p2s,(0,=s:
