audacia/libraries/lib-network-manager/curl/CurlHandleManager.cpp

350 lines
8.5 KiB
C++

/*!********************************************************************
Audacity: A Digital Audio Editor
@file CurlHandleManager.cpp
@brief Define a class responsible for reuse of CURL hanldes.
Dmitry Vedenko
**********************************************************************/
#include "CurlHandleManager.h"
#include <algorithm>
#include <sstream>
#include <wx/platinfo.h>
namespace audacity
{
namespace network_manager
{
namespace
{
void GetOSString (std::ostringstream& output, const wxPlatformInfo& platformInfo)
{
const wxOperatingSystemId osID = platformInfo.GetOperatingSystemId ();
if (osID & wxOS_WINDOWS)
output << "Windows ";
else if (osID & wxOS_MAC)
output << "MacOS ";
else if (osID & wxOS_UNIX_LINUX)
output << "Linux ";
else if (osID & wxOS_UNIX_FREEBSD)
output << "FreeBSD ";
else if (osID & wxOS_UNIX_OPENBSD)
output << "OpenBSD ";
else
output << "Other ";
output <<
platformInfo.GetOSMajorVersion () <<
"_" <<
platformInfo.GetOSMinorVersion () <<
"_" <<
platformInfo.GetOSMicroVersion() <<
"; ";
#if defined(__amd64__) || defined(__x86_64__) || defined(_M_X64)
output << "x64";
#elif defined(__i386__) || defined(i386) || defined(_M_IX86) || defined(_X86_) || defined(__THW_INTEL)
output << "x86";
#elif defined(__arm64__) || defined(__aarch64__)
output << "arm64";
#elif defined(arm) || defined(__arm__) || defined(ARM) || defined(_ARM_)
output << "arm";
#else
output << "unknown";
#endif
}
}
constexpr std::chrono::milliseconds CurlHandleManager::KEEP_ALIVE_IDLE;
constexpr std::chrono::milliseconds CurlHandleManager::KEEP_ALIVE_PROBE;
CurlHandleManager::Handle::Handle(CurlHandleManager* owner, CURL* handle, RequestVerb verb, std::string url) noexcept
: mHandle (handle),
mOwner (owner),
mVerb (verb),
mUrl (std::move (url)),
mHandleFromCache (handle != nullptr)
{
if (mHandle == nullptr)
mHandle = curl_easy_init ();
setOption (CURLOPT_URL, mUrl);
switch (verb)
{
case RequestVerb::Head:
setOption (CURLOPT_NOBODY, 1);
break;
case RequestVerb::Get:
// This is a default, no additional setup is needed
// We cache handles by the verb, so there is no need to
// reset the handle state
break;
case RequestVerb::Post:
setOption (CURLOPT_POST, 1);
break;
case RequestVerb::Put:
setOption (CURLOPT_UPLOAD, 1);
break;
case RequestVerb::Delete:
setOption (CURLOPT_CUSTOMREQUEST, "DELETE");
break;
}
setOption (CURLOPT_NOSIGNAL, 1L);
setOption (CURLOPT_SSL_VERIFYPEER, 1L);
setOption (CURLOPT_SSL_VERIFYHOST, 2L);
setOption (CURLOPT_ACCEPT_ENCODING, "");
}
CurlHandleManager::Handle::Handle (Handle&& rhs) noexcept
{
*this = std::move (rhs);
}
CurlHandleManager::Handle::~Handle () noexcept
{
if (mReuse)
mOwner->cacheHandle (*this);
else
curl_easy_cleanup (mHandle);
}
CurlHandleManager::Handle& CurlHandleManager::Handle::operator=(Handle&& rhs) noexcept
{
std::swap (mHandle, rhs.mHandle);
std::swap (mOwner, rhs.mOwner);
std::swap (mVerb, rhs.mVerb);
mUrl = std::move (rhs.mUrl);
mHeaders = std::move (rhs.mHeaders);
mReuse = rhs.mReuse;
rhs.mReuse = false;
return *this;
}
CURLcode CurlHandleManager::Handle::setOption (CURLoption option, const std::string& value) noexcept
{
return setOption (option, value.c_str ());
}
CURLcode CurlHandleManager::Handle::appendCookie (const Cookie& cookie) noexcept
{
return setOption(CURLOPT_COOKIE, "Set-Cookie: " + cookie.Name + "=" + cookie.Value);
}
CURLcode CurlHandleManager::Handle::appendCookies (const CookiesList& cookies)noexcept
{
for (const Cookie& cookie : cookies)
{
const CURLcode result = appendCookie (cookie);
if (result != CURLE_OK)
return result;
}
return CURLE_OK;
}
void CurlHandleManager::Handle::appendHeader (const Header& header)
{
if (header.hasSameName ("User-Agent"))
mUserAgentSet = true;
mHeaders.append(header.Name + ": " + header.Value);
}
void CurlHandleManager::Handle::appendHeaders (const HeadersList& headers)
{
for (const Header& header : headers)
appendHeader (header);
}
CurlHandleManager::Handle::Result CurlHandleManager::Handle::perform ()
{
if (!mUserAgentSet)
mHeaders.append ("User-Agent: " + mOwner->getUserAgent ());
CURLcode result = setOption (CURLOPT_HTTPHEADER, mHeaders.getCurlList ());
if (result != CURLE_OK)
return { result, std::string () };
char currentError[CURL_ERROR_SIZE] = {};
setOption(CURLOPT_ERRORBUFFER, currentError);
result = curl_easy_perform (mHandle);
mReuse = mReuse && result == CURLE_OK;
return { result, std::string (currentError) };
}
void CurlHandleManager::Handle::markKeepAlive ()
{
mReuse = true;
}
bool CurlHandleManager::Handle::isHandleFromCache () const noexcept
{
return mHandleFromCache;
}
unsigned CurlHandleManager::Handle::getHTTPCode () const noexcept
{
long code = 0;
if (CURLE_OK != curl_easy_getinfo(mHandle, CURLINFO_RESPONSE_CODE, &code))
return 0;
return code;
}
void CurlHandleManager::Handle::reset () noexcept
{
setOption (CURLOPT_COOKIELIST, nullptr);
setOption (CURLOPT_PROXY, nullptr);
setOption (CURLOPT_SSL_OPTIONS, 0);
mUserAgentSet = false;
}
CurlHandleManager::CurlHandleManager ()
{
std::ostringstream ss;
ss << "Audacity/" <<
AUDACITY_VERSION << "." <<
AUDACITY_RELEASE << "." <<
AUDACITY_REVISION <<
" (";
GetOSString (ss, wxPlatformInfo::Get ());
ss << ")";
mUserAgent = ss.str ();
}
CurlHandleManager::~CurlHandleManager ()
{
std::lock_guard<std::mutex> lock (mHandleCacheLock);
for (auto& cachedHandle : mHandleCache)
curl_easy_cleanup (cachedHandle.Handle);
}
void CurlHandleManager::setProxy (std::string proxy)
{
mProxy = std::move (proxy);
}
CurlHandleManager::Handle CurlHandleManager::getHandle (RequestVerb verb, const std::string& url)
{
Handle handle (this, getCurlHandleFromCache (verb, url), verb, url);
if (!mProxy.empty ())
{
handle.setOption (CURLOPT_PROXY, mProxy);
// If we use proxy, checking the CRL will likely break the SSL proxying
handle.setOption (CURLOPT_SSL_OPTIONS, CURLSSLOPT_NO_REVOKE);
}
handle.setOption (CURLOPT_TCP_KEEPALIVE, 1L);
handle.setOption (CURLOPT_TCP_KEEPIDLE,
std::chrono::duration_cast<std::chrono::seconds> (KEEP_ALIVE_IDLE).count ()
);
handle.setOption (CURLOPT_TCP_KEEPINTVL,
std::chrono::duration_cast<std::chrono::seconds> (KEEP_ALIVE_PROBE).count ()
);
return handle;
}
std::string CurlHandleManager::getUserAgent () const
{
return mUserAgent;
}
CURL* CurlHandleManager::getCurlHandleFromCache (RequestVerb verb, const std::string& url)
{
std::lock_guard<std::mutex> lock (mHandleCacheLock);
cleanupHandlesCache ();
const std::string schemeAndDomain = GetSchemeAndDomain (url);
auto it = std::find_if (mHandleCache.begin (), mHandleCache.end (), [verb, schemeAndDomain](const CachedHandle& handle) {
return handle.Verb == verb && handle.SchemeAndDomain == schemeAndDomain;
});
if (it == mHandleCache.end ())
return nullptr;
CURL* handle = it->Handle;
mHandleCache.erase (it);
return handle;
}
void CurlHandleManager::cacheHandle (Handle& handle)
{
// Reset the state to the safe defaults
handle.reset ();
std::lock_guard<std::mutex> lock (mHandleCacheLock);
cleanupHandlesCache ();
mHandleCache.push_back ({
handle.mVerb,
GetSchemeAndDomain (handle.mUrl),
handle.mHandle,
RequestClock::now ()
});
}
void CurlHandleManager::cleanupHandlesCache ()
{
const RequestTimePoint timePoint = RequestClock::now ();
mHandleCache.erase (std::remove_if (mHandleCache.begin (), mHandleCache.end (), [timePoint](const CachedHandle& cachedHandle) {
return (timePoint - cachedHandle.RequestTime) >= KEEP_ALIVE_IDLE;
}), mHandleCache.end ());
}
std::string CurlHandleManager::GetSchemeAndDomain (const std::string& url)
{
const size_t schemeEndPosition = url.find ("://");
if (schemeEndPosition == std::string::npos) // Is url even valid?
return url;
const size_t domainStartPosition = schemeEndPosition + 3;
const size_t slashLocation = url.find ('/', domainStartPosition);
if (slashLocation == std::string::npos)
return url;
return url.substr (domainStartPosition, slashLocation - domainStartPosition);
}
}
}