/*
 * Copyright (C) 2013 Apple Inc.  All rights reserved.
 * Copyright (C) 2018 Sony Interactive Entertainment Inc.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 */

#include "config.h"
#include "CurlContext.h"

#if USE(CURL)
#include "CertificateInfo.h"
#include "CurlRequestScheduler.h"
#include "CurlSSLHandle.h"
#include "CurlSSLVerifier.h"
#include "HTTPHeaderMap.h"
#include <NetworkLoadMetrics.h>
#include <mutex>
#include <wtf/MainThread.h>
#include <wtf/NeverDestroyed.h>
#include <wtf/text/CString.h>

#if OS(WINDOWS)
#include "../../win/WebCoreBundleWin.h"
#include <shlobj.h>
#include <shlwapi.h>
#endif

namespace WebCore {

class EnvironmentVariableReader {
public:
    const char* read(const char* name) { return ::getenv(name); }
    bool defined(const char* name) { return read(name) != nullptr; }

    template<typename T> Optional<T> readAs(const char* name)
    {
        if (const char* valueStr = read(name)) {
            T value;
            if (sscanf(valueStr, sscanTemplate<T>(), &value) == 1)
                return value;
        }

        return WTF::nullopt;
    }

private:
    template<typename T> const char* sscanTemplate()
    {
        ASSERT_NOT_REACHED();
        return nullptr;
    }
};

template<>
constexpr const char* EnvironmentVariableReader::sscanTemplate<signed>() { return "%d"; }

template<>
constexpr const char* EnvironmentVariableReader::sscanTemplate<unsigned>() { return "%u"; }

// ALPN Protocol ID (RFC7301) https://tools.ietf.org/html/rfc7301
static const ASCIILiteral httpVersion10 { "http/1.0"_s };
static const ASCIILiteral httpVersion11 { "http/1.1"_s };
static const ASCIILiteral httpVersion2 { "h2"_s };

// CurlContext -------------------------------------------------------------------

CurlContext& CurlContext::singleton()
{
    static NeverDestroyed<CurlContext> sharedInstance;
    return sharedInstance;
}

CurlContext::CurlContext()
{
    initShareHandle();

    EnvironmentVariableReader envVar;

    if (auto value = envVar.readAs<unsigned>("WEBKIT_CURL_DNS_CACHE_TIMEOUT"))
        m_dnsCacheTimeout = Seconds(*value);

    if (auto value = envVar.readAs<unsigned>("WEBKIT_CURL_CONNECT_TIMEOUT"))
        m_connectTimeout = Seconds(*value);

    long maxConnects { CurlDefaultMaxConnects };
    long maxTotalConnections { CurlDefaultMaxTotalConnections };
    long maxHostConnections { CurlDefaultMaxHostConnections };

    if (auto value = envVar.readAs<signed>("WEBKIT_CURL_MAXCONNECTS"))
        maxConnects = *value;

    if (auto value = envVar.readAs<signed>("WEBKIT_CURL_MAX_TOTAL_CONNECTIONS"))
        maxTotalConnections = *value;

    if (auto value = envVar.readAs<signed>("WEBKIT_CURL_MAX_HOST_CONNECTIONS"))
        maxHostConnections = *value;

    m_scheduler = std::make_unique<CurlRequestScheduler>(maxConnects, maxTotalConnections, maxHostConnections);

#ifndef NDEBUG
    m_verbose = envVar.defined("DEBUG_CURL");

    auto logFile = envVar.read("CURL_LOG_FILE");
    if (logFile)
        m_logFile = fopen(logFile, "a");
#endif
}

CurlContext::~CurlContext()
{
#ifndef NDEBUG
    if (m_logFile)
        fclose(m_logFile);
#endif
}

void CurlContext::initShareHandle()
{
    CURL* curl = curl_easy_init();

    if (!curl)
        return;

    curl_easy_setopt(curl, CURLOPT_SHARE, m_shareHandle.handle());

    curl_easy_cleanup(curl);
}

bool CurlContext::isHttp2Enabled() const
{
    curl_version_info_data* data = curl_version_info(CURLVERSION_NOW);
    return data->features & CURL_VERSION_HTTP2;
}

// CurlShareHandle --------------------------------------------

CurlShareHandle::CurlShareHandle()
{
    m_shareHandle = curl_share_init();
    curl_share_setopt(m_shareHandle, CURLSHOPT_SHARE, CURL_LOCK_DATA_COOKIE);
    curl_share_setopt(m_shareHandle, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS);
    curl_share_setopt(m_shareHandle, CURLSHOPT_LOCKFUNC, lockCallback);
    curl_share_setopt(m_shareHandle, CURLSHOPT_UNLOCKFUNC, unlockCallback);
}

CurlShareHandle::~CurlShareHandle()
{
    if (m_shareHandle)
        curl_share_cleanup(m_shareHandle);
}

void CurlShareHandle::lockCallback(CURL*, curl_lock_data data, curl_lock_access, void*)
{
    if (auto* mutex = mutexFor(data))
        mutex->lock();
}

void CurlShareHandle::unlockCallback(CURL*, curl_lock_data data, void*)
{
    if (auto* mutex = mutexFor(data))
        mutex->unlock();
}

Lock* CurlShareHandle::mutexFor(curl_lock_data data)
{
    static Lock cookieMutex;
    static Lock dnsMutex;
    static Lock shareMutex;

    switch (data) {
    case CURL_LOCK_DATA_COOKIE:
        return &cookieMutex;
    case CURL_LOCK_DATA_DNS:
        return &dnsMutex;
    case CURL_LOCK_DATA_SHARE:
        return &shareMutex;
    default:
        ASSERT_NOT_REACHED();
        return nullptr;
    }
}

// CurlMultiHandle --------------------------------------------

CurlMultiHandle::CurlMultiHandle()
{
    m_multiHandle = curl_multi_init();

    if (CurlContext::singleton().isHttp2Enabled())
        curl_multi_setopt(m_multiHandle, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX);
}

CurlMultiHandle::~CurlMultiHandle()
{
    if (m_multiHandle)
        curl_multi_cleanup(m_multiHandle);
}

void CurlMultiHandle::setMaxConnects(long maxConnects)
{
    if (maxConnects < 0)
        return;

    curl_multi_setopt(m_multiHandle, CURLMOPT_MAXCONNECTS, maxConnects);
}

void CurlMultiHandle::setMaxTotalConnections(long maxTotalConnections)
{
    curl_multi_setopt(m_multiHandle, CURLMOPT_MAX_TOTAL_CONNECTIONS, maxTotalConnections);
}

void CurlMultiHandle::setMaxHostConnections(long maxHostConnections)
{
    curl_multi_setopt(m_multiHandle, CURLMOPT_MAX_HOST_CONNECTIONS, maxHostConnections);
}

CURLMcode CurlMultiHandle::addHandle(CURL* handle)
{
    return curl_multi_add_handle(m_multiHandle, handle);
}

CURLMcode CurlMultiHandle::removeHandle(CURL* handle)
{
    return curl_multi_remove_handle(m_multiHandle, handle);
}

CURLMcode CurlMultiHandle::getFdSet(fd_set& readFdSet, fd_set& writeFdSet, fd_set& excFdSet, int& maxFd)
{
    FD_ZERO(&readFdSet);
    FD_ZERO(&writeFdSet);
    FD_ZERO(&excFdSet);
    maxFd = 0;

    return curl_multi_fdset(m_multiHandle, &readFdSet, &writeFdSet, &excFdSet, &maxFd);
}

CURLMcode CurlMultiHandle::perform(int& runningHandles)
{
    return curl_multi_perform(m_multiHandle, &runningHandles);
}

CURLMsg* CurlMultiHandle::readInfo(int& messagesInQueue)
{
    return curl_multi_info_read(m_multiHandle, &messagesInQueue);
}

// CurlHandle -------------------------------------------------

CurlHandle::CurlHandle()
{
    m_handle = curl_easy_init();
    curl_easy_setopt(m_handle, CURLOPT_ERRORBUFFER, m_errorBuffer);

    enableShareHandle();
    enableAllowedProtocols();
    enableAcceptEncoding();

    setDnsCacheTimeout(CurlContext::singleton().dnsCacheTimeout());
    setConnectTimeout(CurlContext::singleton().connectTimeout());

    enableProxyIfExists();

#ifndef NDEBUG
    enableVerboseIfUsed();
    enableStdErrIfUsed();
#endif
}

CurlHandle::~CurlHandle()
{
    if (m_handle)
        curl_easy_cleanup(m_handle);
}

const String CurlHandle::errorDescription(CURLcode errorCode)
{
    return String(curl_easy_strerror(errorCode));
}

void CurlHandle::enableSSLForHost(const String& host)
{
    auto& sslHandle = CurlContext::singleton().sslHandle();
    if (auto sslClientCertificate = sslHandle.getSSLClientCertificate(host)) {
        setSslCert(sslClientCertificate->first.utf8().data());
        setSslCertType("P12");
        setSslKeyPassword(sslClientCertificate->second.utf8().data());
    }

    if (sslHandle.canIgnoreAnyHTTPSCertificatesForHost(host) || sslHandle.shouldIgnoreSSLErrors()) {
        setSslVerifyPeer(CurlHandle::VerifyPeer::Disable);
        setSslVerifyHost(CurlHandle::VerifyHost::LooseNameCheck);
    } else {
        setSslVerifyPeer(CurlHandle::VerifyPeer::Enable);
        setSslVerifyHost(CurlHandle::VerifyHost::StrictNameCheck);
    }

    const auto& cipherList = sslHandle.getCipherList();
    if (!cipherList.isEmpty())
        setSslCipherList(cipherList.utf8().data());

    setSslCtxCallbackFunction(willSetupSslCtxCallback, this);

    if (auto* path = WTF::get_if<String>(sslHandle.getCACertInfo()))
        setCACertPath(path->utf8().data());
}

void CurlHandle::disableServerTrustEvaluation()
{
    setSslVerifyPeer(CurlHandle::VerifyPeer::Disable);
    setSslVerifyHost(CurlHandle::VerifyHost::LooseNameCheck);
}

CURLcode CurlHandle::willSetupSslCtx(void* sslCtx)
{
    if (!sslCtx)
        return CURLE_ABORTED_BY_CALLBACK;

    if (!m_sslVerifier)
        m_sslVerifier = std::make_unique<CurlSSLVerifier>(sslCtx);

    return CURLE_OK;
}

CURLcode CurlHandle::willSetupSslCtxCallback(CURL*, void* sslCtx, void* userData)
{
    return static_cast<CurlHandle*>(userData)->willSetupSslCtx(sslCtx);
}

int CurlHandle::sslErrors() const
{
    return m_sslVerifier ? m_sslVerifier->sslErrors() : 0;
}

CURLcode CurlHandle::perform()
{
    return curl_easy_perform(m_handle);
}

CURLcode CurlHandle::pause(int bitmask)
{
    return curl_easy_pause(m_handle, bitmask);
}

void CurlHandle::enableShareHandle()
{
    curl_easy_setopt(m_handle, CURLOPT_SHARE, CurlContext::singleton().shareHandle().handle());
}

void CurlHandle::setUrl(const URL& url)
{
    m_url = url.isolatedCopy();

    URL curlUrl = url;

    // Remove any fragment part, otherwise curl will send it as part of the request.
    curlUrl.removeFragmentIdentifier();

    // Remove any query part sent to a local file.
    if (curlUrl.isLocalFile()) {
        // By setting the query to a null string it'll be removed.
        if (!curlUrl.query().isEmpty())
            curlUrl.setQuery(String());
    }

    // url is in ASCII so latin1() will only convert it to char* without character translation.
    curl_easy_setopt(m_handle, CURLOPT_URL, curlUrl.string().latin1().data());

    if (url.protocolIs("https"))
        enableSSLForHost(m_url.host().toString());
}

void CurlHandle::appendRequestHeaders(const HTTPHeaderMap& headers)
{
    if (headers.size()) {
        for (auto& entry : headers)
            appendRequestHeader(entry.key, entry.value);
    }
}

void CurlHandle::appendRequestHeader(const String& name, const String& value)
{
    String header(name);

    if (value.isEmpty()) {
        // Insert the ; to tell curl that this header has an empty value.
        header.append(";");
    } else {
        header.append(": ");
        header.append(value);
    }

    appendRequestHeader(header);
}

void CurlHandle::removeRequestHeader(const String& name)
{
    // Add a header with no content, the internally used header will get disabled. 
    String header(name);
    header.append(":");

    appendRequestHeader(header);
}

void CurlHandle::appendRequestHeader(const String& header)
{
    bool needToEnable = m_requestHeaders.isEmpty();

    m_requestHeaders.append(header);

    if (needToEnable)
        enableRequestHeaders();
}

void CurlHandle::enableRequestHeaders()
{
    if (m_requestHeaders.isEmpty())
        return;

    const struct curl_slist* headers = m_requestHeaders.head();
    curl_easy_setopt(m_handle, CURLOPT_HTTPHEADER, headers);
}

void CurlHandle::enableHttp()
{
    if (m_url.protocolIs("https") && CurlContext::singleton().isHttp2Enabled()) {
        curl_easy_setopt(m_handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS);
        curl_easy_setopt(m_handle, CURLOPT_PIPEWAIT, 1L);
        curl_easy_setopt(m_handle, CURLOPT_SSL_ENABLE_ALPN, 1L);
        curl_easy_setopt(m_handle, CURLOPT_SSL_ENABLE_NPN, 0L);
    } else
        curl_easy_setopt(m_handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
}

void CurlHandle::enableHttpGetRequest()
{
    enableHttp();
    curl_easy_setopt(m_handle, CURLOPT_HTTPGET, 1L);
}

void CurlHandle::enableHttpHeadRequest()
{
    enableHttp();
    curl_easy_setopt(m_handle, CURLOPT_NOBODY, 1L);
}

void CurlHandle::enableHttpPostRequest()
{
    enableHttp();
    curl_easy_setopt(m_handle, CURLOPT_POST, 1L);
    curl_easy_setopt(m_handle, CURLOPT_POSTFIELDSIZE, 0L);
}

void CurlHandle::setPostFields(const char* data, long size)
{
    curl_easy_setopt(m_handle, CURLOPT_POSTFIELDS, data);
    curl_easy_setopt(m_handle, CURLOPT_POSTFIELDSIZE, size);
}

void CurlHandle::setPostFieldLarge(curl_off_t size)
{
    if (expectedSizeOfCurlOffT() != sizeof(long long))
        size = static_cast<int>(size);

    curl_easy_setopt(m_handle, CURLOPT_POSTFIELDSIZE_LARGE, size);
}

void CurlHandle::enableHttpPutRequest()
{
    enableHttp();
    curl_easy_setopt(m_handle, CURLOPT_UPLOAD, 1L);
    curl_easy_setopt(m_handle, CURLOPT_INFILESIZE, 0L);
}

void CurlHandle::setInFileSizeLarge(curl_off_t size)
{
    if (expectedSizeOfCurlOffT() != sizeof(long long))
        size = static_cast<int>(size);

    curl_easy_setopt(m_handle, CURLOPT_INFILESIZE_LARGE, size);
}

void CurlHandle::setHttpCustomRequest(const String& method)
{
    enableHttp();
    curl_easy_setopt(m_handle, CURLOPT_CUSTOMREQUEST, method.ascii().data());
}

void CurlHandle::enableAcceptEncoding()
{
    // enable all supported built-in compressions (gzip and deflate) through Accept-Encoding:
    curl_easy_setopt(m_handle, CURLOPT_ENCODING, "");
}

void CurlHandle::enableAllowedProtocols()
{
    static const long allowedProtocols = CURLPROTO_FILE | CURLPROTO_FTP | CURLPROTO_FTPS | CURLPROTO_HTTP | CURLPROTO_HTTPS;

    curl_easy_setopt(m_handle, CURLOPT_PROTOCOLS, allowedProtocols);
}

void CurlHandle::setHttpAuthUserPass(const String& user, const String& password, long authType)
{
    curl_easy_setopt(m_handle, CURLOPT_USERNAME, user.utf8().data());
    curl_easy_setopt(m_handle, CURLOPT_PASSWORD, password.utf8().data());
    curl_easy_setopt(m_handle, CURLOPT_HTTPAUTH, authType);
}

void CurlHandle::setCACertPath(const char* path)
{
    if (path)
        curl_easy_setopt(m_handle, CURLOPT_CAINFO, path);
}

void CurlHandle::setSslVerifyPeer(VerifyPeer verifyPeer)
{
    curl_easy_setopt(m_handle, CURLOPT_SSL_VERIFYPEER, static_cast<long>(verifyPeer));
}

void CurlHandle::setSslVerifyHost(VerifyHost verifyHost)
{
    curl_easy_setopt(m_handle, CURLOPT_SSL_VERIFYHOST, static_cast<long>(verifyHost));
}

void CurlHandle::setSslCert(const char* cert)
{
    curl_easy_setopt(m_handle, CURLOPT_SSLCERT, cert);
}

void CurlHandle::setSslCertType(const char* type)
{
    curl_easy_setopt(m_handle, CURLOPT_SSLCERTTYPE, type);
}

void CurlHandle::setSslKeyPassword(const char* password)
{
    curl_easy_setopt(m_handle, CURLOPT_KEYPASSWD, password);
}

void CurlHandle::setSslCipherList(const char* cipherList)
{
    curl_easy_setopt(m_handle, CURLOPT_SSL_CIPHER_LIST, cipherList);
}

void CurlHandle::enableProxyIfExists()
{
    auto& proxy = CurlContext::singleton().proxySettings();

    switch (proxy.mode()) {
    case CurlProxySettings::Mode::Default :
        // For the proxy set by environment variable
        if (!proxy.user().isEmpty())
            curl_easy_setopt(m_handle, CURLOPT_PROXYUSERNAME, proxy.user().utf8().data());
        if (!proxy.password().isEmpty())
            curl_easy_setopt(m_handle, CURLOPT_PROXYPASSWORD, proxy.password().utf8().data());
        curl_easy_setopt(m_handle, CURLOPT_PROXYAUTH, proxy.authMethod());
        break;
    case CurlProxySettings::Mode::NoProxy :
        // Disable the use of a proxy, even if there is an environment variable set for it.
        curl_easy_setopt(m_handle, CURLOPT_PROXY, "");
        break;
    case CurlProxySettings::Mode::Custom :
        curl_easy_setopt(m_handle, CURLOPT_PROXY, proxy.url().utf8().data());
        curl_easy_setopt(m_handle, CURLOPT_NOPROXY, proxy.ignoreHosts().utf8().data());
        curl_easy_setopt(m_handle, CURLOPT_PROXYAUTH, proxy.authMethod());
        break;
    }
}

static CURLoption safeTimeValue(double time)
{
    auto value = static_cast<unsigned>(time >= 0.0 ? time : 0);
    return static_cast<CURLoption>(value);
}

void CurlHandle::setDnsCacheTimeout(Seconds timeout)
{
    curl_easy_setopt(m_handle, CURLOPT_DNS_CACHE_TIMEOUT, safeTimeValue(timeout.seconds()));
}

void CurlHandle::setConnectTimeout(Seconds timeout)
{
    curl_easy_setopt(m_handle, CURLOPT_CONNECTTIMEOUT, safeTimeValue(timeout.seconds()));
}

void CurlHandle::setTimeout(Seconds timeout)
{
    // Originally CURLOPT_TIMEOUT_MS was used here, but that is not the
    // idle timeout, but entire duration time limit. It's not safe to specify
    // such a time limit for communications, such as downloading.
    // CURLOPT_LOW_SPEED_LIMIT is used instead. It enables the speed watcher
    // and if the speed is below specified limit and last for specified duration,
    // it invokes timeout error.
    curl_easy_setopt(m_handle, CURLOPT_LOW_SPEED_LIMIT, 1L);
    curl_easy_setopt(m_handle, CURLOPT_LOW_SPEED_TIME, safeTimeValue(timeout.seconds()));
}

void CurlHandle::setHeaderCallbackFunction(curl_write_callback callbackFunc, void* userData)
{
    curl_easy_setopt(m_handle, CURLOPT_HEADERFUNCTION, callbackFunc);
    curl_easy_setopt(m_handle, CURLOPT_HEADERDATA, userData);
}

void CurlHandle::setWriteCallbackFunction(curl_write_callback callbackFunc, void* userData)
{
    curl_easy_setopt(m_handle, CURLOPT_WRITEFUNCTION, callbackFunc);
    curl_easy_setopt(m_handle, CURLOPT_WRITEDATA, userData);
}

void CurlHandle::setReadCallbackFunction(curl_read_callback callbackFunc, void* userData)
{
    curl_easy_setopt(m_handle, CURLOPT_READFUNCTION, callbackFunc);
    curl_easy_setopt(m_handle, CURLOPT_READDATA, userData);
}

void CurlHandle::setSslCtxCallbackFunction(curl_ssl_ctx_callback callbackFunc, void* userData)
{
    curl_easy_setopt(m_handle, CURLOPT_SSL_CTX_DATA, userData);
    curl_easy_setopt(m_handle, CURLOPT_SSL_CTX_FUNCTION, callbackFunc);
}

void CurlHandle::setDebugCallbackFunction(curl_debug_callback callbackFunc, void* userData)
{
    curl_easy_setopt(m_handle, CURLOPT_DEBUGFUNCTION, callbackFunc);
    curl_easy_setopt(m_handle, CURLOPT_DEBUGDATA, userData);
    curl_easy_setopt(m_handle, CURLOPT_VERBOSE, 1L);
}

void CurlHandle::enableConnectionOnly()
{
    curl_easy_setopt(m_handle, CURLOPT_CONNECT_ONLY, 1L);
}

Optional<String> CurlHandle::getProxyUrl()
{
    auto& proxy = CurlContext::singleton().proxySettings();
    if (proxy.mode() == CurlProxySettings::Mode::Default)
        return WTF::nullopt;

    return proxy.url();
}

Optional<long> CurlHandle::getResponseCode()
{
    if (!m_handle)
        return WTF::nullopt;

    long responseCode;
    CURLcode errorCode = curl_easy_getinfo(m_handle, CURLINFO_RESPONSE_CODE, &responseCode);
    if (errorCode != CURLE_OK)
        return WTF::nullopt;

    return responseCode;
}

Optional<long> CurlHandle::getHttpConnectCode()
{
    if (!m_handle)
        return WTF::nullopt;

    long httpConnectCode;
    CURLcode errorCode = curl_easy_getinfo(m_handle, CURLINFO_HTTP_CONNECTCODE, &httpConnectCode);
    if (errorCode != CURLE_OK)
        return WTF::nullopt;

    return httpConnectCode;
}

Optional<long long> CurlHandle::getContentLength()
{
    if (!m_handle)
        return WTF::nullopt;

    double contentLength;

    CURLcode errorCode = curl_easy_getinfo(m_handle, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &contentLength);
    if (errorCode != CURLE_OK)
        return WTF::nullopt;

    return static_cast<long long>(contentLength);
}

Optional<long> CurlHandle::getHttpAuthAvail()
{
    if (!m_handle)
        return WTF::nullopt;

    long httpAuthAvailable;
    CURLcode errorCode = curl_easy_getinfo(m_handle, CURLINFO_HTTPAUTH_AVAIL, &httpAuthAvailable);
    if (errorCode != CURLE_OK)
        return WTF::nullopt;

    return httpAuthAvailable;
}

Optional<long> CurlHandle::getProxyAuthAvail()
{
    if (!m_handle)
        return WTF::nullopt;

    long proxyAuthAvailable;
    CURLcode errorCode = curl_easy_getinfo(m_handle, CURLINFO_PROXYAUTH_AVAIL, &proxyAuthAvailable);
    if (errorCode != CURLE_OK)
        return WTF::nullopt;

    return proxyAuthAvailable;
}

Optional<long> CurlHandle::getHttpVersion()
{
    if (!m_handle)
        return WTF::nullopt;

    long version;
    CURLcode errorCode = curl_easy_getinfo(m_handle, CURLINFO_HTTP_VERSION, &version);
    if (errorCode != CURLE_OK)
        return WTF::nullopt;

    return version;
}

Optional<NetworkLoadMetrics> CurlHandle::getNetworkLoadMetrics(const WTF::Seconds& domainLookupStart)
{
    double nameLookup = 0.0;
    double connect = 0.0;
    double appConnect = 0.0;
    double startTransfer = 0.0;
    long version = 0;

    if (!m_handle)
        return WTF::nullopt;

    CURLcode errorCode = curl_easy_getinfo(m_handle, CURLINFO_NAMELOOKUP_TIME, &nameLookup);
    if (errorCode != CURLE_OK)
        return WTF::nullopt;

    errorCode = curl_easy_getinfo(m_handle, CURLINFO_CONNECT_TIME, &connect);
    if (errorCode != CURLE_OK)
        return WTF::nullopt;

    errorCode = curl_easy_getinfo(m_handle, CURLINFO_APPCONNECT_TIME, &appConnect);
    if (errorCode != CURLE_OK)
        return WTF::nullopt;

    errorCode = curl_easy_getinfo(m_handle, CURLINFO_STARTTRANSFER_TIME, &startTransfer);
    if (errorCode != CURLE_OK)
        return WTF::nullopt;

    errorCode = curl_easy_getinfo(m_handle, CURLINFO_HTTP_VERSION, &version);
    if (errorCode != CURLE_OK)
        return WTF::nullopt;

    NetworkLoadMetrics networkLoadMetrics;

    networkLoadMetrics.domainLookupStart = domainLookupStart;
    networkLoadMetrics.domainLookupEnd = domainLookupStart + Seconds(nameLookup);
    networkLoadMetrics.connectStart = domainLookupStart + Seconds(nameLookup);
    networkLoadMetrics.connectEnd = domainLookupStart + Seconds(connect);

    if (appConnect > 0.0) {
        networkLoadMetrics.secureConnectionStart = domainLookupStart + Seconds(connect);
        networkLoadMetrics.connectEnd = domainLookupStart + Seconds(appConnect);
    }

    networkLoadMetrics.requestStart = networkLoadMetrics.connectEnd;
    networkLoadMetrics.responseStart = domainLookupStart + Seconds(startTransfer);

    if (version == CURL_HTTP_VERSION_1_0)
        networkLoadMetrics.protocol = httpVersion10;
    else if (version == CURL_HTTP_VERSION_1_1)
        networkLoadMetrics.protocol = httpVersion11;
    else if (version == CURL_HTTP_VERSION_2)
        networkLoadMetrics.protocol = httpVersion2;

    return networkLoadMetrics;
}

void CurlHandle::addExtraNetworkLoadMetrics(NetworkLoadMetrics& networkLoadMetrics)
{
    long requestHeaderSize = 0;
    curl_off_t requestBodySize = 0;
    long responseHeaderSize = 0;
    curl_off_t responseBodySize = 0;
    char* ip = nullptr;
    long port = 0;

    // FIXME: Gets total request size not just headers https://bugs.webkit.org/show_bug.cgi?id=188363
    CURLcode errorCode = curl_easy_getinfo(m_handle, CURLINFO_REQUEST_SIZE, &requestHeaderSize);
    if (errorCode != CURLE_OK)
        return;

    errorCode = curl_easy_getinfo(m_handle, CURLINFO_SIZE_UPLOAD_T, &requestBodySize);
    if (errorCode != CURLE_OK)
        return;

    errorCode = curl_easy_getinfo(m_handle, CURLINFO_HEADER_SIZE, &responseHeaderSize);
    if (errorCode != CURLE_OK)
        return;

    errorCode = curl_easy_getinfo(m_handle, CURLINFO_SIZE_DOWNLOAD_T, &responseBodySize);
    if (errorCode != CURLE_OK)
        return;

    errorCode = curl_easy_getinfo(m_handle, CURLINFO_PRIMARY_IP, &ip);
    if (errorCode != CURLE_OK)
        return;

    errorCode = curl_easy_getinfo(m_handle, CURLINFO_PRIMARY_PORT, &port);
    if (errorCode != CURLE_OK)
        return;

    networkLoadMetrics.requestHeaderBytesSent = requestHeaderSize;
    networkLoadMetrics.requestBodyBytesSent = requestBodySize;
    networkLoadMetrics.responseHeaderBytesReceived = responseHeaderSize;
    networkLoadMetrics.responseBodyBytesReceived = responseBodySize;

    if (ip) {
        networkLoadMetrics.remoteAddress = String(ip);
        if (port)
            networkLoadMetrics.remoteAddress.append(":" + String::number(port));
    }
}

Optional<CertificateInfo> CurlHandle::certificateInfo() const
{
    if (!m_sslVerifier)
        return WTF::nullopt;

    return m_sslVerifier->certificateInfo();
}

long long CurlHandle::maxCurlOffT()
{
    static const long long maxCurlOffT = (1LL << (expectedSizeOfCurlOffT() * 8 - 1)) - 1;

    return maxCurlOffT;
}

int CurlHandle::expectedSizeOfCurlOffT()
{
    // The size of a curl_off_t could be different in WebKit and in cURL depending on
    // compilation flags of both.
    static int expectedSizeOfCurlOffT = 0;
    if (!expectedSizeOfCurlOffT) {
        curl_version_info_data* infoData = curl_version_info(CURLVERSION_NOW);
        if (infoData->features & CURL_VERSION_LARGEFILE)
            expectedSizeOfCurlOffT = sizeof(long long);
        else
            expectedSizeOfCurlOffT = sizeof(int);
    }

    return expectedSizeOfCurlOffT;
}

#ifndef NDEBUG

void CurlHandle::enableVerboseIfUsed()
{
    if (CurlContext::singleton().isVerbose())
        curl_easy_setopt(m_handle, CURLOPT_VERBOSE, 1);
}

void CurlHandle::enableStdErrIfUsed()
{
    if (auto log = CurlContext::singleton().getLogFile())
        curl_easy_setopt(m_handle, CURLOPT_STDERR, log);
}

#endif

// CurlSocketHandle

CurlSocketHandle::CurlSocketHandle(const URL& url, Function<void(CURLcode)>&& errorHandler)
    : m_errorHandler(WTFMove(errorHandler))
{
    // Libcurl is not responsible for the protocol handling. It just handles connection.
    // Only scheme, host and port is required.
    URL urlForConnection;
    urlForConnection.setProtocol(url.protocolIs("wss") ? "https" : "http");
    urlForConnection.setHostAndPort(url.hostAndPort());
    setUrl(urlForConnection);

    enableConnectionOnly();
}

bool CurlSocketHandle::connect()
{
    CURLcode errorCode = perform();
    if (errorCode != CURLE_OK) {
        m_errorHandler(errorCode);
        return false;
    }

    return true;
}

size_t CurlSocketHandle::send(const uint8_t* buffer, size_t size)
{
    size_t totalBytesSent = 0;

    while (totalBytesSent < size) {
        size_t bytesSent = 0;
        CURLcode errorCode = curl_easy_send(handle(), buffer + totalBytesSent, size - totalBytesSent, &bytesSent);
        if (errorCode != CURLE_OK) {
            if (errorCode != CURLE_AGAIN)
                m_errorHandler(errorCode);
            break;
        }

        totalBytesSent += bytesSent;
    }

    return totalBytesSent;
}

Optional<size_t> CurlSocketHandle::receive(uint8_t* buffer, size_t bufferSize)
{
    size_t bytesRead = 0;

    CURLcode errorCode = curl_easy_recv(handle(), buffer, bufferSize, &bytesRead);
    if (errorCode != CURLE_OK) {
        if (errorCode != CURLE_AGAIN)
            m_errorHandler(errorCode);

        return WTF::nullopt;
    }

    return bytesRead;
}

Optional<CurlSocketHandle::WaitResult> CurlSocketHandle::wait(const Seconds& timeout, bool alsoWaitForWrite)
{
    curl_socket_t socket;
    CURLcode errorCode = curl_easy_getinfo(handle(), CURLINFO_ACTIVESOCKET, &socket);
    if (errorCode != CURLE_OK) {
        m_errorHandler(errorCode);
        return WTF::nullopt;
    }

    int64_t usec = timeout.microsecondsAs<int64_t>();

    struct timeval selectTimeout;
    if (usec <= 0) {
        selectTimeout.tv_sec = 0;
        selectTimeout.tv_usec = 0;
    } else {
        selectTimeout.tv_sec = usec / 1000000;
        selectTimeout.tv_usec = usec % 1000000;
    }

    int rc = 0;
    int maxfd = static_cast<int>(socket) + 1;
    fd_set fdread;
    fd_set fdwrite;
    fd_set fderr;

    // Retry 'select' if it was interrupted by a process signal.
    do {
        FD_ZERO(&fdread);
        FD_SET(socket, &fdread);

        FD_ZERO(&fdwrite);
        if (alsoWaitForWrite)
            FD_SET(socket, &fdwrite);

        FD_ZERO(&fderr);
        FD_SET(socket, &fderr);

        rc = ::select(maxfd, &fdread, &fdwrite, &fderr, &selectTimeout);
    } while (rc == -1 && errno == EINTR);

    if (rc <= 0)
        return WTF::nullopt;

    WaitResult result;
    result.readable = FD_ISSET(socket, &fdread) || FD_ISSET(socket, &fderr);
    result.writable = FD_ISSET(socket, &fdwrite);
    return result;
}

}

#endif
