/* source: xio-ipapp.c */
/* Copyright Gerhard Rieger and contributors (see file CHANGES) */
/* Published under the GNU General Public License V.2, see file COPYING */

/* this file contains the source for TCP and UDP related options */

#include "xiosysincludes.h"

#if WITH_TCP || WITH_UDP || WITH_SCTP || WITH_DCCP || WITH_UDPLITE

#include "xioopen.h"
#include "xio-socket.h"
#include "xio-ip.h"
#include "xio-listen.h"
#include "xio-ip6.h"
#include "xio-ipapp.h"

const struct optdesc opt_sourceport = { "sourceport", "sp",       OPT_SOURCEPORT,  GROUP_IPAPP,     PH_LATE,TYPE_2BYTE,	OFUNC_SPEC };
/*const struct optdesc opt_port = { "port",  NULL,    OPT_PORT,        GROUP_IPAPP, PH_BIND,    TYPE_USHORT,	OFUNC_SPEC };*/
const struct optdesc opt_lowport = { "lowport", NULL, OPT_LOWPORT, GROUP_IPAPP, PH_LATE, TYPE_BOOL, OFUNC_SPEC };


#if _WITH_IP4 || _WITH_IP6
/* we expect the form "host:port" */
int xioopen_ipapp_connect(
	int argc,
	const char *argv[],
	struct opt *opts,
	int xioflags,
	xiofile_t *xxfd,
	const struct addrdesc *addrdesc)
{
   struct single *sfd = &xxfd->stream;
   struct opt *opts0 = NULL;
   const char *hostname = argv[1], *portname = argv[2];
   int pf = addrdesc->arg3;
   int socktype = addrdesc->arg1;
   int ipproto = addrdesc->arg2;
   bool dofork = false;
   int maxchildren = 0;
   struct addrinfo **bindarr = NULL;
   struct addrinfo **themarr = NULL;
   uint16_t bindport = 0;
   bool needbind = false;
   bool lowport = false;
   int level = E_ERROR;
   int result;

   if (argc != 3) {
      xio_syntax(argv[0], 2, argc-1, addrdesc->syntax);
      return STAT_NORETRY;
   }

   /* Apply and retrieve some options */
   result = _xioopen_ipapp_init(sfd, xioflags, opts,
				&dofork, &maxchildren,
				&pf, &socktype, &ipproto,
				socktype == SOCK_DGRAM ? XIOSHUT_NULL : -1);
   if (result != STAT_OK)
      return result;

   opts0 = opts; 	/* save remaining options for each loop */
   opts = NULL;

   Notice2("opening connection to %s:%s", hostname, portname);

   do {	/* loop over retries and/or forks */
      int _errno;

#if WITH_RETRY
      if (sfd->forever || sfd->retry) {
	 level = E_NOTICE;
      } else
#endif /* WITH_RETRY */
	 level = E_WARN;

      opts = copyopts(opts0, GROUP_ALL);

      result =
	 _xioopen_ipapp_prepare(&opts, opts0, hostname, portname,
				pf, socktype, ipproto,
				sfd->para.socket.ip.ai_flags,
				&themarr, &bindarr, &bindport, &needbind, &lowport);
      switch (result) {
      case STAT_OK: break;
#if WITH_RETRY
      case STAT_RETRYLATER:
      case STAT_RETRYNOW:
	 if (sfd->forever || sfd->retry--) {
	    if (result == STAT_RETRYLATER)
	       Nanosleep(&sfd->intervall, NULL);
	    if (bindarr != NULL)  xiofreeaddrinfo(bindarr);
	    xiofreeaddrinfo(themarr);
	    freeopts(opts);
	    continue;
	 }
#endif /* WITH_RETRY */
	 /* FALLTHROUGH */
      case STAT_NORETRY:
	 if (bindarr != NULL)  xiofreeaddrinfo(bindarr);
	 xiofreeaddrinfo(themarr);
	 freeopts(opts);
	 freeopts(opts0);
	 return result;
      }

      result =
	 _xioopen_ipapp_connect(sfd, hostname, opts, themarr,
				needbind, bindarr, bindport, lowport, level);
      _errno = errno;
      if (bindarr != NULL)
	 xiofreeaddrinfo(bindarr);
      xiofreeaddrinfo(themarr);
      switch (result) {
      case STAT_OK: break;
#if WITH_RETRY
      case STAT_RETRYLATER:
      case STAT_RETRYNOW:
	 if (sfd->forever || sfd->retry--) {
	    if (result == STAT_RETRYLATER) {
	       Nanosleep(&sfd->intervall, NULL);
	    }
	    freeopts(opts);
	    continue;
	 }
#endif /* WITH_RETRY */
	 /* FALLTHROUGH */
      default:
	 Error4("%s:%s:%s: %s", argv[0], argv[1], argv[2],
		_errno?strerror(_errno):"(See above)");
	 freeopts(opts);
	 freeopts(opts0);
	 return result;
      }

#if WITH_RETRY
      if (dofork) {
	 pid_t pid;
	 int level = E_ERROR;
	 if (sfd->forever || sfd->retry) {
	    level = E_WARN;	/* most users won't expect a problem here,
				   so Notice is too weak */
	 }
	 while ((pid = xio_fork(false, level, sfd->shutup)) < 0) {
	    if (sfd->forever || sfd->retry--) {
	       Nanosleep(&sfd->intervall, NULL);
	       continue;
	    }
	    freeopts(opts);
	    freeopts(opts0);
	    return STAT_RETRYLATER;
	 }

	 if (pid == 0) {	/* child process */
	    sfd->forever = false;
	    sfd->retry = 0;
	    break;
	 }

	 /* parent process */
	 Close(sfd->fd);
	 /* with and without retry */
	 Nanosleep(&sfd->intervall, NULL);
	 while (maxchildren > 0 && num_child >= maxchildren) {
	    Info1("all %d allowed children are active, waiting", maxchildren);
	    Nanosleep(&sfd->intervall, NULL);
	 }
	 freeopts(opts);
	 continue;	/* with next socket() bind() connect() */
      } else
#endif /* WITH_RETRY */
      {
	 break;
      }
   } while (true); 	/* end of loop over retries and/or forks */
   /* only "active" process breaks (master without fork, or child) */

   Notice2("successfully connected to %s:%s", hostname, portname);

   result = _xio_openlate(sfd, opts);
   freeopts(opts);
   freeopts(opts0);
   return result;
}


/* This function performs static initializations for addresses like TCP-CONNECT
   before start of the outer loop:
   it retrieves some options
   returns STAT_OK on success or some other value on failure;
   applies and consumes the following options:
   PH_INIT, OPT_FORK, OPT_MAX_CHILDREN, OPT_PROTOCOL_FAMILY, OPT_SO_TYPE,
   OPT_SO_PROTOTYPE
*/
int _xioopen_ipapp_init(
	struct single *sfd,
	int xioflags,
	struct opt *opts,
	bool *dofork,
	int *maxchildren,
	int *pf,
	int *socktype,
	int *ipproto,
	int howtoshut)
{
   if (sfd->howtoend == END_UNSPEC)
      sfd->howtoend = END_SHUTDOWN;

   if (sfd->howtoshut == XIOSHUT_UNSPEC && howtoshut >= 0) {
      sfd->howtoshut = howtoshut;
   }

   if (applyopts_single(sfd, opts, PH_INIT) < 0)
      return -1;
   if (applyopts(sfd, -1, opts, PH_INIT) < 0)
      return -1;

   retropt_bool(opts, OPT_FORK, dofork);
   if (dofork) {
      if (!(xioflags & XIO_MAYFORK)) {
	 Error1("%s: option fork not allowed here", sfd->addr->defname);
	 return STAT_NORETRY;
      }
      sfd->flags |= XIO_DOESFORK;
   }

   retropt_int(opts, OPT_MAX_CHILDREN, maxchildren);
   if (! dofork && maxchildren) {
      Error1("%s: option max-children not allowed without option fork", sfd->addr->defname);
      return STAT_NORETRY;
   }

   retropt_socket_pf(opts, pf);
   retropt_int(opts, OPT_SO_TYPE, socktype);
   retropt_int(opts, OPT_SO_PROTOTYPE, ipproto);

   if (dofork) {
      xiosetchilddied();	/* set SIGCHLD handler */
   }

   if (xioparms.logopt == 'm') {
      Info("starting connect loop, switching to syslog");
      diag_set('y', xioparms.syslogfac);
      xioparms.logopt = 'y';
   } else {
      Info("starting connect loop");
   }

   return STAT_OK;
}


/* This function performs preparations for addresses like TCP-CONNECT
   at the beginning of the outer (retry/fork) loop:
   it evaluates some options and performs name resolution of both server
   (target, "them") address and bind ("us") address.
   It is intended to be invoked before the connect loop starts;
   returns STAT_OK on success or some other value on failure;
   applies and consumes the following options:
   PH_EARLY
   OPT_BIND, OPT_SOURCEPORT, OPT_LOWPORT
   returns STAT_OK, STAT_RETRYLATER, or STAT_NORETRY (+errno)
*/
int _xioopen_ipapp_prepare(
	struct opt **opts,
	struct opt *opts0,
	const char *hostname,
	const char *portname,
	int pf,
	int socktype,
	int protocol,
	const int ai_flags[2],
	struct addrinfo ***themarr, 	/* always from getaddrinfo(); xiofreeaddrinfo()! */
	struct addrinfo ***bindarr, 	/* on bind from getaddrinfo(); xiofreeaddrinfo()! */
	uint16_t *bindport, 		/* for bind without address */
	bool *needbind,
	bool *lowport)
{
   uint16_t port;
   int rc;

   *opts = copyopts(opts0, GROUP_ALL);

   if (hostname != NULL || portname != NULL) {
      rc = xiogetaddrinfo(hostname, portname, pf, socktype, protocol,
			  themarr, ai_flags);
      if (rc == EAI_AGAIN) {
	 Warn4("_xioopen_ipapp_prepare(node=\"%s\", service=\"%s\", pf=%d, ...): %s",
	       hostname?hostname:"NULL", portname?portname:"NULL",
	       pf, gai_strerror(rc));
	 errno = EAGAIN;
	 return STAT_RETRYLATER;
      } else if (rc != 0) {
	 Error4("_xioopen_ipapp_prepare(node=\"%s\", service=\"%s\", pf=%d, ...): %s",
		hostname?hostname:"NULL", portname?portname:"NULL",
		pf, (rc == EAI_SYSTEM)?strerror(errno):gai_strerror(rc));
	 errno = 0; 	/* unspecified */
	 return STAT_NORETRY;	/*! STAT_RETRYLATER? */
      }
   }

   applyopts(NULL, -1, *opts, PH_EARLY);

   /* 3 means: IP address AND port accepted */
   if (retropt_bind_ip(*opts, pf, socktype, protocol, bindarr, 3, ai_flags)
       != STAT_NOACTION) {
      *needbind = true;
   }
   if (retropt_2bytes(*opts, OPT_SOURCEPORT, &port) >= 0) {
      if (*bindarr) {
	 struct addrinfo **bindp;
	 bindp = *bindarr;
	 switch ((*bindp)->ai_family) {
#if WITH_IP4
	 case PF_INET:  ((struct sockaddr_in *)(*bindp)->ai_addr)->sin_port = htons(port); break;
#endif /* WITH_IP4 */
#if WITH_IP6
	 case PF_INET6: ((struct sockaddr_in6 *)(*bindp)->ai_addr)->sin6_port = htons(port); break;
#endif /* WITH_IP6 */
	 default:
	    Error("unsupported protocol family");
	    errno = EPROTONOSUPPORT;
	    return STAT_NORETRY;
	 }
      } else {
	 *bindport = port;
      }
      *needbind = true;
   }

   retropt_bool(*opts, OPT_LOWPORT, lowport);

   return STAT_OK;
}

#endif /* _WITH_IP4 || _WITH_IP6 */


/* Tries to connect to the addresses in themarr, for each one it tries to bind
   to the addresses in bindarr.
   Ends on success or when all attempts failed.
   Returns STAT_OK on success, or STAT_RETRYLATER (+errno) on failure. */
int _xioopen_ipapp_connect(struct single *sfd,
			   const char *hostname,
			   struct opt *opts,
			   struct addrinfo **themarr,
			   bool needbind,
			   struct addrinfo **bindarr,
			   uint16_t bindport,
			   bool lowport,
			   int level)
{
   struct addrinfo **themp;
   struct addrinfo **bindp;
   union sockaddr_union bindaddr = {0};
   union sockaddr_union *bindaddrp = NULL;
   socklen_t bindlen = 0;
   char infobuff[256];
   int _errno;
   int result = STAT_OK;

   --level;

   /* Loop over server addresses (themarr) */
   themp = themarr;
   while (*themp != NULL) {
      Notice1("opening connection to %s",
	      sockaddr_info((*themp)->ai_addr, (*themp)->ai_addrlen,
			    infobuff, sizeof(infobuff)));

      if (*(themp+1) == NULL) {
	 ++level; 	/* last attempt */
      }

      /* Loop over array (list) of bind addresses */
      if (needbind && bindarr != NULL) {
	 /* Bind by hostname, use resolvers results list */
	 bindp = bindarr;
	 while (*bindp != NULL) {
	    if ((*bindp)->ai_family == (*themp)->ai_family)
	       break;
	    ++bindp;
	 }
	 if (*bindp == NULL) {
	    Warn3("%s: No bind address with matching address family (%d) of %s available",
		  sfd->addr->defname, (*themp)->ai_family, hostname);
	    ++themp;
	    if ((*themp) == NULL) {
	       result = STAT_RETRYLATER;
	    }
	    _errno = ENOPROTOOPT;
	    continue;
	 }
	 bindaddrp = (union sockaddr_union *)(*bindp)->ai_addr;
	 bindlen  = (*bindp)->ai_addrlen;
      } else if (needbind && bindport) {
	 /* Bind by sourceport option */
	 switch ((*themp)->ai_family) {
#if WITH_IP4
	 case PF_INET:
	    bindaddr.ip4.sin_family = (*themp)->ai_family;
	    bindaddr.ip4.sin_port = htons(bindport);
	    bindaddrp = &bindaddr;
	    bindlen = sizeof(bindaddr.ip4);
	    break;
#endif
#if WITH_IP6
	 case PF_INET6:
	    bindaddr.ip6.sin6_family = (*themp)->ai_family;
	    bindaddr.ip6.sin6_port = htons(bindport);
	    bindaddrp = &bindaddr;
	    bindlen = sizeof(bindaddr.ip6);
	    break;
#endif
	 }
      }

      result =
	 _xioopen_connect(sfd,
			  bindaddrp, bindlen,
			  (*themp)->ai_addr, (*themp)->ai_addrlen,
			  opts, /*pf?pf:*/(*themp)->ai_family, (*themp)->ai_socktype, (*themp)->ai_protocol,
			  lowport, level);
      if (result == STAT_OK)
	 break;
      _errno = errno;
      ++themp;
      if (*themp == NULL)
	 result = STAT_RETRYLATER;
   } 	/* end of loop over target addresses */

   if (result != STAT_OK)
      errno = _errno;
   return result;
}


#if WITH_TCP && WITH_LISTEN
/*
   applies and consumes the following options:
   OPT_PROTOCOL_FAMILY, OPT_BIND
 */
int _xioopen_ipapp_listen_prepare(
	struct opt *opts,
	struct opt **opts0,
	const char *portname,
	int *pf,
	int ipproto,
	const int ai_flags[2],
	union sockaddr_union *us,
	socklen_t *uslen,
	int socktype)
{
   char *bindname = NULL;
   int ai_flags2[2];
   int result;

   retropt_socket_pf(opts, pf);

   retropt_string(opts, OPT_BIND, &bindname);

   /* Set AI_PASSIVE, except when it is explicitly disabled */
   ai_flags2[0] = ai_flags[0];
   ai_flags2[1] = ai_flags[1];
   if (!(ai_flags2[1] & AI_PASSIVE))
      ai_flags2[0] |= AI_PASSIVE;

   result =
	xioresolve(bindname, portname, *pf, socktype, ipproto,
		   us, uslen, ai_flags2);
   if (result != STAT_OK) {
      /*! STAT_RETRY? */
      return result;
   }
   *opts0 = copyopts(opts, GROUP_ALL);
   return STAT_OK;
}


/* we expect the form: port */
/* currently only used for TCP4 */
int xioopen_ipapp_listen(
	int argc,
	const char *argv[],
	struct opt *opts,
	int xioflags,
	xiofile_t *xfd,
	const struct addrdesc *addrdesc)
{
   struct single *sfd = &xfd->stream;
   struct opt *opts0 = NULL;
   int socktype = addrdesc->arg1;
   int ipproto = addrdesc->arg2;
   int pf = addrdesc->arg3;
   union sockaddr_union us_sa, *us = &us_sa;
   socklen_t uslen = sizeof(us_sa);
   int result;

   if (argc != 2) {
      xio_syntax(argv[0], 2, argc-1, addrdesc->syntax);
      return STAT_NORETRY;
   }

   xioinit_ip(&pf, xioparms.default_ip);
   if (pf == PF_UNSPEC) {
#if WITH_IP4 && WITH_IP6
      switch (xioparms.default_ip) {
      case '4': pf = PF_INET; break;
      case '6': pf = PF_INET6; break;
      default: break;		/* includes \0 */
      }
#elif WITH_IP6
      pf = PF_INET6;
#else
      pf = PF_INET;
#endif
   }

   if (sfd->howtoend == END_UNSPEC)
      sfd->howtoend = END_SHUTDOWN;

   if (applyopts_single(sfd, opts, PH_INIT) < 0)  return -1;
   applyopts(sfd, -1, opts, PH_INIT);
   applyopts(sfd, -1, opts, PH_EARLY);

   if (_xioopen_ipapp_listen_prepare(opts, &opts0, argv[1], &pf, ipproto,
				     sfd->para.socket.ip.ai_flags,
				     us, &uslen, socktype)
       != STAT_OK) {
      return STAT_NORETRY;
   }

   if ((result =
	xioopen_listen(sfd, xioflags,
		       (struct sockaddr *)us, uslen,
		       opts, opts0, pf, socktype, ipproto))
       != 0)
      return result;
   return 0;
}
#endif /* WITH_TCP && WITH_LISTEN */

#endif /* WITH_TCP || WITH_UDP || WITH_SCTP || WITH_DCCP || WITH_UDPLITE */
