/* ========================================================================== */
/*! \file
 * \brief Internet connection handling
 *
 * Copyright (c) 2015-2022 by the developers. See the LICENSE file for details.
 *
 * If nothing else is specified, functions return zero to indicate success
 * and a negative value to indicate an error.
 */


/* ========================================================================== */
/* Include headers */

#include "posix.h"  /* Include this first because of feature test macros */

#include <string.h>

#include "config.h"
#include "inet.h"
#include "main.h"


/* ========================================================================== */
/*! \defgroup INET INET: Internet protocol
 *
 * Internet name resolution, socket and connection handling.
 */
/*! @{ */


/* ========================================================================== */
/* Constants */

/*! \brief Message prefix for INET module */
#define MAIN_ERR_PREFIX  "INET: "

/*! \name Socket option actions */
/*! @{ */
#define INET_OPTS_CLEAR  0  /*!< \brief Remove socket options */
#define INET_OPTS_SET    1  /*!< \brief Add socket options */
/*! @} */


/* ========================================================================== */
/* Variables */

int  inet_force_ipv4 = 0;


/* ========================================================================== */
/* Host and service name resolver
 *
 * \param[in]  host     Name of host
 * \param[in]  service  Name of service
 * \param[in]  af       Address family
 * \param[out] sai      Pointer to socket address information
 *
 * The parameter \e af should be set to \c POSIX_AF_UNSPEC except if only
 * results for a specific address family should be requested.
 *
 * \note
 * The data at the location pointed to by sai is undefined after error status
 * is returned.
 *
 * \note
 * On success the caller is responsible to free the ressources allocated by this
 * function using \c posix_freeaddrinfo() .
 *
 * \return
 * - Zero if connection was successfully established
 * - Negative value on error (use \c INET_ERR_xxx constants for checks)
 */

static int  inet_name_resolver(const char*  host, const char*  service,
                               int  af, struct_posix_addrinfo**  sai)
{
   int  res = 0;
   struct_posix_addrinfo  hints;
   int  rv;
   const char*  err_str;
   char*  ebuf = NULL;
   size_t  len;

   memset((void*) &hints, 0, sizeof(hints));
   /* Only return results for address families configured on local system */
   hints.ai_flags = POSIX_AI_ADDRCONFIG;
   hints.ai_family = af;
   hints.ai_socktype = POSIX_SOCK_STREAM;
   hints.ai_protocol = 0;
   rv = posix_getaddrinfo(host, service, &hints, sai);
   if(rv)
   {
      PRINT_ERROR("getaddrinfo() failed");
      err_str = posix_gai_strerror(rv);
      if(NULL != err_str)
      {
         len = strlen(MAIN_ERR_PREFIX);
         len += strlen(err_str);
         ebuf = (char*) posix_malloc(++len);
         if(NULL != ebuf)
         {
            strcpy(ebuf, MAIN_ERR_PREFIX);
            strcat(ebuf, err_str);
            print_error(ebuf);
         }
         posix_free((void*) ebuf);
      }
      switch(rv)
      {
         case POSIX_EAI_FAMILY:  { res = INET_ERR_AF;  break; }
         case POSIX_EAI_NONAME:  { res = INET_ERR_HNR;  break; }
         case POSIX_EAI_SERVICE:  { res = INET_ERR_SNR;  break; }
         default:  { res = INET_ERR_UNSPEC;  break; }
      }
   }

   return(res);
}


/* ========================================================================== */
/* Create socket
 *
 * \param[out] sd  Pointer to socket descriptor
 * \param[in]  af  Address family
 *
 * The value for \e af must be \c POSIX_INET or \c POSIX_INET6 respectively.
 *
 * \note
 * The value -1 is written to the location pointed to by \e sd on error.
 *
 * \return
 * - Zero if socket was successfully created
 * - Negative value on error (use \c INET_ERR_xxx constants for checks)
 */

static int  inet_socket_create(int*  sd, int  af)
{
   int  res = 0;
   int  rv;

   if(POSIX_AF_INET != af
#if CFG_USE_IP6
      && POSIX_AF_INET6 != af
#endif  /* CFG_USE_IP6 */
     )
   {
      PRINT_ERROR("socket(): Address family not supported");
      res = INET_ERR_AF;
   }
   else
   {
      *sd = posix_socket(af, POSIX_SOCK_STREAM, 0);
      if(0 > *sd)
      {
         PRINT_ERROR("socket() failed");
         *sd = -1;
         res = INET_ERR_SOCK;
      }
      else
      {
         /* Set "Close on exec()" flag */
         do  { rv = posix_fcntl(*sd, POSIX_F_SETFD, POSIX_FD_CLOEXEC); }
         while(-1 == rv && POSIX_EINTR == posix_errno);
         if(-1 == rv)
         {
            PRINT_ERROR("fcntl() failed");
            inet_close(sd);
            res = INET_ERR_SOCK;
         }
      }
   }

   return(res);
}


#if CFG_USE_CONNECT_TIMEOUT
/* ========================================================================== */
/* Configure socket options
 *
 * \param[in] sd      Socket descriptor
 * \param[in] action  Socket option action (use \c INET_OPTION_xxx constants)
 * \param[in] opts    Socket options
 *
 * \return
 * - Zero if socket was successfully configured
 * - Negative value on error (use \c INET_ERR_xxx constants for checks)
 */

static int  inet_socket_options(int  sd, int  action, int  opts)
{
   int  res = 0;
   int  rv;
   int  opts_new;

   /* Get socket options */
   do  { rv = posix_fcntl(sd, POSIX_F_GETFL); }
   while(-1 == rv && POSIX_EINTR == posix_errno);
   if(-1 == rv)
   {
      PRINT_ERROR("fcntl() failed");
      res = INET_ERR_SOCK;
   }

   if (!res)
   {
      /* Modify socket options */
      if (INET_OPTS_SET == action)  { opts_new = rv | opts; }
      else  { opts_new = rv & ~opts; }

      /* Set socket options */
      do  { rv = posix_fcntl(sd, POSIX_F_SETFL, opts_new); }
      while(-1 == rv && POSIX_EINTR == posix_errno);
      if(-1 == rv)
      {
         PRINT_ERROR("fcntl() failed");
         res = INET_ERR_SOCK;
      }
   }

   return(res);
}
#endif


/* ========================================================================== */
/*! \brief Establish stream oriented connection
 *
 * \param[out]    sd       Pointer to socket descriptor
 * \param[in,out] af       Address family
 * \param[in]     host     Name of host
 * \param[in]     service  Name of service
 *
 * On success the socket descriptor of the established connection is written to
 * the location pointed to by \e sd . If the address family was specified as
 * \c POSIX_AF_UNSPEC or was enforced by configuration, it is overwritten with
 * the address family that was used to establish the connection.
 *
 * On error -1 is written to the location pointed to by \e sd and the value
 * pointed to by \e af is unchanged.
 *
 * \note
 * If the name resolver reports multiple addresses for \e host , all entries are
 * tried in order. If no connection could be established, error status is
 * returned for the last entry in the list.
 *
 * \return
 * - Zero if connection was successfully established
 * - Negative value on error (use \c INET_ERR_xxx constants for checks)
 */

int  inet_connect(int*  sd, int*  af, const char*  host, const char*  service)
{
   int  res;
   int  rv;
   struct_posix_addrinfo*  sai;
   struct_posix_addrinfo*  saie;
   int  family;
   int  socket;
   struct_posix_pollfd  fds[1];
   posix_socklen_t  opt_len;
#if CFG_USE_CONNECT_TIMEOUT
   posix_time_t  ts1, ts2, ts_diff;
#endif
   int  timeout;
   int  timed_out = 0;
   int  refused = 0;

   *sd = -1;

   /* Override address family on request */
   if(inet_force_ipv4)  { *af = POSIX_AF_INET; }

   /* Resolve host and service names */
   res = inet_name_resolver(host, service, *af, &sai);
   if(!res)
   {
      for(saie = sai; saie != NULL; saie = saie->ai_next)
      {
         /* Create socket */
         family = saie->ai_family;
         res = inet_socket_create(&socket, family);
#if CFG_USE_CONNECT_TIMEOUT
         if(!res)
         {
            /* Configure socket to nonblocking mode */
            res = inet_socket_options(socket, INET_OPTS_SET, POSIX_O_NONBLOCK);
            /* Store timestamp */
            ts1 = posix_time(NULL);
            if((posix_time_t) 0 > ts1)  { ts1 = 0; }
         }
#endif
         if(!res)
         {
            /* Try to establish connection */
            refused = 0;
            timed_out = 0;
            rv = posix_connect(socket, saie->ai_addr, saie->ai_addrlen);
            if(rv)
            {
               if(POSIX_ECONNREFUSED == posix_errno)  { refused = 1; }
               res = INET_ERR_CONN;
            }
            if(-1 == rv &&
               (POSIX_EINPROGRESS == posix_errno || POSIX_EINTR == posix_errno))
            {
               /*
                * Interrupted by signal (or nonblocking mode)
                * => Connection is established asynchronously
                */
               do
               {
                  timeout = -1;
#if CFG_USE_CONNECT_TIMEOUT
                  timeout = 500;  /* Milliseconds */
                  /* Timeout will be aborted/restartet by signals */
                  ts2 = posix_time(NULL);
                  if((posix_time_t) 0 > ts2)  { ts2 = 0; }
                  ts_diff = ts2 - ts1;
                  if ((float) CFG_USE_CONNECT_TIMEOUT < (float) ts_diff)
                  {
                      rv = 0;
                      break;
                  }
#endif
                  fds[0].fd = socket;
                  fds[0].events = POSIX_POLLOUT;
                  fds[0].revents = 0;
                  rv = posix_poll(fds, 1, timeout);
               }
               while(0 == rv || (-1 == rv && POSIX_EINTR == posix_errno));
               if(0 == rv) { timed_out = 1; }
               else if(1 == rv)
               {
                  /* Get return value of 'connect()' */
                  opt_len = sizeof(int);
                  rv = posix_getsockopt(socket, POSIX_SOL_SOCKET,
                                        POSIX_SO_ERROR, &res, &opt_len);
                  if(-1 == rv || res)  { res = INET_ERR_CONN; }
               }
            }
         }
#if CFG_USE_CONNECT_TIMEOUT
         if (!res)
         {
            /* Configure socket to blocking mode */
            res = inet_socket_options(socket, INET_OPTS_CLEAR,
                                      POSIX_O_NONBLOCK);
         }
#endif

         /* Check for error */
         if(res)
         {
            if(refused)  { PRINT_ERROR("Connection refused"); }
            else if(timed_out)
            {
               PRINT_ERROR("User defined TCP connection timeout");
            }
            else  { PRINT_ERROR("connect() failed"); }
            inet_close(&socket);
            /* Try next entry in linked list */
         }
         else
         {
            /* Connection established */
            if(POSIX_AF_INET == family)
            {
               printf("%s: %s%s\n", CFG_NAME, MAIN_ERR_PREFIX,
                      "Using IPv4 protocol");
            }
#if CFG_USE_IP6
            else if(POSIX_AF_INET6 == family)
            {
               printf("%s: %s%s\n", CFG_NAME, MAIN_ERR_PREFIX,
                      "Using IPv6 protocol");
            }
#endif  /* CFG_USE_IP6 */
            *af = family;
            *sd = socket;
            break;
         }
      }
      /* Release ressources allocated by name resolver */
      posix_freeaddrinfo(sai);
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Try to set RX timeout for socket
 *
 * \param[in] sd     Socket descriptor
 * \param[in] rx_to  RX timeout in seconds (Upper limit: 3600)
 *
 * \attention
 * On POSIX systems it is implementation defined whether timeouts on sockets
 * can be set or not. It is no misbehaviour if the OS rejects the requests and
 * this function returns an error.
 *
 * If \e rx_to is set to zero this means "no timeout" (an existing timeout
 * setting will be removed).
 *
 * \note
 * POSIX offers no way to get the type (integer and floating point allowed) and
 * range of "time_t". We limit the range to [0, 3600], expecting that every sane
 * platform can handle 1 hour (even if this strictly isn't required by C90 and
 * POSIX).
 *
 * \return
 * - Zero on success
 * - Negative value on error (use \c INET_ERR_xxx constants for checks)
 */

int  inet_set_rx_timeout(int  sd, unsigned int  rx_to)
{
   int res = INET_ERR_BADF;
   int  rv;
   struct_posix_timeval  rx;

   if(-1 != sd)
   {
      if(3600U >= rx_to)
      {
         rx.tv_sec = (posix_time_t) rx_to;
         rx.tv_usec = 0;
         rv = posix_setsockopt(sd, POSIX_SOL_SOCKET, POSIX_SO_RCVTIMEO,
                               (void*) &rx,
                               (posix_socklen_t) sizeof(struct_posix_timeval));
         if(-1 == rv)
         {
            PRINT_ERROR("setsockopt() failed for RX timeout");
            if(POSIX_EINVAL == posix_errno)  { res = INET_ERR_RX_TO; }
            if(POSIX_ENOPROTOOPT == posix_errno)  { res = INET_ERR_RX_TO; }
            if(POSIX_EBADF != posix_errno && POSIX_ENOTSOCK != errno)
            {
               res = INET_ERR_UNSPEC;
            }
         }
         else  { res = 0; }
      }
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Try to set TX timeout for socket
 *
 * \param[in] sd     Socket descriptor
 * \param[in] tx_to  TX timeout in seconds (Upper limit: 3600)
 *
 * \attention
 * On POSIX systems it is implementation defined whether timeouts on sockets
 * can be set or not. It is no misbehaviour if the OS rejects the requests and
 * this function returns an error.
 *
 * If \e tx_to is set to zero this means "no timeout" (an existing timeout
 * setting will be removed).
 *
 * \note
 * POSIX offers no way to get the type (integer and floating point allowed) and
 * range of "time_t". We limit the range to [0, 3600], expecting that every sane
 * platform can handle 1 hour (even if this strictly isn't required by C90 and
 * POSIX).
 *
 * \return
 * - Zero on success
 * - Negative value on error (use \c INET_ERR_xxx constants for checks)
 */

int  inet_set_tx_timeout(int  sd, unsigned int  tx_to)
{
   int res = INET_ERR_BADF;
   int  rv;
   struct_posix_timeval  tx;

   if(-1 != sd)
   {
      if(3600U >= tx_to)
      {
         tx.tv_sec = (posix_time_t) tx_to;
         tx.tv_usec = 0;
         rv = posix_setsockopt(sd, POSIX_SOL_SOCKET, POSIX_SO_SNDTIMEO,
                               (void*) &tx,
                               (posix_socklen_t) sizeof(struct_posix_timeval));
         if(-1 == rv)
         {
            PRINT_ERROR("setsockopt() failed for TX timeout");
            if(POSIX_EINVAL == posix_errno)  { res = INET_ERR_TX_TO; }
            if(POSIX_ENOPROTOOPT == posix_errno)  { res = INET_ERR_TX_TO; }
            if(POSIX_EBADF != posix_errno && POSIX_ENOTSOCK != errno)
            {
               res = INET_ERR_UNSPEC;
            }
         }
         else  { res = 0; }
      }
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Close connection and destroy socket
 *
 * \param[in,out] sd  Pointer to socket descriptor
 *
 * \note
 * It is save to call this function with \e sd pointing to \c -1 . This will
 * execute as NOP.
 *
 * \note
 * The value -1 is present at the location pointed to by \e sd in any case after
 * return.
 */

void  inet_close(int*  sd)
{
   if(-1 != *sd)
   {
      posix_close(*sd);
      *sd = -1;
   }
}


/*! @} */

/* EOF */
