/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
   file Copyright.txt or https://cmake.org/licensing for details.  */
#include "cmCTestCurl.h"

#include "cmCTest.h"
#include "cmCurl.h"
#include "cmSystemTools.h"

#include <ostream>
#include <stdio.h>

cmCTestCurl::cmCTestCurl(cmCTest* ctest)
{
    this->CTest = ctest;
    this->SetProxyType();
    this->UseHttp10 = false;
    // In windows, this will init the winsock stuff
    ::curl_global_init(CURL_GLOBAL_ALL);
    // default is to verify https
    this->VerifyPeerOff  = false;
    this->VerifyHostOff  = false;
    this->Quiet          = false;
    this->TimeOutSeconds = 0;
    this->Curl           = curl_easy_init();
}

cmCTestCurl::~cmCTestCurl()
{
    ::curl_easy_cleanup(this->Curl);
    ::curl_global_cleanup();
}

std::string
cmCTestCurl::Escape(std::string const& source)
{
    char*       data1 = curl_easy_escape(this->Curl, source.c_str(), 0);
    std::string ret   = data1;
    curl_free(data1);
    return ret;
}

namespace
{
size_t
curlWriteMemoryCallback(void* ptr, size_t size, size_t nmemb, void* data)
{
    int realsize = static_cast<int>(size * nmemb);

    std::vector<char>* vec   = static_cast<std::vector<char>*>(data);
    const char*        chPtr = static_cast<char*>(ptr);
    vec->insert(vec->end(), chPtr, chPtr + realsize);
    return realsize;
}

size_t
curlDebugCallback(CURL* /*unused*/, curl_infotype /*unused*/, char* chPtr,
                  size_t size, void* data)
{
    std::vector<char>* vec = static_cast<std::vector<char>*>(data);
    vec->insert(vec->end(), chPtr, chPtr + size);

    return size;
}
}

void
cmCTestCurl::SetCurlOptions(std::vector<std::string> const& args)
{
    for(std::string const& arg : args)
    {
        if(arg == "CURLOPT_SSL_VERIFYPEER_OFF")
        {
            this->VerifyPeerOff = true;
        }
        if(arg == "CURLOPT_SSL_VERIFYHOST_OFF")
        {
            this->VerifyHostOff = true;
        }
    }
}

bool
cmCTestCurl::InitCurl()
{
    if(!this->Curl)
    {
        return false;
    }
    cmCurlSetCAInfo(this->Curl);
    if(this->VerifyPeerOff)
    {
        curl_easy_setopt(this->Curl, CURLOPT_SSL_VERIFYPEER, 0);
    }
    if(this->VerifyHostOff)
    {
        curl_easy_setopt(this->Curl, CURLOPT_SSL_VERIFYHOST, 0);
    }
    if(!this->HTTPProxy.empty())
    {
        curl_easy_setopt(this->Curl, CURLOPT_PROXY, this->HTTPProxy.c_str());
        curl_easy_setopt(this->Curl, CURLOPT_PROXYTYPE, this->HTTPProxyType);
        if(!this->HTTPProxyAuth.empty())
        {
            curl_easy_setopt(this->Curl, CURLOPT_PROXYUSERPWD,
                             this->HTTPProxyAuth.c_str());
        }
    }
    if(this->UseHttp10)
    {
        curl_easy_setopt(this->Curl, CURLOPT_HTTP_VERSION,
                         CURL_HTTP_VERSION_1_0);
    }
    // enable HTTP ERROR parsing
    curl_easy_setopt(this->Curl, CURLOPT_FAILONERROR, 1);

    // if there is little to no activity for too long stop submitting
    if(this->TimeOutSeconds)
    {
        curl_easy_setopt(this->Curl, CURLOPT_LOW_SPEED_LIMIT, 1);
        curl_easy_setopt(this->Curl, CURLOPT_LOW_SPEED_TIME,
                         this->TimeOutSeconds);
    }

    return true;
}

bool
cmCTestCurl::UploadFile(std::string const& local_file, std::string const& url,
                        std::string const& fields, std::string& response)
{
    response.clear();
    if(!this->InitCurl())
    {
        cmCTestLog(this->CTest, ERROR_MESSAGE, "Initialization of curl failed");
        return false;
    }
    /* enable uploading */
    curl_easy_setopt(this->Curl, CURLOPT_UPLOAD, 1);

    /* HTTP PUT please */
    ::curl_easy_setopt(this->Curl, CURLOPT_PUT, 1);
    ::curl_easy_setopt(this->Curl, CURLOPT_VERBOSE, 1);

    FILE* ftpfile = cmsys::SystemTools::Fopen(local_file, "rb");
    if(!ftpfile)
    {
        cmCTestLog(this->CTest, ERROR_MESSAGE,
                   "Could not open file for upload: " << local_file << "\n");
        return false;
    }
    // set the url
    std::string upload_url = url;
    upload_url += "?";
    upload_url += fields;
    ::curl_easy_setopt(this->Curl, CURLOPT_URL, upload_url.c_str());
    // now specify which file to upload
    ::curl_easy_setopt(this->Curl, CURLOPT_INFILE, ftpfile);
    unsigned long filelen = cmSystemTools::FileLength(local_file);
    // and give the size of the upload (optional)
    ::curl_easy_setopt(this->Curl, CURLOPT_INFILESIZE,
                       static_cast<long>(filelen));
    ::curl_easy_setopt(this->Curl, CURLOPT_WRITEFUNCTION,
                       curlWriteMemoryCallback);
    ::curl_easy_setopt(this->Curl, CURLOPT_DEBUGFUNCTION, curlDebugCallback);
    // Set Content-Type to satisfy fussy modsecurity rules.
    struct curl_slist* headers =
        ::curl_slist_append(nullptr, "Content-Type: text/xml");
    // Add any additional headers that the user specified.
    for(std::string const& h : this->HttpHeaders)
    {
        cmCTestOptionalLog(this->CTest, DEBUG,
                           "   Add HTTP Header: \"" << h << "\"" << std::endl,
                           this->Quiet);
        headers = ::curl_slist_append(headers, h.c_str());
    }
    ::curl_easy_setopt(this->Curl, CURLOPT_HTTPHEADER, headers);
    std::vector<char> responseData;
    std::vector<char> debugData;
    ::curl_easy_setopt(this->Curl, CURLOPT_FILE, &responseData);
    ::curl_easy_setopt(this->Curl, CURLOPT_DEBUGDATA, &debugData);
    ::curl_easy_setopt(this->Curl, CURLOPT_FAILONERROR, 1);
    // Now run off and do what you've been told!
    ::curl_easy_perform(this->Curl);
    ::fclose(ftpfile);
    ::curl_easy_setopt(this->Curl, CURLOPT_HTTPHEADER, NULL);
    ::curl_slist_free_all(headers);

    if(!responseData.empty())
    {
        response = std::string(responseData.begin(), responseData.end());
        cmCTestOptionalLog(this->CTest, HANDLER_VERBOSE_OUTPUT,
                           "Curl response: [" << response << "]\n",
                           this->Quiet);
    }
    std::string curlDebug;
    if(!debugData.empty())
    {
        curlDebug = std::string(debugData.begin(), debugData.end());
        cmCTestOptionalLog(this->CTest, DEBUG,
                           "Curl debug: [" << curlDebug << "]\n", this->Quiet);
    }
    if(response.empty())
    {
        cmCTestLog(this->CTest, ERROR_MESSAGE,
                   "No response from server.\n"
                       << curlDebug);
        return false;
    }
    return true;
}

bool
cmCTestCurl::HttpRequest(std::string const& url, std::string const& fields,
                         std::string& response)
{
    response.clear();
    cmCTestOptionalLog(this->CTest, DEBUG,
                       "HttpRequest\n"
                           << "url: " << url << "\n"
                           << "fields " << fields << "\n",
                       this->Quiet);
    if(!this->InitCurl())
    {
        cmCTestLog(this->CTest, ERROR_MESSAGE, "Initialization of curl failed");
        return false;
    }
    curl_easy_setopt(this->Curl, CURLOPT_POST, 1);
    curl_easy_setopt(this->Curl, CURLOPT_POSTFIELDS, fields.c_str());
    ::curl_easy_setopt(this->Curl, CURLOPT_URL, url.c_str());
    ::curl_easy_setopt(this->Curl, CURLOPT_FOLLOWLOCATION, 1);
    // set response options
    ::curl_easy_setopt(this->Curl, CURLOPT_WRITEFUNCTION,
                       curlWriteMemoryCallback);
    ::curl_easy_setopt(this->Curl, CURLOPT_DEBUGFUNCTION, curlDebugCallback);
    std::vector<char> responseData;
    std::vector<char> debugData;
    ::curl_easy_setopt(this->Curl, CURLOPT_FILE, &responseData);
    ::curl_easy_setopt(this->Curl, CURLOPT_DEBUGDATA, &debugData);
    ::curl_easy_setopt(this->Curl, CURLOPT_FAILONERROR, 1);

    // Add headers if any were specified.
    struct curl_slist* headers = nullptr;
    if(!this->HttpHeaders.empty())
    {
        for(std::string const& h : this->HttpHeaders)
        {
            cmCTestOptionalLog(this->CTest, DEBUG,
                               "   Add HTTP Header: \"" << h << "\""
                                                        << std::endl,
                               this->Quiet);
            headers = ::curl_slist_append(headers, h.c_str());
        }
    }

    ::curl_easy_setopt(this->Curl, CURLOPT_HTTPHEADER, headers);
    CURLcode res = ::curl_easy_perform(this->Curl);
    ::curl_slist_free_all(headers);

    if(!responseData.empty())
    {
        response = std::string(responseData.begin(), responseData.end());
        cmCTestOptionalLog(this->CTest, DEBUG,
                           "Curl response: [" << response << "]\n",
                           this->Quiet);
    }
    if(!debugData.empty())
    {
        std::string curlDebug = std::string(debugData.begin(), debugData.end());
        cmCTestOptionalLog(this->CTest, DEBUG,
                           "Curl debug: [" << curlDebug << "]\n", this->Quiet);
    }
    cmCTestOptionalLog(this->CTest, DEBUG, "Curl res: " << res << "\n",
                       this->Quiet);
    return (res == 0);
}

void
cmCTestCurl::SetProxyType()
{
    this->HTTPProxy.clear();
    // this is the default
    this->HTTPProxyType = CURLPROXY_HTTP;
    this->HTTPProxyAuth.clear();
    if(cmSystemTools::GetEnv("HTTP_PROXY", this->HTTPProxy))
    {
        std::string port;
        if(cmSystemTools::GetEnv("HTTP_PROXY_PORT", port))
        {
            this->HTTPProxy += ":";
            this->HTTPProxy += port;
        }
        std::string type;
        if(cmSystemTools::GetEnv("HTTP_PROXY_TYPE", type))
        {
            // HTTP/SOCKS4/SOCKS5
            if(type == "HTTP")
            {
                this->HTTPProxyType = CURLPROXY_HTTP;
            } else if(type == "SOCKS4")
            {
                this->HTTPProxyType = CURLPROXY_SOCKS4;
            } else if(type == "SOCKS5")
            {
                this->HTTPProxyType = CURLPROXY_SOCKS5;
            }
        }
        cmSystemTools::GetEnv("HTTP_PROXY_USER", this->HTTPProxyAuth);
        std::string passwd;
        if(cmSystemTools::GetEnv("HTTP_PROXY_PASSWD", passwd))
        {
            this->HTTPProxyAuth += ":";
            this->HTTPProxyAuth += passwd;
        }
    }
}
