/* $Id: connecthostport.c,v 1.24 2020/11/09 19:26:53 nanard Exp $ */
/* vim: tabstop=4 shiftwidth=4 noexpandtab
 * Project : miniupnp
 * Author : Thomas Bernard
 * Copyright (c) 2010-2020 Thomas Bernard
 * This software is subject to the conditions detailed in the
 * LICENCE file provided in this distribution. */

/* use getaddrinfo() or gethostbyname()
 * uncomment the following line in order to use gethostbyname() */
#ifdef NO_GETADDRINFO
#define USE_GETHOSTBYNAME
#endif

#include <string.h>
#include <stdio.h>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#include <io.h>
#define MAXHOSTNAMELEN 64
#include "win32_snprintf.h"
#define herror
#define socklen_t int
#else /* #ifdef _WIN32 */
#include <unistd.h>
#include <sys/types.h>
#ifdef MINIUPNPC_SET_SOCKET_TIMEOUT
#include <fcntl.h>
#include <sys/time.h>
#endif /* #ifdef MINIUPNPC_SET_SOCKET_TIMEOUT */
#include <sys/param.h>
#include <poll.h>
#include <errno.h>
#define closesocket close
#include <netdb.h>
#include <netinet/in.h>
#include <sys/socket.h>
#endif /* #else _WIN32 */

#if defined(__amigaos__) || defined(__amigaos4__)
#define herror(A) printf("%s\n", A)
#endif

#include "connecthostport.h"

#ifndef MAXHOSTNAMELEN
#define MAXHOSTNAMELEN 64
#endif

static int connectsocketport(SOCKET s, struct sockaddr * dest, socklen_t destlen)
{
    int n;
    int err;
#ifdef MINIUPNPC_SET_SOCKET_TIMEOUT
#ifdef _WIN32
    struct timeval timeout;
    u_long nonblock = 1;
    fd_set xset;
    ioctlsocket(s, FIONBIO, &nonblock);
#else /* #ifdef _WIN32 */
    int nonblock = fcntl(s, F_GETFL, NULL);
    if(nonblock >= 0)
    {
        fcntl(s, F_SETFL, nonblock | O_NONBLOCK);
    }
#endif /* #ifdef _WIN32 */
#endif /* #ifdef MINIUPNPC_SET_SOCKET_TIMEOUT */

    n = connect(s, dest, destlen);

    /* EINTR The system call was interrupted by a signal that was caught
     * EINPROGRESS The socket is nonblocking and the connection cannot
     *             be completed immediately. */
#ifdef _WIN32
    while(n < 0 && WSAGetLastError() == WSAEWOULDBLOCK)
#else /* #ifdef _WIN32 */
    while(n < 0 && (errno == EINTR || errno == EINPROGRESS))
#endif /* #ifdef _WIN32 */
    {
        socklen_t len;
#ifdef _WIN32
        fd_set wset;
        FD_ZERO(&wset);
        FD_SET(s, &wset);
#ifdef MINIUPNPC_SET_SOCKET_TIMEOUT
        timeout.tv_sec = 3;
        timeout.tv_usec = 0;
        FD_ZERO(&xset);
        FD_SET(s, &xset);
        n = select(s + 1, NULL, &wset, &xset, &timeout);
        err = FD_ISSET(s, &xset)?1:0;
#else /* #ifdef MINIUPNPC_SET_SOCKET_TIMEOUT */
        n = select(s + 1, NULL, &wset, NULL, NULL);
        err = 0;
#endif /* #ifdef MINIUPNPC_SET_SOCKET_TIMEOUT */
        if(n < 0)
            break;
#else /* #ifdef _WIN32 */
        struct pollfd fds;
        fds.fd = s;
        fds.events = POLLOUT;
#ifdef MINIUPNPC_SET_SOCKET_TIMEOUT
        n = poll(&fds, 1, 3000);
#else /* #ifdef MINIUPNPC_SET_SOCKET_TIMEOUT */
        n = poll(&fds, 1, -1);
#endif /* #ifdef MINIUPNPC_SET_SOCKET_TIMEOUT */
        if(n < 0)
            continue;
        len = sizeof(err);
        if(getsockopt(s, SOL_SOCKET, SO_ERROR, &err, &len) < 0)
        {
            PRINT_SOCKET_ERROR("getsockopt");
            n = -1;
            break;
        }
        if (err != 0)
        {
            errno = err;
        }
#endif /* #ifdef _WIN32 */
        if (!n || err != 0)
        {
            n = -1;
        }
        else
        {
            n = 0;
        }
        break;
    }

#ifdef MINIUPNPC_SET_SOCKET_TIMEOUT
    err = errno;
#ifdef _WIN32
    nonblock = 0;
    ioctlsocket(s, FIONBIO, &nonblock);
#else /* #ifdef _WIN32 */
    if(nonblock >= 0)
    {
        fcntl(s, F_SETFL, nonblock);
    }
#endif /* #ifdef _WIN32 */
    errno = err;
#endif /* #ifdef MINIUPNPC_SET_SOCKET_TIMEOUT */

    return n;
}

/* connecthostport()
 * return a socket connected (TCP) to the host and port
 * or -1 in case of error */
SOCKET connecthostport(const char * host, unsigned short port,
                       unsigned int scope_id)
{
    SOCKET s;
    int n;
#ifdef USE_GETHOSTBYNAME
    struct sockaddr_in dest;
    struct hostent *hp;
#else /* #ifdef USE_GETHOSTBYNAME */
    char tmp_host[MAXHOSTNAMELEN+1];
    char port_str[8];
    struct addrinfo *ai, *p;
    struct addrinfo hints;
#endif /* #ifdef USE_GETHOSTBYNAME */

#ifdef USE_GETHOSTBYNAME
    hp = gethostbyname(host);
    if(hp == NULL)
    {
        herror(host);
        return INVALID_SOCKET;
    }
    memcpy(&dest.sin_addr, hp->h_addr, sizeof(dest.sin_addr));
    memset(dest.sin_zero, 0, sizeof(dest.sin_zero));
    s = socket(PF_INET, SOCK_STREAM, 0);
    if(ISINVALID(s))
    {
        PRINT_SOCKET_ERROR("socket");
        return INVALID_SOCKET;
    }
    n = connectsocketport(s, (struct sockaddr *)&dest, sizeof(struct sockaddr_in));
    if(n<0)
    {
        PRINT_SOCKET_ERROR("connect");
        closesocket(s);
        return INVALID_SOCKET;
    }
#else /* #ifdef USE_GETHOSTBYNAME */
    /* use getaddrinfo() instead of gethostbyname() */
    memset(&hints, 0, sizeof(hints));
    /* hints.ai_flags = AI_ADDRCONFIG; */
#ifdef AI_NUMERICSERV
    hints.ai_flags = AI_NUMERICSERV;
#endif
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_family = AF_UNSPEC; /* AF_INET, AF_INET6 or AF_UNSPEC */
    /* hints.ai_protocol = IPPROTO_TCP; */
    snprintf(port_str, sizeof(port_str), "%hu", port);
    if(host[0] == '[')
    {
        /* literal ip v6 address */
        int i, j;
        for(i = 0, j = 1; host[j] && (host[j] != ']') && i < MAXHOSTNAMELEN; i++, j++)
        {
            tmp_host[i] = host[j];
            if(0 == strncmp(host+j, "%25", 3))  /* %25 is just url encoding for '%' */
                j+=2;                           /* skip "25" */
        }
        tmp_host[i] = '\0';
    }
    else
    {
        strncpy(tmp_host, host, MAXHOSTNAMELEN);
    }
    tmp_host[MAXHOSTNAMELEN] = '\0';
    n = getaddrinfo(tmp_host, port_str, &hints, &ai);
    if(n != 0)
    {
#ifdef _WIN32
        fprintf(stderr, "getaddrinfo() error : %d\n", n);
#else
        fprintf(stderr, "getaddrinfo() error : %s\n", gai_strerror(n));
#endif
        return INVALID_SOCKET;
    }
    s = INVALID_SOCKET;
    for(p = ai; p; p = p->ai_next)
    {
        if(!ISINVALID(s))
            closesocket(s);
#ifdef DEBUG
        printf("ai_family=%d ai_socktype=%d ai_protocol=%d (PF_INET=%d, PF_INET6=%d)\n",
               p->ai_family, p->ai_socktype, p->ai_protocol, PF_INET, PF_INET6);
#endif
        s = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
        if(ISINVALID(s))
            continue;
        if(p->ai_addr->sa_family == AF_INET6 && scope_id > 0) {
            struct sockaddr_in6 * addr6 = (struct sockaddr_in6 *)p->ai_addr;
            addr6->sin6_scope_id = scope_id;
        }
        n = connectsocketport(s, p->ai_addr, MSC_CAST_INT p->ai_addrlen);
        if(n >= 0)  /* connect() was successful */
            break;
    }
    freeaddrinfo(ai);
    if(ISINVALID(s))
    {
        PRINT_SOCKET_ERROR("socket");
        return INVALID_SOCKET;
    }
    if(n < 0)
    {
        PRINT_SOCKET_ERROR("connect");
        closesocket(s);
        return INVALID_SOCKET;
    }
#endif /* #ifdef USE_GETHOSTBYNAME */
    return s;
}
