/*
 *****************************************************************************
 *
 * File:    http_resolve_host.c
 *
 * Purpose: Routine for using an http request to obtain a client's IP
 *          address as seen from the outside world.
 *
 *  Fwknop is developed primarily by the people listed in the file 'AUTHORS'.
 *  Copyright (C) 2009-2014 fwknop developers and contributors. For a full
 *  list of contributors, see the file 'CREDITS'.
 *
 *  License (GNU General Public License):
 *
 *  This program is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU General Public License
 *  as published by the Free Software Foundation; either version 2
 *  of the License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 *  USA
 *
 *****************************************************************************
*/
#include "fwknop_common.h"
#include "utils.h"
#include <sys/wait.h>

#include <errno.h>

#ifdef WIN32
  #include <winsock2.h>
  #include <ws2tcpip.h>
#else
  #if HAVE_SYS_SOCKET_H
    #include <sys/socket.h>
  #endif
  #include <netdb.h>
#endif

struct url
{
    char    port[MAX_PORT_STR_LEN+1];
    char    host[MAX_URL_HOST_LEN+1];
    char    path[MAX_URL_PATH_LEN+1];
};

static int
try_url(struct url *url, fko_cli_options_t *options)
{
    int     sock=-1, sock_success=0, res, error, http_buf_len, i;
    int     bytes_read = 0, position = 0;
    int     o1, o2, o3, o4;
    struct  addrinfo *result=NULL, *rp, hints;
    char    http_buf[HTTP_MAX_REQUEST_LEN]       = {0};
    char    http_response[HTTP_MAX_RESPONSE_LEN] = {0};
    char   *ndx;

#ifdef WIN32
    WSADATA wsa_data;

    /* Winsock needs to be initialized...
    */
    res = WSAStartup( MAKEWORD(1,1), &wsa_data );
    if( res != 0 )
    {
        log_msg(LOG_VERBOSITY_ERROR, "Winsock initialization error %d", res );
        return(-1);
    }
#endif

    /* Build our HTTP request to resolve the external IP (this is similar to
     * to contacting whatismyip.org, but using a different URL).
    */
    snprintf(http_buf, HTTP_MAX_REQUEST_LEN,
        "GET %s HTTP/1.0\r\nUser-Agent: %s\r\nAccept: */*\r\n"
        "Host: %s\r\nConnection: close\r\n\r\n",
        url->path,
        options->http_user_agent,
        url->host
    );

    http_buf_len = strlen(http_buf);

    memset(&hints, 0, sizeof(struct addrinfo));

    hints.ai_family   = AF_UNSPEC; /* Allow IPv4 or IPv6 */
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;

    error = getaddrinfo(url->host, url->port, &hints, &result);
    if (error != 0)
    {
        log_msg(LOG_VERBOSITY_ERROR, "error in getaddrinfo: %s", gai_strerror(error));
        return(-1);
    }

    for (rp = result; rp != NULL; rp = rp->ai_next) {
        sock = socket(rp->ai_family, rp->ai_socktype,
                rp->ai_protocol);
        if (sock < 0)
            continue;

        if ((error = (connect(sock, rp->ai_addr, rp->ai_addrlen) != -1)))
        {
            sock_success = 1;
            break;  /* made it */
        }
        else /* close the open socket if there was a connect error */
        {
#ifdef WIN32
            closesocket(sock);
#else
            close(sock);
#endif
        }

    }
    if(result != NULL)
        freeaddrinfo(result);

    if (! sock_success)
    {
        log_msg(LOG_VERBOSITY_ERROR, "resolve_ip_http: Could not create socket: ", strerror(errno));
        return(-1);
    }

    log_msg(LOG_VERBOSITY_DEBUG, "\nHTTP request: %s", http_buf);

    res = send(sock, http_buf, http_buf_len, 0);

    if(res < 0)
    {
        log_msg(LOG_VERBOSITY_ERROR, "resolve_ip_http: write error: ", strerror(errno));
    }
    else if(res != http_buf_len)
    {
        log_msg(LOG_VERBOSITY_WARNING,
            "[#] Warning: bytes sent (%i) not spa data length (%i).",
            res, http_buf_len
        );
    }

    do
    {
        memset(http_buf, 0x0, sizeof(http_buf));
        bytes_read = recv(sock, http_buf, sizeof(http_buf), 0);
        if ( bytes_read > 0 ) {
            if(position + bytes_read >= HTTP_MAX_RESPONSE_LEN)
                break;
            memcpy(&http_response[position], http_buf, bytes_read);
            position += bytes_read;
        }
    }
    while ( bytes_read > 0 );

    http_response[HTTP_MAX_RESPONSE_LEN-1] = '\0';

#ifdef WIN32
    closesocket(sock);
#else
    close(sock);
#endif

    log_msg(LOG_VERBOSITY_DEBUG, "\nHTTP response: %s", http_response);

    /* Move to the end of the HTTP header and to the start of the content.
    */
    ndx = strstr(http_response, "\r\n\r\n");
    if(ndx == NULL)
    {
        log_msg(LOG_VERBOSITY_ERROR, "Did not find the end of HTTP header.");
        return(-1);
    }
    ndx += 4;

    /* Walk along the content to try to find the end of the IP address.
     * Note: We are expecting the content to be just an IP address
     *       (possibly followed by whitespace or other not-digit value).
     */
    for(i=0; i<MAX_IPV4_STR_LEN; i++) {
        if(! isdigit(*(ndx+i)) && *(ndx+i) != '.')
            break;
    }

    /* Terminate at the first non-digit and non-dot.
    */
    *(ndx+i) = '\0';

    /* Now that we have what we think is an IP address string.  We make
     * sure the format and values are sane.
     */
    if((sscanf(ndx, "%u.%u.%u.%u", &o1, &o2, &o3, &o4)) == 4
            && o1 >= 0 && o1 <= 255
            && o2 >= 0 && o2 <= 255
            && o3 >= 0 && o3 <= 255
            && o4 >= 0 && o4 <= 255)
    {
        strlcpy(options->allow_ip_str, ndx, sizeof(options->allow_ip_str));

        log_msg(LOG_VERBOSITY_INFO,
                    "\n[+] Resolved external IP (via http://%s%s) as: %s",
                    url->host,
                    url->path,
                    options->allow_ip_str);

        return(1);
    }
    else
    {
        log_msg(LOG_VERBOSITY_ERROR,
            "[-] From http://%s%s\n    Invalid IP (%s) in HTTP response:\n\n%s",
            url->host, url->path, ndx, http_response);
        return(-1);
    }
}

static int
parse_url(char *res_url, struct url* url)
{
    char *s_ndx, *e_ndx;
    int  tlen, tlen_offset, port, is_err;

    /* Strip off https:// or http:// portion if necessary
    */
    if(strncasecmp(res_url, "https://", 8) == 0)
        s_ndx = res_url + 8;
    else if(strncasecmp(res_url, "http://", 7) == 0)
        s_ndx = res_url + 7;
    else
        s_ndx = res_url;

    /* Look for a colon in case an alternate port was specified.
    */
    e_ndx = strchr(s_ndx, ':');
    if(e_ndx != NULL)
    {
        port = strtol_wrapper(e_ndx+1, 1, MAX_PORT, NO_EXIT_UPON_ERR, &is_err);
        if(is_err != FKO_SUCCESS)
        {
            log_msg(LOG_VERBOSITY_ERROR,
                "[*] resolve-url port value is invalid, must be in [%d-%d]",
                1, MAX_PORT);
            return(-1);
        }

        snprintf(url->port, sizeof(url->port)-1, "%u", port);

        /* Get the offset we need to skip the port portion when we
         * extract the hostname part.
        */
        tlen_offset = strlen(url->port)+1;
    }
    else
    {
        strlcpy(url->port, "80", sizeof(url->port));
        tlen_offset = 0;
    }

    /* Get rid of any trailing slash
    */
    if(res_url[strlen(res_url)-1] == '/')
        res_url[strlen(res_url)-1] = '\0';

    e_ndx = strchr(s_ndx, '/');
    if(e_ndx == NULL)
        tlen = strlen(s_ndx)+1;
    else
        tlen = (e_ndx-s_ndx)+1;

    tlen -= tlen_offset;

    if(tlen > MAX_URL_HOST_LEN)
    {
        log_msg(LOG_VERBOSITY_ERROR, "resolve-url hostname portion is too large.");
        return(-1);
    }
    strlcpy(url->host, s_ndx, tlen);

    if(e_ndx != NULL)
    {
        if(strlen(e_ndx) > MAX_URL_PATH_LEN)
        {
            log_msg(LOG_VERBOSITY_ERROR, "resolve-url path portion is too large.");
            return(-1);
        }

        strlcpy(url->path, e_ndx, sizeof(url->path));
    }
    else
    {
        /* default to "GET /" if there isn't a more specific URL
        */
        strlcpy(url->path, "/", sizeof(url->path));
    }

    return(0);
}

int
resolve_ip_https(fko_cli_options_t *options)
{
    int     o1, o2, o3, o4, got_resp=0, i=0;
    char   *ndx, resp[MAX_IPV4_STR_LEN+1] = {0};
    struct  url url; /* for validation only */
    char    wget_ssl_cmd[MAX_URL_PATH_LEN] = {0};  /* for verbose logging only */

#if HAVE_EXECVPE
    char   *wget_argv[MAX_CMDLINE_ARGS]; /* for execvpe() */
    int     wget_argc=0;
    int     pipe_fd[2];
    pid_t   pid=0;
    FILE   *output;
    int     status;
#else
    FILE *wget;
#endif

#if HAVE_EXECVPE
    memset(wget_argv, 0x0, sizeof(wget_argv));
#endif
    memset(&url, 0x0, sizeof(url));

    if(options->wget_bin != NULL)
    {
        strlcpy(wget_ssl_cmd, options->wget_bin, sizeof(wget_ssl_cmd));
    }
    else
    {
#ifdef WGET_EXE
        strlcpy(wget_ssl_cmd, WGET_EXE, sizeof(wget_ssl_cmd));
#else
        log_msg(LOG_VERBOSITY_ERROR,
                "[*] Use --wget-cmd <path> to specify path to the wget command.");
        return(-1);
#endif
    }

    /* See whether we're supposed to change the default wget user agent
    */
    if(! options->use_wget_user_agent)
    {
        strlcat(wget_ssl_cmd, " -U ", sizeof(wget_ssl_cmd));
        strlcat(wget_ssl_cmd, options->http_user_agent, sizeof(wget_ssl_cmd));
    }

    /* We collect the IP from wget's stdout
    */
    strlcat(wget_ssl_cmd,
            " --secure-protocol=auto --quiet -O - ", sizeof(wget_ssl_cmd));

    if(options->resolve_url != NULL)
    {
        if(strncasecmp(options->resolve_url, "https", 5) != 0)
        {
            log_msg(LOG_VERBOSITY_ERROR,
                    "[-] Warning: IP resolution URL '%s' should begin with 'https://' in -R mode.",
                    options->resolve_url);
        }

        if(parse_url(options->resolve_url, &url) < 0)
        {
            log_msg(LOG_VERBOSITY_ERROR, "Error parsing resolve-url");
            return(-1);
        }
        /* tack on the original URL to the wget command
        */
        strlcat(wget_ssl_cmd, options->resolve_url, sizeof(wget_ssl_cmd));
    }
    else
    {
        /* tack on the default URL to the wget command
        */
        strlcat(wget_ssl_cmd, WGET_RESOLVE_URL_SSL, sizeof(wget_ssl_cmd));
    }

#if HAVE_EXECVPE
    if(strtoargv(wget_ssl_cmd, wget_argv, &wget_argc, options) != 1)
    {
        log_msg(LOG_VERBOSITY_ERROR, "Error converting wget cmd str to argv");
        return(-1);
    }

    /* We drive wget to resolve the external IP via SSL. This may not
     * work on all platforms, but is a better strategy for now than
     * requiring that fwknop link against an SSL library.
    */
    if(pipe(pipe_fd) < 0)
    {
        log_msg(LOG_VERBOSITY_ERROR, "[*] pipe() error");
        free_argv(wget_argv, &wget_argc);
        return -1;
    }

    pid = fork();
    if (pid == 0)
    {
        close(pipe_fd[0]);
        dup2(pipe_fd[1], STDOUT_FILENO);
        dup2(pipe_fd[1], STDERR_FILENO);
        execvpe(wget_argv[0], wget_argv, (char * const *)NULL); /* don't use env */
    }
    else if(pid == -1)
    {
        log_msg(LOG_VERBOSITY_INFO, "[*] Could not fork() for wget.");
        free_argv(wget_argv, &wget_argc);
        return -1;
    }

    /* Only the parent process makes it here
    */
    close(pipe_fd[1]);
    if ((output = fdopen(pipe_fd[0], "r")) != NULL)
    {
        if(fgets(resp, sizeof(resp), output) != NULL)
        {
            got_resp = 1;
        }
        fclose(output);
    }
    else
    {
        log_msg(LOG_VERBOSITY_INFO,
                "[*] Could not fdopen() pipe output file descriptor.");
        free_argv(wget_argv, &wget_argc);
        return -1;
    }

    waitpid(pid, &status, 0);

    free_argv(wget_argv, &wget_argc);

#else /* fall back to popen() */
    wget = popen(wget_ssl_cmd, "r");
    if(wget == NULL)
    {
        log_msg(LOG_VERBOSITY_ERROR, "[*] Could not run cmd: %s",
                wget_ssl_cmd);
        return -1;
    }
    /* Expecting one line of wget output that contains the resolved IP.
     * */
    if ((fgets(resp, sizeof(resp), wget)) != NULL)
    {
        got_resp = 1;
    }
    pclose(wget);
#endif

    if(got_resp)
    {
        ndx = resp;
        for(i=0; i<MAX_IPV4_STR_LEN; i++) {
            if(! isdigit(*(ndx+i)) && *(ndx+i) != '.')
                break;
        }
        *(ndx+i) = '\0';

        if((sscanf(ndx, "%u.%u.%u.%u", &o1, &o2, &o3, &o4)) == 4
                && o1 >= 0 && o1 <= 255
                && o2 >= 0 && o2 <= 255
                && o3 >= 0 && o3 <= 255
                && o4 >= 0 && o4 <= 255)
        {
            strlcpy(options->allow_ip_str, ndx, sizeof(options->allow_ip_str));

            log_msg(LOG_VERBOSITY_INFO,
                        "\n[+] Resolved external IP (via '%s') as: %s",
                        wget_ssl_cmd, options->allow_ip_str);
            return 1;
        }
    }
    log_msg(LOG_VERBOSITY_ERROR,
        "[-] Could not resolve IP via: '%s'", wget_ssl_cmd);
    return -1;
}

int
resolve_ip_http(fko_cli_options_t *options)
{
    int     res;
    struct  url url;

    memset(&url, 0, sizeof(url));

    if(options->resolve_url != NULL)
    {
        /* we only enter this function when the user forces non-HTTPS
         * IP resolution
        */
        if(strncasecmp(options->resolve_url, "https", 5) == 0)
        {
            log_msg(LOG_VERBOSITY_ERROR,
                    "[*] https is not supported for --resolve-http-only.");
            return(-1);
        }

        if(parse_url(options->resolve_url, &url) < 0)
        {
            log_msg(LOG_VERBOSITY_ERROR, "Error parsing resolve-url");
            return(-1);
        }

        res = try_url(&url, options);

    } else {
        strlcpy(url.port, "80", sizeof(url.port));
        strlcpy(url.host, HTTP_RESOLVE_HOST, sizeof(url.host));
        strlcpy(url.path, HTTP_RESOLVE_URL, sizeof(url.path));

        res = try_url(&url, options);
        if(res != 1)
        {
            /* try the backup url (just switches the host to cipherdyne.com)
            */
            strlcpy(url.host, HTTP_BACKUP_RESOLVE_HOST, sizeof(url.host));

#ifndef WIN32
            sleep(2);
#endif
            res = try_url(&url, options);
        }
    }
    return(res);
}

/***EOF***/
