Use the better understandable alias CURLOPT_HTTPHEADER in debug output Bug fix - finally! This fixes a bug I've been looking for a week. By accidently calling get_clock_count() for the mTimeoutLowSpeedClock 'event', the time difference 'now' - 'event' becomes negative, which should be impossible; the result being that timeout_low_speed stalled for 24 seconds while looping over the full 32 bit. That in turn made SSL handshakes of libcurl fail, which seemed to be impossible since the calls to libcurl had not changed!
2007 lines
71 KiB
C++
2007 lines
71 KiB
C++
/**
|
|
* @file aicurl.cpp
|
|
* @brief Implementation of AICurl.
|
|
*
|
|
* Copyright (c) 2012, Aleric Inglewood.
|
|
* Copyright (C) 2010, Linden Research, Inc.
|
|
*
|
|
* 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, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
* There are special exceptions to the terms and conditions of the GPL as
|
|
* it is applied to this Source Code. View the full text of the exception
|
|
* in the file doc/FLOSS-exception.txt in this software distribution.
|
|
*
|
|
* CHANGELOG
|
|
* and additional copyright holders.
|
|
*
|
|
* 17/03/2012
|
|
* Initial version, written by Aleric Inglewood @ SL
|
|
*
|
|
* 20/03/2012
|
|
* Added copyright notice for Linden Lab for those parts that were
|
|
* copied or derived from llcurl.cpp. The code of those parts are
|
|
* already in their own llcurl.cpp, so they do not ever need to
|
|
* even look at this file; the reason I added the copyright notice
|
|
* is to make clear that I am not the author of 100% of this code
|
|
* and hence I cannot change the license of it.
|
|
*/
|
|
|
|
#include "linden_common.h"
|
|
|
|
#if LL_WINDOWS
|
|
#include <winsock2.h> //remove classic winsock from windows.h
|
|
#endif
|
|
#define OPENSSL_THREAD_DEFINES
|
|
#include <openssl/opensslconf.h> // OPENSSL_THREADS
|
|
#include <openssl/crypto.h>
|
|
#include <openssl/ssl.h>
|
|
|
|
#include "aicurl.h"
|
|
#include "llbufferstream.h"
|
|
#include "llsdserialize.h"
|
|
#include "aithreadsafe.h"
|
|
#include "llqueuedthread.h"
|
|
#include "lltimer.h" // ms_sleep
|
|
#include "llproxy.h"
|
|
#include "llhttpstatuscodes.h"
|
|
#include "aihttpheaders.h"
|
|
#include "aihttptimeoutpolicy.h"
|
|
#include "aicurleasyrequeststatemachine.h"
|
|
|
|
// Some pretty printing for curl easy handle related things:
|
|
// Print the lock object related to the current easy handle in every debug output.
|
|
#ifdef CWDEBUG
|
|
#include <libcwd/buf2str.h>
|
|
#include <sstream>
|
|
#define DoutCurlEasy(x) do { \
|
|
using namespace libcwd; \
|
|
std::ostringstream marker; \
|
|
marker << (void*)this->get_lockobj(); \
|
|
libcw_do.push_marker(); \
|
|
libcw_do.marker().assign(marker.str().data(), marker.str().size()); \
|
|
libcw_do.inc_indent(2); \
|
|
Dout(dc::curl, x); \
|
|
libcw_do.dec_indent(2); \
|
|
libcw_do.pop_marker(); \
|
|
} while(0)
|
|
#define DoutCurlEasyEntering(x) do { \
|
|
using namespace libcwd; \
|
|
std::ostringstream marker; \
|
|
marker << (void*)this->get_lockobj(); \
|
|
libcw_do.push_marker(); \
|
|
libcw_do.marker().assign(marker.str().data(), marker.str().size()); \
|
|
libcw_do.inc_indent(2); \
|
|
DoutEntering(dc::curl, x); \
|
|
libcw_do.dec_indent(2); \
|
|
libcw_do.pop_marker(); \
|
|
} while(0)
|
|
#else // !CWDEBUG
|
|
#define DoutCurlEasy(x) Dout(dc::curl, x << " [" << (void*)this->get_lockobj() << ']')
|
|
#define DoutCurlEasyEntering(x) DoutEntering(dc::curl, x << " [" << (void*)this->get_lockobj() << ']')
|
|
#endif // CWDEBUG
|
|
|
|
//==================================================================================
|
|
// Local variables.
|
|
//
|
|
|
|
namespace {
|
|
|
|
struct CertificateAuthority {
|
|
std::string file;
|
|
std::string path;
|
|
};
|
|
|
|
AIThreadSafeSimpleDC<CertificateAuthority> gCertificateAuthority;
|
|
typedef AIAccess<CertificateAuthority> CertificateAuthority_wat;
|
|
typedef AIAccessConst<CertificateAuthority> CertificateAuthority_rat;
|
|
|
|
enum gSSLlib_type {
|
|
ssl_unknown,
|
|
ssl_openssl,
|
|
ssl_gnutls,
|
|
ssl_nss
|
|
};
|
|
|
|
// No locking needed: initialized before threads are created, and subsequently only read.
|
|
gSSLlib_type gSSLlib;
|
|
bool gSetoptParamsNeedDup;
|
|
void (*statemachines_flush_hook)(void);
|
|
|
|
} // namespace
|
|
|
|
// See http://www.openssl.org/docs/crypto/threads.html:
|
|
// CRYPTO_THREADID and associated functions were introduced in OpenSSL 1.0.0 to replace
|
|
// (actually, deprecate) the previous CRYPTO_set_id_callback(), CRYPTO_get_id_callback(),
|
|
// and CRYPTO_thread_id() functions which assumed thread IDs to always be represented by
|
|
// 'unsigned long'.
|
|
#define HAVE_CRYPTO_THREADID (OPENSSL_VERSION_NUMBER >= (1 << 28))
|
|
|
|
//-----------------------------------------------------------------------------------
|
|
// Needed for thread-safe openSSL operation.
|
|
|
|
// Must be defined in global namespace.
|
|
struct CRYPTO_dynlock_value
|
|
{
|
|
AIRWLock rwlock;
|
|
};
|
|
|
|
namespace {
|
|
|
|
AIRWLock* ssl_rwlock_array;
|
|
|
|
// OpenSSL locking function.
|
|
void ssl_locking_function(int mode, int n, char const* file, int line)
|
|
{
|
|
if ((mode & CRYPTO_LOCK))
|
|
{
|
|
if ((mode & CRYPTO_READ))
|
|
ssl_rwlock_array[n].rdlock();
|
|
else
|
|
ssl_rwlock_array[n].wrlock();
|
|
}
|
|
else
|
|
{
|
|
if ((mode & CRYPTO_READ))
|
|
ssl_rwlock_array[n].rdunlock();
|
|
else
|
|
ssl_rwlock_array[n].wrunlock();
|
|
}
|
|
}
|
|
|
|
#if HAVE_CRYPTO_THREADID
|
|
// OpenSSL uniq id function.
|
|
void ssl_id_function(CRYPTO_THREADID* thread_id)
|
|
{
|
|
#if LL_WINDOWS || LL_DARWIN // apr_os_thread_current() returns a pointer,
|
|
CRYPTO_THREADID_set_pointer(thread_id, apr_os_thread_current());
|
|
#else // else it returns an unsigned long.
|
|
CRYPTO_THREADID_set_numeric(thread_id, apr_os_thread_current());
|
|
#endif
|
|
}
|
|
#endif // HAVE_CRYPTO_THREADID
|
|
|
|
// OpenSSL allocate and initialize dynamic crypto lock.
|
|
CRYPTO_dynlock_value* ssl_dyn_create_function(char const* file, int line)
|
|
{
|
|
return new CRYPTO_dynlock_value;
|
|
}
|
|
|
|
// OpenSSL destroy dynamic crypto lock.
|
|
void ssl_dyn_destroy_function(CRYPTO_dynlock_value* l, char const* file, int line)
|
|
{
|
|
delete l;
|
|
}
|
|
|
|
// OpenSSL dynamic locking function.
|
|
void ssl_dyn_lock_function(int mode, CRYPTO_dynlock_value* l, char const* file, int line)
|
|
{
|
|
if ((mode & CRYPTO_LOCK))
|
|
{
|
|
if ((mode & CRYPTO_READ))
|
|
l->rwlock.rdlock();
|
|
else
|
|
l->rwlock.wrlock();
|
|
}
|
|
else
|
|
{
|
|
if ((mode & CRYPTO_READ))
|
|
l->rwlock.rdunlock();
|
|
else
|
|
l->rwlock.wrunlock();
|
|
}
|
|
}
|
|
|
|
typedef void (*ssl_locking_function_type)(int, int, char const*, int);
|
|
#if HAVE_CRYPTO_THREADID
|
|
typedef void (*ssl_id_function_type)(CRYPTO_THREADID*);
|
|
#else
|
|
typedef unsigned long (*ulong_thread_id_function_type)(void);
|
|
#endif
|
|
typedef CRYPTO_dynlock_value* (*ssl_dyn_create_function_type)(char const*, int);
|
|
typedef void (*ssl_dyn_destroy_function_type)(CRYPTO_dynlock_value*, char const*, int);
|
|
typedef void (*ssl_dyn_lock_function_type)(int, CRYPTO_dynlock_value*, char const*, int);
|
|
|
|
ssl_locking_function_type old_ssl_locking_function;
|
|
#if HAVE_CRYPTO_THREADID
|
|
ssl_id_function_type old_ssl_id_function;
|
|
#else
|
|
ulong_thread_id_function_type old_ulong_thread_id_function;
|
|
#endif
|
|
ssl_dyn_create_function_type old_ssl_dyn_create_function;
|
|
ssl_dyn_destroy_function_type old_ssl_dyn_destroy_function;
|
|
ssl_dyn_lock_function_type old_ssl_dyn_lock_function;
|
|
|
|
#if LL_WINDOWS
|
|
static unsigned long __cdecl apr_os_thread_current_wrapper()
|
|
{
|
|
return (unsigned long)apr_os_thread_current();
|
|
}
|
|
#endif
|
|
|
|
// Set for openssl-1.0.1...1.0.1c.
|
|
static bool need_renegotiation_hack = false;
|
|
|
|
// Initialize OpenSSL library for thread-safety.
|
|
void ssl_init(void)
|
|
{
|
|
// The version identifier format is: MMNNFFPPS: major minor fix patch status.
|
|
int const compiled_openSSL_major = (OPENSSL_VERSION_NUMBER >> 28) & 0xff;
|
|
int const compiled_openSSL_minor = (OPENSSL_VERSION_NUMBER >> 20) & 0xff;
|
|
unsigned long const ssleay = SSLeay();
|
|
int const linked_openSSL_major = (ssleay >> 28) & 0xff;
|
|
int const linked_openSSL_minor = (ssleay >> 20) & 0xff;
|
|
// Check if dynamically loaded version is compatible with the one we compiled against.
|
|
// As off version 1.0.0 also minor versions are compatible.
|
|
if (linked_openSSL_major != compiled_openSSL_major ||
|
|
(linked_openSSL_major == 0 && linked_openSSL_minor != compiled_openSSL_minor))
|
|
{
|
|
llerrs << "The viewer was compiled against " << OPENSSL_VERSION_TEXT <<
|
|
" but linked against " << SSLeay_version(SSLEAY_VERSION) <<
|
|
". Those versions are not compatible." << llendl;
|
|
}
|
|
// Static locks vector.
|
|
ssl_rwlock_array = new AIRWLock[CRYPTO_num_locks()];
|
|
// Static locks callbacks.
|
|
old_ssl_locking_function = CRYPTO_get_locking_callback();
|
|
#if HAVE_CRYPTO_THREADID
|
|
old_ssl_id_function = CRYPTO_THREADID_get_callback();
|
|
#else
|
|
old_ulong_thread_id_function = CRYPTO_get_id_callback();
|
|
#endif
|
|
CRYPTO_set_locking_callback(&ssl_locking_function);
|
|
// Setting this avoids the need for a thread-local error number facility, which is hard to check.
|
|
#if HAVE_CRYPTO_THREADID
|
|
CRYPTO_THREADID_set_callback(&ssl_id_function);
|
|
#else
|
|
#if LL_WINDOWS
|
|
CRYPTO_set_id_callback(&apr_os_thread_current_wrapper);
|
|
#else
|
|
CRYPTO_set_id_callback(&apr_os_thread_current);
|
|
#endif
|
|
#endif
|
|
// Dynamic locks callbacks.
|
|
old_ssl_dyn_create_function = CRYPTO_get_dynlock_create_callback();
|
|
old_ssl_dyn_lock_function = CRYPTO_get_dynlock_lock_callback();
|
|
old_ssl_dyn_destroy_function = CRYPTO_get_dynlock_destroy_callback();
|
|
CRYPTO_set_dynlock_create_callback(&ssl_dyn_create_function);
|
|
CRYPTO_set_dynlock_lock_callback(&ssl_dyn_lock_function);
|
|
CRYPTO_set_dynlock_destroy_callback(&ssl_dyn_destroy_function);
|
|
need_renegotiation_hack = (0x10001000UL <= ssleay);
|
|
llinfos << "Successful initialization of " <<
|
|
SSLeay_version(SSLEAY_VERSION) << " (0x" << std::hex << SSLeay() << std::dec << ")." << llendl;
|
|
}
|
|
|
|
// Cleanup OpenSSL library thread-safety.
|
|
void ssl_cleanup(void)
|
|
{
|
|
// Dynamic locks callbacks.
|
|
CRYPTO_set_dynlock_destroy_callback(old_ssl_dyn_destroy_function);
|
|
CRYPTO_set_dynlock_lock_callback(old_ssl_dyn_lock_function);
|
|
CRYPTO_set_dynlock_create_callback(old_ssl_dyn_create_function);
|
|
// Static locks callbacks.
|
|
#if HAVE_CRYPTO_THREADID
|
|
CRYPTO_THREADID_set_callback(old_ssl_id_function);
|
|
#else
|
|
CRYPTO_set_id_callback(old_ulong_thread_id_function);
|
|
#endif
|
|
CRYPTO_set_locking_callback(old_ssl_locking_function);
|
|
// Static locks vector.
|
|
delete [] ssl_rwlock_array;
|
|
}
|
|
|
|
} // namespace openSSL
|
|
//-----------------------------------------------------------------------------------
|
|
|
|
static unsigned int encoded_version(int major, int minor, int patch)
|
|
{
|
|
return (major << 16) | (minor << 8) | patch;
|
|
}
|
|
|
|
//==================================================================================
|
|
// External API
|
|
//
|
|
|
|
#undef AICurlPrivate
|
|
|
|
namespace AICurlInterface {
|
|
|
|
// MAIN-THREAD
|
|
void initCurl(void (*flush_hook)())
|
|
{
|
|
DoutEntering(dc::curl, "AICurlInterface::initCurl(" << (void*)flush_hook << ")");
|
|
|
|
llassert(LLThread::getRunning() == 0); // We must not call curl_global_init unless we are the only thread.
|
|
CURLcode res = curl_global_init(CURL_GLOBAL_ALL);
|
|
if (res != CURLE_OK)
|
|
{
|
|
llerrs << "curl_global_init(CURL_GLOBAL_ALL) failed." << llendl;
|
|
}
|
|
|
|
// Print version and do some feature sanity checks.
|
|
{
|
|
curl_version_info_data* version_info = curl_version_info(CURLVERSION_NOW);
|
|
|
|
llassert_always(version_info->age >= 0);
|
|
if (version_info->age < 1)
|
|
{
|
|
llwarns << "libcurl's age is 0; no ares support." << llendl;
|
|
}
|
|
llassert_always((version_info->features & CURL_VERSION_SSL)); // SSL support, added in libcurl 7.10.
|
|
if (!(version_info->features & CURL_VERSION_ASYNCHDNS)) // Asynchronous name lookups (added in libcurl 7.10.7).
|
|
{
|
|
llwarns << "libcurl was not compiled with support for asynchronous name lookups!" << llendl;
|
|
}
|
|
if (!version_info->ssl_version)
|
|
{
|
|
llerrs << "This libcurl has no SSL support!" << llendl;
|
|
}
|
|
|
|
llinfos << "Successful initialization of libcurl " <<
|
|
version_info->version << " (0x" << std::hex << version_info->version_num << std::dec << "), (" <<
|
|
version_info->ssl_version;
|
|
if (version_info->libz_version)
|
|
{
|
|
llcont << ", libz/" << version_info->libz_version;
|
|
}
|
|
llcont << ")." << llendl;
|
|
|
|
// Detect SSL library used.
|
|
gSSLlib = ssl_unknown;
|
|
std::string ssl_version(version_info->ssl_version);
|
|
if (ssl_version.find("OpenSSL") != std::string::npos)
|
|
gSSLlib = ssl_openssl; // See http://www.openssl.org/docs/crypto/threads.html#DESCRIPTION
|
|
else if (ssl_version.find("GnuTLS") != std::string::npos)
|
|
gSSLlib = ssl_gnutls; // See http://www.gnu.org/software/gnutls/manual/html_node/Thread-safety.html
|
|
else if (ssl_version.find("NSS") != std::string::npos)
|
|
gSSLlib = ssl_nss; // Supposedly thread-safe without any requirements.
|
|
|
|
// Set up thread-safety requirements of underlaying SSL library.
|
|
// See http://curl.haxx.se/libcurl/c/libcurl-tutorial.html
|
|
switch (gSSLlib)
|
|
{
|
|
case ssl_unknown:
|
|
{
|
|
llerrs << "Unknown SSL library \"" << version_info->ssl_version << "\", required actions for thread-safe handling are unknown! Bailing out." << llendl;
|
|
}
|
|
case ssl_openssl:
|
|
{
|
|
#ifndef OPENSSL_THREADS
|
|
llerrs << "OpenSSL was not configured with thread support! Bailing out." << llendl;
|
|
#endif
|
|
ssl_init();
|
|
}
|
|
case ssl_gnutls:
|
|
{
|
|
// I don't think we ever get here, do we? --Aleric
|
|
llassert_always(gSSLlib != ssl_gnutls);
|
|
// If we do, then didn't curl_global_init already call gnutls_global_init?
|
|
// It seems there is nothing to do for us here.
|
|
}
|
|
case ssl_nss:
|
|
{
|
|
break; // No requirements.
|
|
}
|
|
}
|
|
|
|
// Before version 7.17.0, strings were not copied. Instead the user was forced keep them available until libcurl no longer needed them.
|
|
gSetoptParamsNeedDup = (version_info->version_num < encoded_version(7, 17, 0));
|
|
if (gSetoptParamsNeedDup)
|
|
{
|
|
llwarns << "Your libcurl version is too old." << llendl;
|
|
}
|
|
llassert_always(!gSetoptParamsNeedDup); // Might add support later.
|
|
}
|
|
|
|
// Called in cleanupCurl.
|
|
statemachines_flush_hook = flush_hook;
|
|
}
|
|
|
|
// MAIN-THREAD
|
|
void cleanupCurl(void)
|
|
{
|
|
using namespace AICurlPrivate;
|
|
|
|
DoutEntering(dc::curl, "AICurlInterface::cleanupCurl()");
|
|
|
|
stopCurlThread();
|
|
if (CurlMultiHandle::getTotalMultiHandles() != 0)
|
|
llwarns << "Not all CurlMultiHandle objects were destroyed!" << llendl;
|
|
if (statemachines_flush_hook)
|
|
(*statemachines_flush_hook)();
|
|
Stats::print();
|
|
ssl_cleanup();
|
|
|
|
llassert(LLThread::getRunning() <= (curlThreadIsRunning() ? 1 : 0)); // We must not call curl_global_cleanup unless we are the only thread left.
|
|
curl_global_cleanup();
|
|
}
|
|
|
|
// THREAD-SAFE
|
|
std::string getVersionString(void)
|
|
{
|
|
// libcurl is thread safe, no locking needed.
|
|
return curl_version();
|
|
}
|
|
|
|
// THREAD-SAFE
|
|
void setCAFile(std::string const& file)
|
|
{
|
|
CertificateAuthority_wat CertificateAuthority_w(gCertificateAuthority);
|
|
CertificateAuthority_w->file = file;
|
|
}
|
|
|
|
// This function is not called from anywhere, but provided as part of AICurlInterface because setCAFile is.
|
|
// THREAD-SAFE
|
|
void setCAPath(std::string const& path)
|
|
{
|
|
CertificateAuthority_wat CertificateAuthority_w(gCertificateAuthority);
|
|
CertificateAuthority_w->path = path;
|
|
}
|
|
|
|
// THREAD-SAFE
|
|
std::string strerror(CURLcode errorcode)
|
|
{
|
|
// libcurl is thread safe, no locking needed.
|
|
return curl_easy_strerror(errorcode);
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// class Responder
|
|
//
|
|
|
|
Responder::Responder(void) : mReferenceCount(0)
|
|
{
|
|
DoutEntering(dc::curl, "AICurlInterface::Responder() with this = " << (void*)this);
|
|
}
|
|
|
|
Responder::~Responder()
|
|
{
|
|
DoutEntering(dc::curl, "AICurlInterface::Responder::~Responder() with this = " << (void*)this << "; mReferenceCount = " << mReferenceCount);
|
|
llassert(mReferenceCount == 0);
|
|
}
|
|
|
|
void Responder::setURL(std::string const& url)
|
|
{
|
|
// setURL is called from llhttpclient.cpp (request()), before calling any of the below (of course).
|
|
// We don't need locking here therefore; it's a case of initializing before use.
|
|
mURL = url;
|
|
}
|
|
|
|
AIHTTPTimeoutPolicy const& Responder::getHTTPTimeoutPolicy(void) const
|
|
{
|
|
return AIHTTPTimeoutPolicy::getDebugSettingsCurlTimeout();
|
|
}
|
|
|
|
// Called with HTML header.
|
|
// virtual
|
|
void Responder::completedHeaders(U32, std::string const&, AIHTTPHeaders const&)
|
|
{
|
|
// This should not be called unless a derived class implemented it.
|
|
llerrs << "Unexpected call to completedHeaders()." << llendl;
|
|
}
|
|
|
|
// Called with HTML body.
|
|
// virtual
|
|
void Responder::completedRaw(U32 status, std::string const& reason, LLChannelDescriptors const& channels, LLIOPipe::buffer_ptr_t const& buffer)
|
|
{
|
|
LLSD content;
|
|
LLBufferStream istr(channels, buffer.get());
|
|
if (!LLSDSerialize::fromXML(content, istr))
|
|
{
|
|
llinfos << "Failed to deserialize LLSD. " << mURL << " [" << status << "]: " << reason << llendl;
|
|
}
|
|
|
|
// Allow derived class to override at this point.
|
|
completed(status, reason, content);
|
|
}
|
|
|
|
void Responder::fatalError(std::string const& reason)
|
|
{
|
|
llwarns << "Responder::fatalError(\"" << reason << "\") is called (" << mURL << "). Passing it to Responder::completed with fake HTML error status and empty HTML body!" << llendl;
|
|
completed(U32_MAX, reason, LLSD());
|
|
}
|
|
|
|
// virtual
|
|
void Responder::completed(U32 status, std::string const& reason, LLSD const& content)
|
|
{
|
|
// HTML status good?
|
|
if (200 <= status && status < 300)
|
|
{
|
|
// Allow derived class to override at this point.
|
|
result(content);
|
|
}
|
|
else
|
|
{
|
|
// Allow derived class to override at this point.
|
|
errorWithContent(status, reason, content);
|
|
}
|
|
}
|
|
|
|
// virtual
|
|
void Responder::errorWithContent(U32 status, std::string const& reason, LLSD const&)
|
|
{
|
|
// Allow derived class to override at this point.
|
|
error(status, reason);
|
|
}
|
|
|
|
// virtual
|
|
void Responder::error(U32 status, std::string const& reason)
|
|
{
|
|
llinfos << mURL << " [" << status << "]: " << reason << llendl;
|
|
}
|
|
|
|
// virtual
|
|
void Responder::result(LLSD const&)
|
|
{
|
|
// Nothing.
|
|
}
|
|
|
|
// Friend functions.
|
|
|
|
void intrusive_ptr_add_ref(Responder* responder)
|
|
{
|
|
responder->mReferenceCount++;
|
|
}
|
|
|
|
void intrusive_ptr_release(Responder* responder)
|
|
{
|
|
if (--responder->mReferenceCount == 0)
|
|
{
|
|
delete responder;
|
|
}
|
|
}
|
|
|
|
} // namespace AICurlInterface
|
|
//==================================================================================
|
|
|
|
|
|
//==================================================================================
|
|
// Local implementation.
|
|
//
|
|
|
|
namespace AICurlPrivate {
|
|
|
|
//static
|
|
LLAtomicU32 Stats::easy_calls;
|
|
LLAtomicU32 Stats::easy_errors;
|
|
LLAtomicU32 Stats::easy_init_calls;
|
|
LLAtomicU32 Stats::easy_init_errors;
|
|
LLAtomicU32 Stats::easy_cleanup_calls;
|
|
LLAtomicU32 Stats::multi_calls;
|
|
LLAtomicU32 Stats::multi_errors;
|
|
|
|
//static
|
|
void Stats::print(void)
|
|
{
|
|
llinfos_nf << "============ CURL STATS ============" << llendl;
|
|
llinfos_nf << " Curl multi errors/calls : " << std::dec << multi_errors << "/" << multi_calls << llendl;
|
|
llinfos_nf << " Curl easy errors/calls : " << std::dec << easy_errors << "/" << easy_calls << llendl;
|
|
llinfos_nf << " curl_easy_init() errors/calls : " << std::dec << easy_init_errors << "/" << easy_init_calls << llendl;
|
|
llinfos_nf << " Current number of curl easy handles: " << std::dec << (easy_init_calls - easy_init_errors - easy_cleanup_calls) << llendl;
|
|
llinfos_nf << "========= END OF CURL STATS =========" << llendl;
|
|
}
|
|
|
|
// THREAD-SAFE
|
|
void handle_multi_error(CURLMcode code)
|
|
{
|
|
Stats::multi_errors++;
|
|
llinfos << "curl multi error detected: " << curl_multi_strerror(code) <<
|
|
"; (errors/calls = " << Stats::multi_errors << "/" << Stats::multi_calls << ")" << llendl;
|
|
}
|
|
|
|
//=============================================================================
|
|
// AICurlEasyRequest (base classes)
|
|
//
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// CurlEasyHandle
|
|
|
|
// THREAD-SAFE
|
|
//static
|
|
void CurlEasyHandle::handle_easy_error(CURLcode code)
|
|
{
|
|
char* error_buffer = LLThreadLocalData::tldata().mCurlErrorBuffer;
|
|
llinfos << "curl easy error detected: " << curl_easy_strerror(code);
|
|
if (error_buffer && *error_buffer != '\0')
|
|
{
|
|
llcont << ": " << error_buffer;
|
|
}
|
|
Stats::easy_errors++;
|
|
llcont << "; (errors/calls = " << Stats::easy_errors << "/" << Stats::easy_calls << ")" << llendl;
|
|
}
|
|
|
|
// Throws AICurlNoEasyHandle.
|
|
CurlEasyHandle::CurlEasyHandle(void) : mActiveMultiHandle(NULL), mErrorBuffer(NULL), mQueuedForRemoval(false)
|
|
#ifdef SHOW_ASSERT
|
|
, mRemovedPerCommand(true)
|
|
#endif
|
|
{
|
|
mEasyHandle = curl_easy_init();
|
|
#if 0
|
|
// Fake curl_easy_init() failures: throw once every 10 times (for debugging purposes).
|
|
static int count = 0;
|
|
if (mEasyHandle && (++count % 10) == 5)
|
|
{
|
|
curl_easy_cleanup(mEasyHandle);
|
|
mEasyHandle = NULL;
|
|
}
|
|
#endif
|
|
Stats::easy_init_calls++;
|
|
if (!mEasyHandle)
|
|
{
|
|
Stats::easy_init_errors++;
|
|
throw AICurlNoEasyHandle("curl_easy_init() returned NULL");
|
|
}
|
|
}
|
|
|
|
#if 0 // Not used
|
|
CurlEasyHandle::CurlEasyHandle(CurlEasyHandle const& orig) : mActiveMultiHandle(NULL), mErrorBuffer(NULL)
|
|
#ifdef SHOW_ASSERT
|
|
, mRemovedPerCommand(true)
|
|
#endif
|
|
{
|
|
mEasyHandle = curl_easy_duphandle(orig.mEasyHandle);
|
|
Stats::easy_init_calls++;
|
|
if (!mEasyHandle)
|
|
{
|
|
Stats::easy_init_errors++;
|
|
throw AICurlNoEasyHandle("curl_easy_duphandle() returned NULL");
|
|
}
|
|
}
|
|
#endif
|
|
|
|
CurlEasyHandle::~CurlEasyHandle()
|
|
{
|
|
llassert(!mActiveMultiHandle);
|
|
curl_easy_cleanup(mEasyHandle);
|
|
Stats::easy_cleanup_calls++;
|
|
}
|
|
|
|
//static
|
|
char* CurlEasyHandle::getTLErrorBuffer(void)
|
|
{
|
|
LLThreadLocalData& tldata = LLThreadLocalData::tldata();
|
|
if (!tldata.mCurlErrorBuffer)
|
|
{
|
|
tldata.mCurlErrorBuffer = new char[CURL_ERROR_SIZE];
|
|
}
|
|
return tldata.mCurlErrorBuffer;
|
|
}
|
|
|
|
void CurlEasyHandle::setErrorBuffer(void)
|
|
{
|
|
char* error_buffer = getTLErrorBuffer();
|
|
if (mErrorBuffer != error_buffer)
|
|
{
|
|
mErrorBuffer = error_buffer;
|
|
CURLcode res = curl_easy_setopt(mEasyHandle, CURLOPT_ERRORBUFFER, error_buffer);
|
|
if (res != CURLE_OK)
|
|
{
|
|
llwarns << "curl_easy_setopt(" << (void*)mEasyHandle << "CURLOPT_ERRORBUFFER, " << (void*)error_buffer << ") failed with error " << res << llendl;
|
|
mErrorBuffer = NULL;
|
|
}
|
|
}
|
|
if (mErrorBuffer)
|
|
{
|
|
mErrorBuffer[0] = '\0';
|
|
}
|
|
}
|
|
|
|
CURLcode CurlEasyHandle::getinfo_priv(CURLINFO info, void* data)
|
|
{
|
|
setErrorBuffer();
|
|
return check_easy_code(curl_easy_getinfo(mEasyHandle, info, data));
|
|
}
|
|
|
|
char* CurlEasyHandle::escape(char* url, int length)
|
|
{
|
|
return curl_easy_escape(mEasyHandle, url, length);
|
|
}
|
|
|
|
char* CurlEasyHandle::unescape(char* url, int inlength , int* outlength)
|
|
{
|
|
return curl_easy_unescape(mEasyHandle, url, inlength, outlength);
|
|
}
|
|
|
|
CURLcode CurlEasyHandle::perform(void)
|
|
{
|
|
llassert(!mActiveMultiHandle);
|
|
setErrorBuffer();
|
|
return check_easy_code(curl_easy_perform(mEasyHandle));
|
|
}
|
|
|
|
CURLcode CurlEasyHandle::pause(int bitmask)
|
|
{
|
|
setErrorBuffer();
|
|
return check_easy_code(curl_easy_pause(mEasyHandle, bitmask));
|
|
}
|
|
|
|
CURLMcode CurlEasyHandle::add_handle_to_multi(AICurlEasyRequest_wat& curl_easy_request_w, CURLM* multi)
|
|
{
|
|
llassert_always(!mActiveMultiHandle && multi);
|
|
mActiveMultiHandle = multi;
|
|
CURLMcode res = check_multi_code(curl_multi_add_handle(multi, mEasyHandle));
|
|
added_to_multi_handle(curl_easy_request_w);
|
|
return res;
|
|
}
|
|
|
|
CURLMcode CurlEasyHandle::remove_handle_from_multi(AICurlEasyRequest_wat& curl_easy_request_w, CURLM* multi)
|
|
{
|
|
llassert_always(mActiveMultiHandle && mActiveMultiHandle == multi);
|
|
mActiveMultiHandle = NULL;
|
|
CURLMcode res = check_multi_code(curl_multi_remove_handle(multi, mEasyHandle));
|
|
removed_from_multi_handle(curl_easy_request_w);
|
|
mPostField = NULL;
|
|
return res;
|
|
}
|
|
|
|
void intrusive_ptr_add_ref(ThreadSafeCurlEasyRequest* threadsafe_curl_easy_request)
|
|
{
|
|
threadsafe_curl_easy_request->mReferenceCount++;
|
|
}
|
|
|
|
void intrusive_ptr_release(ThreadSafeCurlEasyRequest* threadsafe_curl_easy_request)
|
|
{
|
|
if (--threadsafe_curl_easy_request->mReferenceCount == 0)
|
|
{
|
|
delete threadsafe_curl_easy_request;
|
|
}
|
|
}
|
|
|
|
CURLcode CurlEasyHandle::setopt(CURLoption option, long parameter)
|
|
{
|
|
llassert((CURLOPTTYPE_LONG <= option && option < CURLOPTTYPE_LONG + 1000) ||
|
|
(sizeof(curl_off_t) == sizeof(long) &&
|
|
CURLOPTTYPE_OFF_T <= option && option < CURLOPTTYPE_OFF_T + 1000));
|
|
llassert(!mActiveMultiHandle);
|
|
setErrorBuffer();
|
|
return check_easy_code(curl_easy_setopt(mEasyHandle, option, parameter));
|
|
}
|
|
|
|
// The standard requires that sizeof(long) < sizeof(long long), so it's safe to overload like this.
|
|
// We assume that one of them is 64 bit, the size of curl_off_t.
|
|
CURLcode CurlEasyHandle::setopt(CURLoption option, long long parameter)
|
|
{
|
|
llassert(sizeof(curl_off_t) == sizeof(long long) &&
|
|
CURLOPTTYPE_OFF_T <= option && option < CURLOPTTYPE_OFF_T + 1000);
|
|
llassert(!mActiveMultiHandle);
|
|
setErrorBuffer();
|
|
return check_easy_code(curl_easy_setopt(mEasyHandle, option, parameter));
|
|
}
|
|
|
|
CURLcode CurlEasyHandle::setopt(CURLoption option, void const* parameter)
|
|
{
|
|
llassert(CURLOPTTYPE_OBJECTPOINT <= option && option < CURLOPTTYPE_OBJECTPOINT + 1000);
|
|
setErrorBuffer();
|
|
return check_easy_code(curl_easy_setopt(mEasyHandle, option, parameter));
|
|
}
|
|
|
|
#define DEFINE_FUNCTION_SETOPT1(function_type, opt1) \
|
|
CURLcode CurlEasyHandle::setopt(CURLoption option, function_type parameter) \
|
|
{ \
|
|
llassert(option == opt1); \
|
|
setErrorBuffer(); \
|
|
return check_easy_code(curl_easy_setopt(mEasyHandle, option, parameter)); \
|
|
}
|
|
|
|
#define DEFINE_FUNCTION_SETOPT3(function_type, opt1, opt2, opt3) \
|
|
CURLcode CurlEasyHandle::setopt(CURLoption option, function_type parameter) \
|
|
{ \
|
|
llassert(option == opt1 || option == opt2 || option == opt3); \
|
|
setErrorBuffer(); \
|
|
return check_easy_code(curl_easy_setopt(mEasyHandle, option, parameter)); \
|
|
}
|
|
|
|
#define DEFINE_FUNCTION_SETOPT4(function_type, opt1, opt2, opt3, opt4) \
|
|
CURLcode CurlEasyHandle::setopt(CURLoption option, function_type parameter) \
|
|
{ \
|
|
llassert(option == opt1 || option == opt2 || option == opt3 || option == opt4); \
|
|
setErrorBuffer(); \
|
|
return check_easy_code(curl_easy_setopt(mEasyHandle, option, parameter)); \
|
|
}
|
|
|
|
DEFINE_FUNCTION_SETOPT1(curl_debug_callback, CURLOPT_DEBUGFUNCTION)
|
|
DEFINE_FUNCTION_SETOPT4(curl_write_callback, CURLOPT_HEADERFUNCTION, CURLOPT_WRITEFUNCTION, CURLOPT_INTERLEAVEFUNCTION, CURLOPT_READFUNCTION)
|
|
//DEFINE_FUNCTION_SETOPT1(curl_read_callback, CURLOPT_READFUNCTION)
|
|
DEFINE_FUNCTION_SETOPT1(curl_ssl_ctx_callback, CURLOPT_SSL_CTX_FUNCTION)
|
|
DEFINE_FUNCTION_SETOPT3(curl_conv_callback, CURLOPT_CONV_FROM_NETWORK_FUNCTION, CURLOPT_CONV_TO_NETWORK_FUNCTION, CURLOPT_CONV_FROM_UTF8_FUNCTION)
|
|
#if 0 // Not used by the viewer.
|
|
DEFINE_FUNCTION_SETOPT1(curl_progress_callback, CURLOPT_PROGRESSFUNCTION)
|
|
DEFINE_FUNCTION_SETOPT1(curl_seek_callback, CURLOPT_SEEKFUNCTION)
|
|
DEFINE_FUNCTION_SETOPT1(curl_ioctl_callback, CURLOPT_IOCTLFUNCTION)
|
|
DEFINE_FUNCTION_SETOPT1(curl_sockopt_callback, CURLOPT_SOCKOPTFUNCTION)
|
|
DEFINE_FUNCTION_SETOPT1(curl_opensocket_callback, CURLOPT_OPENSOCKETFUNCTION)
|
|
DEFINE_FUNCTION_SETOPT1(curl_closesocket_callback, CURLOPT_CLOSESOCKETFUNCTION)
|
|
DEFINE_FUNCTION_SETOPT1(curl_sshkeycallback, CURLOPT_SSH_KEYFUNCTION)
|
|
DEFINE_FUNCTION_SETOPT1(curl_chunk_bgn_callback, CURLOPT_CHUNK_BGN_FUNCTION)
|
|
DEFINE_FUNCTION_SETOPT1(curl_chunk_end_callback, CURLOPT_CHUNK_END_FUNCTION)
|
|
DEFINE_FUNCTION_SETOPT1(curl_fnmatch_callback, CURLOPT_FNMATCH_FUNCTION)
|
|
#endif
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// CurlEasyRequest
|
|
|
|
void CurlEasyRequest::setoptString(CURLoption option, std::string const& value)
|
|
{
|
|
llassert(!gSetoptParamsNeedDup);
|
|
setopt(option, value.c_str());
|
|
}
|
|
|
|
void CurlEasyRequest::setPost(AIPostFieldPtr const& postdata, U32 size)
|
|
{
|
|
llassert_always(postdata->data());
|
|
|
|
DoutCurlEasy("POST size is " << size << " bytes: \"" << libcwd::buf2str(postdata->data(), size) << "\".");
|
|
setPostField(postdata); // Make sure the data stays around until we don't need it anymore.
|
|
|
|
setPost_raw(size, postdata->data());
|
|
}
|
|
|
|
void CurlEasyRequest::setPost_raw(U32 size, char const* data)
|
|
{
|
|
if (!data)
|
|
{
|
|
// data == NULL when we're going to read the data using CURLOPT_READFUNCTION.
|
|
DoutCurlEasy("POST size is " << size << " bytes.");
|
|
}
|
|
|
|
// Accept everything (send an Accept-Encoding header containing all encodings we support (zlib and gzip)).
|
|
setoptString(CURLOPT_ENCODING, ""); // CURLOPT_ACCEPT_ENCODING
|
|
|
|
// The server never replies with 100-continue, so suppress the "Expect: 100-continue" header that libcurl adds by default.
|
|
addHeader("Expect:");
|
|
if (size > 0)
|
|
{
|
|
addHeader("Connection: keep-alive");
|
|
addHeader("Keep-alive: 300");
|
|
}
|
|
setopt(CURLOPT_POSTFIELDSIZE, size);
|
|
setopt(CURLOPT_POSTFIELDS, data); // Implies CURLOPT_POST
|
|
}
|
|
|
|
ThreadSafeCurlEasyRequest* CurlEasyRequest::get_lockobj(void)
|
|
{
|
|
return static_cast<ThreadSafeCurlEasyRequest*>(AIThreadSafeSimpleDC<CurlEasyRequest>::wrapper_cast(this));
|
|
}
|
|
|
|
//static
|
|
size_t CurlEasyRequest::headerCallback(char* ptr, size_t size, size_t nmemb, void* userdata)
|
|
{
|
|
CurlEasyRequest* self = static_cast<CurlEasyRequest*>(userdata);
|
|
ThreadSafeCurlEasyRequest* lockobj = self->get_lockobj();
|
|
AICurlEasyRequest_wat lock_self(*lockobj);
|
|
return self->mHeaderCallback(ptr, size, nmemb, self->mHeaderCallbackUserData);
|
|
}
|
|
|
|
void CurlEasyRequest::setHeaderCallback(curl_write_callback callback, void* userdata)
|
|
{
|
|
mHeaderCallback = callback;
|
|
mHeaderCallbackUserData = userdata;
|
|
setopt(CURLOPT_HEADERFUNCTION, callback ? &CurlEasyRequest::headerCallback : NULL);
|
|
setopt(CURLOPT_WRITEHEADER, userdata ? this : NULL);
|
|
}
|
|
|
|
//static
|
|
size_t CurlEasyRequest::writeCallback(char* ptr, size_t size, size_t nmemb, void* userdata)
|
|
{
|
|
CurlEasyRequest* self = static_cast<CurlEasyRequest*>(userdata);
|
|
ThreadSafeCurlEasyRequest* lockobj = self->get_lockobj();
|
|
AICurlEasyRequest_wat lock_self(*lockobj);
|
|
return self->mWriteCallback(ptr, size, nmemb, self->mWriteCallbackUserData);
|
|
}
|
|
|
|
void CurlEasyRequest::setWriteCallback(curl_write_callback callback, void* userdata)
|
|
{
|
|
mWriteCallback = callback;
|
|
mWriteCallbackUserData = userdata;
|
|
setopt(CURLOPT_WRITEFUNCTION, callback ? &CurlEasyRequest::writeCallback : NULL);
|
|
setopt(CURLOPT_WRITEDATA, userdata ? this : NULL);
|
|
}
|
|
|
|
//static
|
|
size_t CurlEasyRequest::readCallback(char* ptr, size_t size, size_t nmemb, void* userdata)
|
|
{
|
|
CurlEasyRequest* self = static_cast<CurlEasyRequest*>(userdata);
|
|
ThreadSafeCurlEasyRequest* lockobj = self->get_lockobj();
|
|
AICurlEasyRequest_wat lock_self(*lockobj);
|
|
return self->mReadCallback(ptr, size, nmemb, self->mReadCallbackUserData);
|
|
}
|
|
|
|
void CurlEasyRequest::setReadCallback(curl_read_callback callback, void* userdata)
|
|
{
|
|
mReadCallback = callback;
|
|
mReadCallbackUserData = userdata;
|
|
setopt(CURLOPT_READFUNCTION, callback ? &CurlEasyRequest::readCallback : NULL);
|
|
setopt(CURLOPT_READDATA, userdata ? this : NULL);
|
|
}
|
|
|
|
//static
|
|
CURLcode CurlEasyRequest::SSLCtxCallback(CURL* curl, void* sslctx, void* userdata)
|
|
{
|
|
CurlEasyRequest* self = static_cast<CurlEasyRequest*>(userdata);
|
|
ThreadSafeCurlEasyRequest* lockobj = self->get_lockobj();
|
|
AICurlEasyRequest_wat lock_self(*lockobj);
|
|
return self->mSSLCtxCallback(curl, sslctx, self->mSSLCtxCallbackUserData);
|
|
}
|
|
|
|
void CurlEasyRequest::setSSLCtxCallback(curl_ssl_ctx_callback callback, void* userdata)
|
|
{
|
|
mSSLCtxCallback = callback;
|
|
mSSLCtxCallbackUserData = userdata;
|
|
setopt(CURLOPT_SSL_CTX_FUNCTION, callback ? &CurlEasyRequest::SSLCtxCallback : NULL);
|
|
setopt(CURLOPT_SSL_CTX_DATA, this);
|
|
}
|
|
|
|
#define llmaybewarns lllog(LLApp::isExiting() ? LLError::LEVEL_INFO : LLError::LEVEL_WARN, NULL, NULL, false, true)
|
|
|
|
static size_t noHeaderCallback(char* ptr, size_t size, size_t nmemb, void* userdata)
|
|
{
|
|
llmaybewarns << "Calling noHeaderCallback(); curl session aborted." << llendl;
|
|
return 0; // Cause a CURL_WRITE_ERROR
|
|
}
|
|
|
|
static size_t noWriteCallback(char* ptr, size_t size, size_t nmemb, void* userdata)
|
|
{
|
|
llmaybewarns << "Calling noWriteCallback(); curl session aborted." << llendl;
|
|
return 0; // Cause a CURL_WRITE_ERROR
|
|
}
|
|
|
|
static size_t noReadCallback(char* ptr, size_t size, size_t nmemb, void* userdata)
|
|
{
|
|
llmaybewarns << "Calling noReadCallback(); curl session aborted." << llendl;
|
|
return CURL_READFUNC_ABORT; // Cause a CURLE_ABORTED_BY_CALLBACK
|
|
}
|
|
|
|
static CURLcode noSSLCtxCallback(CURL* curl, void* sslctx, void* parm)
|
|
{
|
|
llmaybewarns << "Calling noSSLCtxCallback(); curl session aborted." << llendl;
|
|
return CURLE_ABORTED_BY_CALLBACK;
|
|
}
|
|
|
|
void CurlEasyRequest::revokeCallbacks(void)
|
|
{
|
|
if (mHeaderCallback == &noHeaderCallback &&
|
|
mWriteCallback == &noWriteCallback &&
|
|
mReadCallback == &noReadCallback &&
|
|
mSSLCtxCallback == &noSSLCtxCallback)
|
|
{
|
|
// Already revoked.
|
|
return;
|
|
}
|
|
mHeaderCallback = &noHeaderCallback;
|
|
mWriteCallback = &noWriteCallback;
|
|
mReadCallback = &noReadCallback;
|
|
mSSLCtxCallback = &noSSLCtxCallback;
|
|
if (active() && !no_warning())
|
|
{
|
|
llwarns << "Revoking callbacks on a still active CurlEasyRequest object!" << llendl;
|
|
}
|
|
curl_easy_setopt(getEasyHandle(), CURLOPT_HEADERFUNCTION, &noHeaderCallback);
|
|
curl_easy_setopt(getEasyHandle(), CURLOPT_WRITEHEADER, &noWriteCallback);
|
|
curl_easy_setopt(getEasyHandle(), CURLOPT_READFUNCTION, &noReadCallback);
|
|
curl_easy_setopt(getEasyHandle(), CURLOPT_SSL_CTX_FUNCTION, &noSSLCtxCallback);
|
|
}
|
|
|
|
CurlEasyRequest::~CurlEasyRequest()
|
|
{
|
|
// If the CurlEasyRequest object is destructed then we need to revoke all callbacks, because
|
|
// we can't set the lock anymore, and neither will mHeaderCallback, mWriteCallback etc,
|
|
// be available anymore.
|
|
send_events_to(NULL);
|
|
revokeCallbacks();
|
|
// This wasn't freed yet if the request never finished.
|
|
curl_slist_free_all(mHeaders);
|
|
}
|
|
|
|
void CurlEasyRequest::resetState(void)
|
|
{
|
|
// This function should not revoke the event call backs!
|
|
revokeCallbacks();
|
|
reset();
|
|
curl_slist_free_all(mHeaders);
|
|
mHeaders = NULL;
|
|
mRequestFinalized = false;
|
|
mEventsTarget = NULL;
|
|
mResult = CURLE_FAILED_INIT;
|
|
applyDefaultOptions();
|
|
}
|
|
|
|
void CurlEasyRequest::addHeader(char const* header)
|
|
{
|
|
llassert(!mRequestFinalized);
|
|
mHeaders = curl_slist_append(mHeaders, header);
|
|
}
|
|
|
|
void CurlEasyRequest::addHeaders(AIHTTPHeaders const& headers)
|
|
{
|
|
llassert(!mRequestFinalized);
|
|
headers.append_to(mHeaders);
|
|
}
|
|
|
|
#if defined(CWDEBUG) || defined(DEBUG_CURLIO)
|
|
|
|
static int curl_debug_cb(CURL*, curl_infotype infotype, char* buf, size_t size, void* user_ptr)
|
|
{
|
|
#ifdef CWDEBUG
|
|
using namespace ::libcwd;
|
|
|
|
CurlEasyRequest* request = (CurlEasyRequest*)user_ptr;
|
|
std::ostringstream marker;
|
|
marker << (void*)request->get_lockobj();
|
|
libcw_do.push_marker();
|
|
libcw_do.marker().assign(marker.str().data(), marker.str().size());
|
|
if (!debug::channels::dc::curlio.is_on())
|
|
debug::channels::dc::curlio.on();
|
|
LibcwDoutScopeBegin(LIBCWD_DEBUGCHANNELS, libcw_do, dc::curlio|cond_nonewline_cf(infotype == CURLINFO_TEXT))
|
|
#else
|
|
if (infotype == CURLINFO_TEXT)
|
|
{
|
|
while (size > 0 && (buf[size - 1] == '\r' || buf[size - 1] == '\n'))
|
|
--size;
|
|
}
|
|
LibcwDoutScopeBegin(LIBCWD_DEBUGCHANNELS, libcw_do, dc::curlio)
|
|
#endif
|
|
switch (infotype)
|
|
{
|
|
case CURLINFO_TEXT:
|
|
LibcwDoutStream << "* ";
|
|
break;
|
|
case CURLINFO_HEADER_IN:
|
|
LibcwDoutStream << "H> ";
|
|
break;
|
|
case CURLINFO_HEADER_OUT:
|
|
LibcwDoutStream << "H< ";
|
|
if (size >= 4 && strncmp(buf, "GET ", 4) == 0)
|
|
request->mDebugIsGetMethod = true;
|
|
break;
|
|
case CURLINFO_DATA_IN:
|
|
LibcwDoutStream << "D> ";
|
|
break;
|
|
case CURLINFO_DATA_OUT:
|
|
LibcwDoutStream << "D< ";
|
|
break;
|
|
case CURLINFO_SSL_DATA_IN:
|
|
LibcwDoutStream << "S> ";
|
|
break;
|
|
case CURLINFO_SSL_DATA_OUT:
|
|
LibcwDoutStream << "S< ";
|
|
break;
|
|
default:
|
|
LibcwDoutStream << "?? ";
|
|
}
|
|
if (infotype == CURLINFO_TEXT)
|
|
LibcwDoutStream.write(buf, size);
|
|
else if (infotype == CURLINFO_HEADER_IN || infotype == CURLINFO_HEADER_OUT)
|
|
LibcwDoutStream << libcwd::buf2str(buf, size);
|
|
else if (infotype == CURLINFO_DATA_IN)
|
|
{
|
|
LibcwDoutStream << size << " bytes";
|
|
bool finished = false;
|
|
size_t i = 0;
|
|
while (i < size)
|
|
{
|
|
char c = buf[i];
|
|
if (!('0' <= c && c <= '9') && !('a' <= c && c <= 'f'))
|
|
{
|
|
if (0 < i && i + 1 < size && buf[i] == '\r' && buf[i + 1] == '\n')
|
|
{
|
|
// Binary output: "[0-9a-f]*\r\n ...binary data..."
|
|
LibcwDoutStream << ": \"" << libcwd::buf2str(buf, i + 2) << "\"...";
|
|
finished = true;
|
|
}
|
|
break;
|
|
}
|
|
++i;
|
|
}
|
|
if (!finished && size > 9 && buf[0] == '<')
|
|
{
|
|
// Human readable output: html, xml or llsd.
|
|
if (!strncmp(buf, "<!DOCTYPE", 9) || !strncmp(buf, "<?xml", 5) || !strncmp(buf, "<llsd>", 6))
|
|
{
|
|
LibcwDoutStream << ": \"" << libcwd::buf2str(buf, size) << '"';
|
|
finished = true;
|
|
}
|
|
}
|
|
if (!finished)
|
|
{
|
|
// Unknown format. Only print the first and last 20 characters.
|
|
if (size > 40UL)
|
|
{
|
|
LibcwDoutStream << ": \"" << libcwd::buf2str(buf, 20) << "\"...\"" << libcwd::buf2str(&buf[size - 20], 20) << '"';
|
|
}
|
|
else
|
|
{
|
|
LibcwDoutStream << ": \"" << libcwd::buf2str(buf, size) << '"';
|
|
}
|
|
}
|
|
}
|
|
else if (infotype == CURLINFO_DATA_OUT)
|
|
LibcwDoutStream << size << " bytes: \"" << libcwd::buf2str(buf, size) << '"';
|
|
else
|
|
LibcwDoutStream << size << " bytes";
|
|
LibcwDoutScopeEnd;
|
|
#ifdef CWDEBUG
|
|
libcw_do.pop_marker();
|
|
#endif
|
|
return 0;
|
|
}
|
|
#endif
|
|
|
|
void CurlEasyRequest::applyProxySettings(void)
|
|
{
|
|
LLProxy& proxy = *LLProxy::getInstance();
|
|
|
|
// Do a faster unlocked check to see if we are supposed to proxy.
|
|
if (proxy.HTTPProxyEnabled())
|
|
{
|
|
// We think we should proxy, read lock the shared proxy members.
|
|
LLProxy::Shared_crat proxy_r(proxy.shared_lockobj());
|
|
|
|
// Now test again to verify that the proxy wasn't disabled between the first check and the lock.
|
|
if (proxy.HTTPProxyEnabled())
|
|
{
|
|
setopt(CURLOPT_PROXY, proxy.getHTTPProxy(proxy_r).getIPString().c_str());
|
|
setopt(CURLOPT_PROXYPORT, proxy.getHTTPProxy(proxy_r).getPort());
|
|
|
|
if (proxy.getHTTPProxyType(proxy_r) == LLPROXY_SOCKS)
|
|
{
|
|
setopt(CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5);
|
|
if (proxy.getSelectedAuthMethod(proxy_r) == METHOD_PASSWORD)
|
|
{
|
|
std::string auth_string = proxy.getSocksUser(proxy_r) + ":" + proxy.getSocksPwd(proxy_r);
|
|
setopt(CURLOPT_PROXYUSERPWD, auth_string.c_str());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
setopt(CURLOPT_PROXYTYPE, CURLPROXY_HTTP);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//static
|
|
CURLcode CurlEasyRequest::curlCtxCallback(CURL* curl, void* sslctx, void* parm)
|
|
{
|
|
DoutEntering(dc::curl, "CurlEasyRequest::curlCtxCallback((CURL*)" << (void*)curl << ", " << sslctx << ", " << parm << ")");
|
|
SSL_CTX* ctx = (SSL_CTX*)sslctx;
|
|
// Turn off TLS v1.1 (which is not supported anyway by Linden Lab) because otherwise we fail to connect.
|
|
// Also turn off SSL v2, which is highly broken and strongly discouraged[1].
|
|
// [1] http://www.openssl.org/docs/ssl/SSL_CTX_set_options.html#SECURE_RENEGOTIATION
|
|
long options = SSL_OP_NO_SSLv2;
|
|
#ifdef SSL_OP_NO_TLSv1_1 // Only defined for openssl version 1.0.1 and up.
|
|
if (need_renegotiation_hack)
|
|
{
|
|
// This option disables openssl to use TLS version 1.1.
|
|
// The Linden Lab servers don't support TLS versions later than 1.0, and libopenssl-1.0.1-beta1 up till and including
|
|
// libopenssl-1.0.1c have a bug (or feature?) where (re)negotiation fails (see http://rt.openssl.org/Ticket/Display.html?id=2828),
|
|
// causing the connection to fail completely without this hack.
|
|
// For a commandline test of the same, observe the difference between:
|
|
// openssl s_client -connect login.agni.lindenlab.com:443 -CAfile packaged/app_settings/CA.pem -debug
|
|
// which gets no response from the server after sending the initial data, and
|
|
// openssl s_client -no_tls1_1 -connect login.agni.lindenlab.com:443 -CAfile packaged/app_settings/CA.pem -debug
|
|
// which finishes the negotiation and ends with 'Verify return code: 0 (ok)'
|
|
options |= SSL_OP_NO_TLSv1_1;
|
|
}
|
|
#else
|
|
llassert_always(!need_renegotiation_hack);
|
|
#endif
|
|
SSL_CTX_set_options(ctx, options);
|
|
return CURLE_OK;
|
|
}
|
|
|
|
void CurlEasyRequest::applyDefaultOptions(void)
|
|
{
|
|
CertificateAuthority_rat CertificateAuthority_r(gCertificateAuthority);
|
|
setoptString(CURLOPT_CAINFO, CertificateAuthority_r->file);
|
|
setSSLCtxCallback(&curlCtxCallback, NULL);
|
|
setopt(CURLOPT_NOSIGNAL, 1);
|
|
// Cache DNS look ups an hour. If we set it smaller we risk frequent connect timeouts in cases where DNS look ups are slow.
|
|
setopt(CURLOPT_DNS_CACHE_TIMEOUT, 3600);
|
|
// Disable SSL/TLS session caching; some servers (aka id.secondlife.com) refuse connections when session ids are enabled.
|
|
setopt(CURLOPT_SSL_SESSIONID_CACHE, 0);
|
|
// Set the CURL options for either SOCKS or HTTP proxy.
|
|
applyProxySettings();
|
|
// Cause libcurl to print all it's I/O traffic on the debug channel.
|
|
Debug(
|
|
if (dc::curlio.is_on())
|
|
{
|
|
setopt(CURLOPT_VERBOSE, 1);
|
|
setopt(CURLOPT_DEBUGFUNCTION, &curl_debug_cb);
|
|
setopt(CURLOPT_DEBUGDATA, this);
|
|
}
|
|
);
|
|
}
|
|
|
|
// url must be of the form
|
|
// (see http://www.ietf.org/rfc/rfc3986.txt Appendix A for definitions not given here):
|
|
//
|
|
// url = sheme ":" hier-part [ "?" query ] [ "#" fragment ]
|
|
// hier-part = "//" authority path-abempty
|
|
// authority = [ userinfo "@" ] host [ ":" port ]
|
|
// path-abempty = *( "/" segment )
|
|
//
|
|
// That is, a hier-part of the form '/ path-absolute', '/ path-rootless' or
|
|
// '/ path-empty' is NOT allowed here. This should be safe because we only
|
|
// call this function for curl access, any file access would use APR.
|
|
//
|
|
// However, as a special exception, this function allows:
|
|
//
|
|
// url = authority path-abempty
|
|
//
|
|
// without the 'sheme ":" "//"' parts.
|
|
//
|
|
// As follows from the ABNF (see RFC, Appendix A):
|
|
// - authority is either terminated by a '/' or by the end of the string because
|
|
// neither userinfo, host nor port may contain a '/'.
|
|
// - userinfo does not contain a '@', and if it exists, is always terminated by a '@'.
|
|
// - port does not contain a ':', and if it exists is always prepended by a ':'.
|
|
//
|
|
// Only called by CurlEasyRequest::finalizeRequest.
|
|
static std::string extract_canonical_hostname(std::string const& url)
|
|
{
|
|
std::string::size_type pos;
|
|
std::string::size_type authority = 0; // Default if there is no sheme.
|
|
if ((pos = url.find("://")) != url.npos && pos < url.find('/')) authority = pos + 3; // Skip the "sheme://" if any, the second find is to avoid finding a "://" as part of path-abempty.
|
|
std::string::size_type host = authority; // Default if there is no userinfo.
|
|
if ((pos = url.find('@', authority)) != url.npos) host = pos + 1; // Skip the "userinfo@" if any.
|
|
authority = url.length() - 1; // Default last character of host if there is no path-abempty.
|
|
if ((pos = url.find('/', host)) != url.npos) authority = pos - 1; // Point to last character of host.
|
|
std::string::size_type len = url.find_last_not_of(":0123456789", authority) - host + 1; // Skip trailing ":port", if any.
|
|
std::string hostname(url, host, len);
|
|
#if APR_CHARSET_EBCDIC
|
|
#error Not implemented
|
|
#else
|
|
// Convert hostname to lowercase in a way that we compare two hostnames equal iff libcurl does.
|
|
for (std::string::iterator iter = hostname.begin(); iter != hostname.end(); ++iter)
|
|
{
|
|
int c = *iter;
|
|
if (c >= 'A' && c <= 'Z')
|
|
*iter = c + ('a' - 'A');
|
|
}
|
|
#endif
|
|
return hostname;
|
|
}
|
|
|
|
void CurlEasyRequest::finalizeRequest(std::string const& url, AIHTTPTimeoutPolicy const& policy, AICurlEasyRequestStateMachine* state_machine)
|
|
{
|
|
DoutCurlEasyEntering("CurlEasyRequest::finalizeRequest(\"" << url << "\", " << policy.name() << ", " << (void*)state_machine << ")");
|
|
llassert(!mRequestFinalized);
|
|
mResult = CURLE_FAILED_INIT; // General error code; the final result code is stored here by MultiHandle::check_run_count when msg is CURLMSG_DONE.
|
|
#ifdef SHOW_ASSERT
|
|
// Do a sanity check on the headers.
|
|
int content_type_count = 0;
|
|
for (curl_slist* list = mHeaders; list; list = list->next)
|
|
{
|
|
if (strncmp(list->data, "Content-Type:", 13) == 0)
|
|
{
|
|
++content_type_count;
|
|
}
|
|
}
|
|
if (content_type_count > 1)
|
|
{
|
|
llwarns << "Requesting: \"" << url << "\": " << content_type_count << " Content-Type: headers!" << llendl;
|
|
}
|
|
#endif
|
|
mRequestFinalized = true;
|
|
setopt(CURLOPT_HTTPHEADER, mHeaders);
|
|
setoptString(CURLOPT_URL, url);
|
|
mTimeoutLowercaseHostname = extract_canonical_hostname(url);
|
|
mTimeoutPolicy = &policy;
|
|
state_machine->setTotalDelayTimeout(policy.getTotalDelay());
|
|
// The following line is a bit tricky: we store a pointer to the object without increasing its reference count.
|
|
// Of course we could increment the reference count, but that doesn't help much: if then this pointer would
|
|
// get "lost" we'd have a memory leak. Either way we must make sure that it is impossible that this pointer
|
|
// will be used if the object is deleted [In fact, since this is finalizeRequest() and not addRequest(),
|
|
// incrementing the reference counter would be wrong: if addRequest() is never called then the object is
|
|
// destroyed shortly after and this pointer is never even used.]
|
|
// This pointer is used in MultiHandle::check_run_count, which means that addRequest() was called and
|
|
// the reference counter was increased and the object is being kept alive, see the comments above
|
|
// command_queue in aicurlthread.cpp. In fact, this object survived until MultiHandle::add_easy_request
|
|
// was called and is kept alive by MultiHandle::mAddedEasyRequests. The only way to get deleted after
|
|
// that is when MultiHandle::remove_easy_request is called, which first removes the easy handle from
|
|
// the multi handle. So that it's (hopefully) no longer possible that info_read() in
|
|
// MultiHandle::check_run_count returns this easy handle, after the object is destroyed by deleting
|
|
// it from MultiHandle::mAddedEasyRequests.
|
|
setopt(CURLOPT_PRIVATE, get_lockobj());
|
|
}
|
|
|
|
//.............................................................................
|
|
// HTTP Timeout stuff
|
|
|
|
//static
|
|
F64 const CurlEasyRequest::sTimeoutClockWidth = 1.0 / calc_clock_frequency(); // Time between two clock ticks, in seconds.
|
|
U64 CurlEasyRequest::sTimeoutClockCount; // Clock count, set once per select() exit.
|
|
|
|
void CurlEasyRequest::timeout_timings(void)
|
|
{
|
|
double t;
|
|
getinfo(CURLINFO_NAMELOOKUP_TIME, &t);
|
|
DoutCurlEasy("CURLINFO_NAMELOOKUP_TIME = " << t);
|
|
getinfo(CURLINFO_CONNECT_TIME, &t);
|
|
DoutCurlEasy("CURLINFO_CONNECT_TIME = " << t);
|
|
getinfo(CURLINFO_APPCONNECT_TIME, &t);
|
|
DoutCurlEasy("CURLINFO_APPCONNECT_TIME = " << t);
|
|
getinfo(CURLINFO_PRETRANSFER_TIME, &t);
|
|
DoutCurlEasy("CURLINFO_PRETRANSFER_TIME = " << t);
|
|
getinfo(CURLINFO_STARTTRANSFER_TIME, &t);
|
|
DoutCurlEasy("CURLINFO_STARTTRANSFER_TIME = " << t);
|
|
}
|
|
|
|
// CURL-THREAD
|
|
// This is called when the easy handle is actually being added to the multi handle (thus after being queued).
|
|
// AIFIXME: Doing this only when it is actually being added assures that the first curl easy handle that is
|
|
// being added for a particular host will be the one getting extra 'DNS lookup' connect time.
|
|
// However, if another curl easy handle for the same host is added immediately after, it will
|
|
// get less connect time, while it still (also) has to wait for this DNS lookup.
|
|
//
|
|
// <-----------------------------mTimeoutNothingReceivedYet-------------------------->
|
|
// queued--><--DNS lookup + connect + send headers-->[<--send body (if any)-->]<--replydelay--><--receive headers + body--><--done
|
|
// ^
|
|
// |
|
|
void CurlEasyRequest::timeout_add_easy_request(void)
|
|
{
|
|
setopt(CURLOPT_CONNECTTIMEOUT, mTimeoutPolicy->getConnectTimeout(mTimeoutLowercaseHostname));
|
|
setopt(CURLOPT_TIMEOUT, mTimeoutPolicy->getCurlTransaction());
|
|
// This boolean is valid (only) if we get a time out event from libcurl.
|
|
mTimeoutLowSpeedOn = false;
|
|
mTimeoutNothingReceivedYet = true;
|
|
mTimeoutStalled = (U64)-1;
|
|
DoutCurlEasy("timeout_add_easy_request: mTimeoutStalled set to -1");
|
|
mTimeoutUploadFinished = false;
|
|
}
|
|
|
|
// CURL-THREAD
|
|
// This is called when body data was sent to the server socket.
|
|
// <--mTimeoutLowSpeedOn-->
|
|
// queued--><--DNS lookup + connect + send headers-->[<--send body (if any)-->]<--replydelay--><--receive headers + body--><--done
|
|
// ^ ^ ^ ^ ^ ^
|
|
// | | | | | |
|
|
bool CurlEasyRequest::timeout_data_sent(size_t n)
|
|
{
|
|
// Generate events.
|
|
if (!mTimeoutLowSpeedOn)
|
|
{
|
|
// If we can send data (for the first time) then that's our only way to know we connected.
|
|
timeout_reset_lowspeed();
|
|
}
|
|
// Detect low speed.
|
|
return timeout_lowspeed(n);
|
|
}
|
|
|
|
// CURL-THREAD
|
|
// This is called when the 'low speed' timer should be started.
|
|
// <--mTimeoutLowSpeedOn--> <----mTimeoutLowSpeedOn---->
|
|
// queued--><--DNS lookup + connect + send headers-->[<--send body (if any)-->]<--replydelay--><--receive headers + body--><--done
|
|
// ^ ^
|
|
// | |
|
|
void CurlEasyRequest::timeout_reset_lowspeed(void)
|
|
{
|
|
mTimeoutLowSpeedClock = sTimeoutClockCount;
|
|
mTimeoutLowSpeedOn = true;
|
|
mTimeoutLastSecond = -1; // This causes timeout_lowspeed to initialize the rest.
|
|
mTimeoutStalled = (U64)-1; // Stop reply delay timer.
|
|
DoutCurlEasy("timeout_reset_lowspeed: mTimeoutLowSpeedClock = " << mTimeoutLowSpeedClock << "; mTimeoutStalled = -1");
|
|
}
|
|
|
|
// CURL-THREAD
|
|
// This is called when everything we had to send to the server has been sent.
|
|
// <--mTimeoutLowSpeedOn-->
|
|
// queued--><--DNS lookup + connect + send headers-->[<--send body (if any)-->]<--replydelay--><--receive headers + body--><--done
|
|
// ^
|
|
// |
|
|
void CurlEasyRequest::timeout_upload_finished(void)
|
|
{
|
|
if (1 || mTimeoutUploadFinished)
|
|
timeout_timings();
|
|
llassert(!mTimeoutUploadFinished); // If we get here twice, then the 'upload finished' detection failed.
|
|
mTimeoutUploadFinished = true;
|
|
// We finished uploading (if there was a body to upload at all), so not more transfer rate timeouts.
|
|
mTimeoutLowSpeedOn = false;
|
|
// Timeout if the server doesn't reply quick enough.
|
|
mTimeoutStalled = sTimeoutClockCount + mTimeoutPolicy->getReplyDelay() / sTimeoutClockWidth;
|
|
DoutCurlEasy("timeout_upload_finished: mTimeoutStalled set to sTimeoutClockCount (" << sTimeoutClockCount << ") + " << (mTimeoutStalled - sTimeoutClockCount) << " (" << mTimeoutPolicy->getReplyDelay() << " seconds)");
|
|
}
|
|
|
|
// CURL-THREAD
|
|
// This is called when data was received from the server.
|
|
//
|
|
// <-----------------------------mTimeoutNothingReceivedYet--------------------------><----mTimeoutLowSpeedOn---->
|
|
// queued--><--DNS lookup + connect + send headers-->[<--send body (if any)-->]<--replydelay--><--receive headers + body--><--done
|
|
// ^ ^ ^ ^ ^ ^ ^ ^
|
|
// | | | | | | | |
|
|
bool CurlEasyRequest::timeout_data_received(size_t n)
|
|
{
|
|
// The HTTP header of the reply is the first thing we receive.
|
|
if (mTimeoutNothingReceivedYet && n > 0)
|
|
{
|
|
mTimeoutNothingReceivedYet = false;
|
|
if (!mTimeoutUploadFinished)
|
|
{
|
|
// mTimeoutUploadFinished not being set this point should only happen for GET requests (in fact, then it is normal),
|
|
// because in that case it is impossible to detect the difference between connecting and waiting for a reply without
|
|
// using CURLOPT_DEBUGFUNCTION. Note that mDebugIsGetMethod is only valid when the debug channel 'curlio' is on,
|
|
// because it is set in the debug callback function.
|
|
Debug(llassert(mDebugIsGetMethod || !dc::curlio.is_on()));
|
|
// 'Upload finished' detection failed, generate it now.
|
|
timeout_upload_finished();
|
|
}
|
|
// We received something; switch to getLowSpeedLimit()/getLowSpeedTime().
|
|
timeout_reset_lowspeed();
|
|
}
|
|
return mTimeoutLowSpeedOn ? timeout_lowspeed(n) : false;
|
|
}
|
|
|
|
// CURL_THREAD
|
|
// bytes is the number of bytes we just sent or received (including headers).
|
|
// Returns true if the transfer should be aborted.
|
|
//
|
|
// queued--><--DNS lookup + connect + send headers-->[<--send body (if any)-->]<--replydelay--><--receive headers + body--><--done
|
|
// ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^
|
|
// | | | | | | | | | | | | | |
|
|
bool CurlEasyRequest::timeout_lowspeed(size_t bytes)
|
|
{
|
|
DoutCurlEasyEntering("CurlEasyRequest::timeout_lowspeed(" << bytes << ")");
|
|
|
|
// The algorithm to determine if we timed out if different from how libcurls CURLOPT_LOW_SPEED_TIME works.
|
|
//
|
|
// libcurl determines the transfer rate since the last call to an equivalent 'lowspeed' function, and then
|
|
// triggers a timeout if CURLOPT_LOW_SPEED_TIME long such a transfer value is less than CURLOPT_LOW_SPEED_LIMIT.
|
|
// That doesn't work right because once there IS data it can happen that this function is called a few
|
|
// times (with less than a milisecond in between) causing seemingly VERY high "transfer rate" spikes.
|
|
// The only correct way to determine the transfer rate is to actually average over CURLOPT_LOW_SPEED_TIME
|
|
// seconds.
|
|
//
|
|
// We do this as follows: we create low_speed_time (in seconds) buckets and fill them with the number
|
|
// of bytes received during that second. We also keep track of the sum of all bytes received between 'now'
|
|
// and 'now - llmax(starttime, low_speed_time)'. Then if that period reaches at least low_speed_time
|
|
// seconds, and the transfer rate (sum / low_speed_time) is less than low_speed_limit, we abort.
|
|
|
|
// When are we?
|
|
S32 second = (sTimeoutClockCount - mTimeoutLowSpeedClock) * sTimeoutClockWidth;
|
|
llassert(sTimeoutClockWidth > 0.0);
|
|
// This REALLY should never happen, but due to another bug it did happened
|
|
// and caused something so evil and hard to find that... NEVER AGAIN!
|
|
llassert(second >= 0);
|
|
|
|
// If this is the same second as last time, just add the number of bytes to the current bucket.
|
|
if (second == mTimeoutLastSecond)
|
|
{
|
|
mTimeoutTotalBytes += bytes;
|
|
mTimeoutBuckets[mTimeoutBucket] += bytes;
|
|
return false;
|
|
}
|
|
|
|
// We arrived at a new second.
|
|
// The below is at most executed once per second, even though for
|
|
// every currently connected transfer, CPU is not a big issue.
|
|
|
|
// Determine the number of buckets needed and increase the number of buckets if needed.
|
|
U16 const low_speed_time = mTimeoutPolicy->getLowSpeedTime();
|
|
if (low_speed_time > mTimeoutBuckets.size())
|
|
{
|
|
mTimeoutBuckets.resize(low_speed_time, 0);
|
|
}
|
|
|
|
S32 s = mTimeoutLastSecond;
|
|
mTimeoutLastSecond = second;
|
|
|
|
// If this is the first time this function is called, we need to do some initialization.
|
|
if (s == -1)
|
|
{
|
|
mTimeoutBucket = 0; // It doesn't really matter where we start.
|
|
mTimeoutTotalBytes = bytes;
|
|
mTimeoutBuckets[mTimeoutBucket] = bytes;
|
|
return false;
|
|
}
|
|
|
|
// Update all administration.
|
|
U16 bucket = mTimeoutBucket;
|
|
while(1) // Run over all the seconds that were skipped.
|
|
{
|
|
if (++bucket == low_speed_time)
|
|
bucket = 0;
|
|
if (++s == second)
|
|
break;
|
|
mTimeoutTotalBytes -= mTimeoutBuckets[bucket];
|
|
mTimeoutBuckets[bucket] = 0;
|
|
}
|
|
mTimeoutBucket = bucket;
|
|
mTimeoutTotalBytes -= mTimeoutBuckets[mTimeoutBucket];
|
|
mTimeoutTotalBytes += bytes;
|
|
mTimeoutBuckets[mTimeoutBucket] = bytes;
|
|
|
|
// Check if we timed out.
|
|
U32 const low_speed_limit = mTimeoutPolicy->getLowSpeedLimit();
|
|
U32 mintotalbytes = low_speed_limit * low_speed_time;
|
|
DoutCurlEasy("Transfered " << mTimeoutTotalBytes << " bytes in " << llmin(second, (S32)low_speed_time) << " seconds after " << second << " second" << ((second == 1) ? "" : "s") << ".");
|
|
if (second >= low_speed_time)
|
|
{
|
|
DoutCurlEasy("Average transfer rate is " << (mTimeoutTotalBytes / low_speed_time) << " bytes/s (low speed limit is " << low_speed_limit << " bytes/s)");
|
|
if (mTimeoutTotalBytes < mintotalbytes)
|
|
{
|
|
// The average transfer rate over the passed low_speed_time seconds is too low. Abort the transfer.
|
|
llwarns <<
|
|
#ifdef CWDEBUG
|
|
(void*)get_lockobj() << ": "
|
|
#endif
|
|
"aborting slow connection (average transfer rate below " << low_speed_limit <<
|
|
" for more than " << low_speed_time << " second" << ((low_speed_time == 1) ? "" : "s") << ")." << llendl;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Calculate how long the data transfer may stall until we should timeout.
|
|
llassert_always(mintotalbytes > 0);
|
|
S32 max_stall_time = 0;
|
|
U32 dropped_bytes = 0;
|
|
while(1)
|
|
{
|
|
if (++bucket == low_speed_time) // The next second the next bucket will be emptied.
|
|
bucket = 0;
|
|
++max_stall_time;
|
|
dropped_bytes += mTimeoutBuckets[bucket];
|
|
// Note how, when max_stall_time == low_speed_time, dropped_bytes has
|
|
// to be equal to mTimeoutTotalBytes, the sum of all vector elements.
|
|
llassert_always(max_stall_time < low_speed_time || dropped_bytes == mTimeoutTotalBytes);
|
|
// And thus the following will certainly abort.
|
|
if (second + max_stall_time >= low_speed_time && mTimeoutTotalBytes - dropped_bytes < mintotalbytes)
|
|
break;
|
|
}
|
|
// If this function isn't called again within max_stall_time seconds, we stalled.
|
|
mTimeoutStalled = sTimeoutClockCount + max_stall_time / sTimeoutClockWidth;
|
|
DoutCurlEasy("mTimeoutStalled set to sTimeoutClockCount (" << sTimeoutClockCount << ") + " << (mTimeoutStalled - sTimeoutClockCount) << " (" << max_stall_time << " seconds)");
|
|
|
|
return false;
|
|
}
|
|
|
|
// CURL-THREAD
|
|
// This is called immediately before done() after curl finished, with code.
|
|
// <----mTimeoutLowSpeedOn---->
|
|
// queued--><--DNS lookup + connect + send headers-->[<--send body (if any)-->]<--replydelay--><--receive headers + body--><--done
|
|
// ^
|
|
// |
|
|
void CurlEasyRequest::timeout_done(CURLcode code)
|
|
{
|
|
timeout_timings();
|
|
llassert(mTimeoutUploadFinished || mTimeoutNothingReceivedYet); // If this is false then the 'upload finished' detection failed.
|
|
if (code == CURLE_OPERATION_TIMEDOUT)
|
|
{
|
|
if (mTimeoutNothingReceivedYet)
|
|
{
|
|
// Only consider this to possibly be related to a DNS lookup if we didn't connect
|
|
// to the remote host yet. Note that CURLINFO_CONNECT_TIME gives the time needed
|
|
// to connect to the proxy, or first host-- and fails to take redirects into account.
|
|
// Also note that CURLINFO_APPCONNECT_TIME gives the time of the FIRST actual
|
|
// connect, so for any transfers on the same pipeline that come after this we
|
|
// also correctly determine that there was not a problem with a DNS lookup.
|
|
double appconnect_time;
|
|
getinfo(CURLINFO_APPCONNECT_TIME, &appconnect_time);
|
|
if (appconnect_time == 0)
|
|
{
|
|
// Inform policy object that there might be problems with resolving this host.
|
|
AIHTTPTimeoutPolicy::connect_timed_out(mTimeoutLowercaseHostname);
|
|
// AIFIXME: use return value to change priority
|
|
}
|
|
}
|
|
}
|
|
// Make sure no timeout will happen anymore.
|
|
mTimeoutLowSpeedOn = false;
|
|
mTimeoutStalled = (U64)-1;
|
|
DoutCurlEasy("timeout_done: mTimeoutStalled set to -1");
|
|
}
|
|
|
|
void CurlEasyRequest::timeout_print_diagnostics(AIHTTPTimeoutPolicy const& policy)
|
|
{
|
|
llwarns << "Request to " << mTimeoutLowercaseHostname << " timed out for " << policy.name() << llendl;
|
|
}
|
|
|
|
// End of HTTP Timeout stuff.
|
|
//.............................................................................
|
|
|
|
void CurlEasyRequest::getTransferInfo(AICurlInterface::TransferInfo* info)
|
|
{
|
|
// Curl explicitly demands a double for these info's.
|
|
double size, total_time, speed;
|
|
getinfo(CURLINFO_SIZE_DOWNLOAD, &size);
|
|
getinfo(CURLINFO_TOTAL_TIME, &total_time);
|
|
getinfo(CURLINFO_SPEED_DOWNLOAD, &speed);
|
|
// Convert to F64.
|
|
info->mSizeDownload = size;
|
|
info->mTotalTime = total_time;
|
|
info->mSpeedDownload = speed;
|
|
}
|
|
|
|
void CurlEasyRequest::getResult(CURLcode* result, AICurlInterface::TransferInfo* info)
|
|
{
|
|
*result = mResult;
|
|
if (info && mResult != CURLE_FAILED_INIT)
|
|
{
|
|
getTransferInfo(info);
|
|
}
|
|
}
|
|
|
|
void CurlEasyRequest::added_to_multi_handle(AICurlEasyRequest_wat& curl_easy_request_w)
|
|
{
|
|
if (mEventsTarget)
|
|
mEventsTarget->added_to_multi_handle(curl_easy_request_w);
|
|
}
|
|
|
|
void CurlEasyRequest::finished(AICurlEasyRequest_wat& curl_easy_request_w)
|
|
{
|
|
if (mEventsTarget)
|
|
mEventsTarget->finished(curl_easy_request_w);
|
|
}
|
|
|
|
void CurlEasyRequest::removed_from_multi_handle(AICurlEasyRequest_wat& curl_easy_request_w)
|
|
{
|
|
if (mEventsTarget)
|
|
mEventsTarget->removed_from_multi_handle(curl_easy_request_w);
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// CurlResponderBuffer
|
|
|
|
static int const HTTP_REDIRECTS_DEFAULT = 10;
|
|
|
|
LLChannelDescriptors const CurlResponderBuffer::sChannels;
|
|
|
|
CurlResponderBuffer::CurlResponderBuffer() : mRequestTransferedBytes(0), mResponseTransferedBytes(0), mEventsTarget(NULL)
|
|
{
|
|
ThreadSafeBufferedCurlEasyRequest* lockobj = get_lockobj();
|
|
AICurlEasyRequest_wat curl_easy_request_w(*lockobj);
|
|
curl_easy_request_w->send_events_to(this);
|
|
}
|
|
|
|
#define llmaybeerrs lllog(LLApp::isRunning() ? LLError::LEVEL_ERROR : LLError::LEVEL_WARN, NULL, NULL, false, true)
|
|
|
|
// The callbacks need to be revoked when the CurlResponderBuffer is destructed (because that is what the callbacks use).
|
|
// The AIThreadSafeSimple<CurlResponderBuffer> is destructed first (right to left), so when we get here then the
|
|
// ThreadSafeCurlEasyRequest base class of ThreadSafeBufferedCurlEasyRequest is still intact and we can create
|
|
// and use curl_easy_request_w.
|
|
CurlResponderBuffer::~CurlResponderBuffer()
|
|
{
|
|
ThreadSafeBufferedCurlEasyRequest* lockobj = get_lockobj();
|
|
AICurlEasyRequest_wat curl_easy_request_w(*lockobj); // Wait 'til possible callbacks have returned.
|
|
curl_easy_request_w->send_events_to(NULL);
|
|
curl_easy_request_w->revokeCallbacks();
|
|
if (mResponder)
|
|
{
|
|
// If the responder is still alive, then that means that CurlResponderBuffer::processOutput was
|
|
// never called, which means that the removed_from_multi_handle event never happened.
|
|
// This is definitely an internal error as it can only happen when libcurl is too slow,
|
|
// in which case AICurlEasyRequestStateMachine::mTimer times out, but that already
|
|
// calls CurlResponderBuffer::timed_out().
|
|
llmaybeerrs << "Calling ~CurlResponderBuffer() with active responder!" << llendl;
|
|
if (!LLApp::isRunning())
|
|
{
|
|
// It might happen if some CurlResponderBuffer escaped clean up somehow :/
|
|
mResponder = NULL;
|
|
}
|
|
else
|
|
{
|
|
// User chose to continue.
|
|
timed_out();
|
|
}
|
|
}
|
|
}
|
|
|
|
void CurlResponderBuffer::timed_out(void)
|
|
{
|
|
mResponder->completedRaw(HTTP_INTERNAL_ERROR, "Request timeout, aborted.", sChannels, mOutput);
|
|
mResponder = NULL;
|
|
}
|
|
|
|
void CurlResponderBuffer::resetState(AICurlEasyRequest_wat& curl_easy_request_w)
|
|
{
|
|
llassert(!mResponder);
|
|
|
|
curl_easy_request_w->resetState();
|
|
|
|
mOutput.reset();
|
|
mInput.reset();
|
|
}
|
|
|
|
ThreadSafeBufferedCurlEasyRequest* CurlResponderBuffer::get_lockobj(void)
|
|
{
|
|
return static_cast<ThreadSafeBufferedCurlEasyRequest*>(AIThreadSafeSimple<CurlResponderBuffer>::wrapper_cast(this));
|
|
}
|
|
|
|
void CurlResponderBuffer::prepRequest(AICurlEasyRequest_wat& curl_easy_request_w, AIHTTPHeaders const& headers, AICurlInterface::ResponderPtr responder)
|
|
{
|
|
mInput.reset(new LLBufferArray);
|
|
mInput->setThreaded(true);
|
|
mLastRead = NULL;
|
|
|
|
mOutput.reset(new LLBufferArray);
|
|
mOutput->setThreaded(true);
|
|
|
|
ThreadSafeBufferedCurlEasyRequest* lockobj = get_lockobj();
|
|
curl_easy_request_w->setWriteCallback(&curlWriteCallback, lockobj);
|
|
curl_easy_request_w->setReadCallback(&curlReadCallback, lockobj);
|
|
curl_easy_request_w->setHeaderCallback(&curlHeaderCallback, lockobj);
|
|
|
|
// Allow up to ten redirects.
|
|
if (responder && responder->followRedir())
|
|
{
|
|
curl_easy_request_w->setopt(CURLOPT_FOLLOWLOCATION, 1);
|
|
curl_easy_request_w->setopt(CURLOPT_MAXREDIRS, HTTP_REDIRECTS_DEFAULT);
|
|
}
|
|
|
|
curl_easy_request_w->setopt(CURLOPT_SSL_VERIFYPEER, 1);
|
|
// Don't verify host name so urls with scrubbed host names will work (improves DNS performance).
|
|
curl_easy_request_w->setopt(CURLOPT_SSL_VERIFYHOST, 0);
|
|
|
|
// Keep responder alive.
|
|
mResponder = responder;
|
|
// Send header events to responder if needed.
|
|
if (mResponder->needsHeaders())
|
|
{
|
|
send_events_to(mResponder.get());
|
|
}
|
|
|
|
// Add extra headers.
|
|
curl_easy_request_w->addHeaders(headers);
|
|
}
|
|
|
|
//static
|
|
size_t CurlResponderBuffer::curlWriteCallback(char* data, size_t size, size_t nmemb, void* user_data)
|
|
{
|
|
ThreadSafeBufferedCurlEasyRequest* lockobj = static_cast<ThreadSafeBufferedCurlEasyRequest*>(user_data);
|
|
|
|
// We need to lock the curl easy request object too, because that lock is used
|
|
// to make sure that callbacks and destruction aren't done simultaneously.
|
|
AICurlEasyRequest_wat buffered_easy_request_w(*lockobj);
|
|
|
|
S32 bytes = size * nmemb; // The amount to write.
|
|
AICurlResponderBuffer_wat buffer_w(*lockobj);
|
|
// CurlResponderBuffer::setBodyLimit is never called, so buffer_w->mBodyLimit is infinite.
|
|
//S32 bytes = llmin(size * nmemb, buffer_w->mBodyLimit); buffer_w->mBodyLimit -= bytes;
|
|
buffer_w->getOutput()->append(sChannels.in(), (U8 const*)data, bytes);
|
|
buffer_w->mResponseTransferedBytes += bytes; // Accumulate data received from the server.
|
|
if (buffered_easy_request_w->timeout_data_received(bytes)) // Update timeout administration.
|
|
{
|
|
// Transfer timed out. Return 0 which will abort with error CURLE_WRITE_ERROR.
|
|
return 0;
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
//static
|
|
size_t CurlResponderBuffer::curlReadCallback(char* data, size_t size, size_t nmemb, void* user_data)
|
|
{
|
|
ThreadSafeBufferedCurlEasyRequest* lockobj = static_cast<ThreadSafeBufferedCurlEasyRequest*>(user_data);
|
|
|
|
// We need to lock the curl easy request object too, because that lock is used
|
|
// to make sure that callbacks and destruction aren't done simultaneously.
|
|
AICurlEasyRequest_wat buffered_easy_request_w(*lockobj);
|
|
|
|
S32 bytes = size * nmemb; // The maximum amount to read.
|
|
AICurlResponderBuffer_wat buffer_w(*lockobj);
|
|
buffer_w->mLastRead = buffer_w->getInput()->readAfter(sChannels.out(), buffer_w->mLastRead, (U8*)data, bytes);
|
|
buffer_w->mRequestTransferedBytes += bytes; // Accumulate data sent to the server.
|
|
if (buffered_easy_request_w->timeout_data_sent(bytes)) // Timeout administration.
|
|
{
|
|
// Transfer timed out. Return CURL_READFUNC_ABORT which will abort with error CURLE_ABORTED_BY_CALLBACK.
|
|
return CURL_READFUNC_ABORT;
|
|
}
|
|
return bytes; // Return the amount actually read (might be lowered by readAfter()).
|
|
}
|
|
|
|
//static
|
|
size_t CurlResponderBuffer::curlHeaderCallback(char* data, size_t size, size_t nmemb, void* user_data)
|
|
{
|
|
ThreadSafeBufferedCurlEasyRequest* lockobj = static_cast<ThreadSafeBufferedCurlEasyRequest*>(user_data);
|
|
|
|
// We need to lock the curl easy request object, because that lock is used
|
|
// to make sure that callbacks and destruction aren't done simultaneously.
|
|
AICurlEasyRequest_wat buffered_easy_request_w(*lockobj);
|
|
|
|
// This used to be headerCallback() in llurlrequest.cpp.
|
|
|
|
char const* const header_line = static_cast<char const*>(data);
|
|
size_t const header_len = size * nmemb;
|
|
if (buffered_easy_request_w->timeout_data_received(header_len)) // Update timeout administration.
|
|
{
|
|
// Transfer timed out. Return 0 which will abort with error CURLE_WRITE_ERROR.
|
|
return 0;
|
|
}
|
|
if (!header_len)
|
|
{
|
|
return header_len;
|
|
}
|
|
std::string header(header_line, header_len);
|
|
if (!LLStringUtil::_isASCII(header))
|
|
{
|
|
return header_len;
|
|
}
|
|
|
|
// Per HTTP spec the first header line must be the status line.
|
|
if (header.substr(0, 5) == "HTTP/")
|
|
{
|
|
std::string::iterator const begin = header.begin();
|
|
std::string::iterator const end = header.end();
|
|
std::string::iterator pos1 = std::find(begin, end, ' ');
|
|
if (pos1 != end) ++pos1;
|
|
std::string::iterator pos2 = std::find(pos1, end, ' ');
|
|
if (pos2 != end) ++pos2;
|
|
std::string::iterator pos3 = std::find(pos2, end, '\r');
|
|
U32 status;
|
|
std::string reason;
|
|
if (pos3 != end && std::isdigit(*pos1))
|
|
{
|
|
status = atoi(&header_line[pos1 - begin]);
|
|
reason.assign(pos2, pos3);
|
|
}
|
|
else
|
|
{
|
|
status = HTTP_INTERNAL_ERROR;
|
|
reason = "Header parse error.";
|
|
llwarns << "Received broken header line from server: \"" << header << "\"" << llendl;
|
|
}
|
|
{
|
|
AICurlResponderBuffer_wat curl_responder_buffer_w(*lockobj);
|
|
curl_responder_buffer_w->received_HTTP_header();
|
|
curl_responder_buffer_w->setStatusAndReason(status, reason);
|
|
}
|
|
return header_len;
|
|
}
|
|
|
|
std::string::iterator sep = std::find(header.begin(), header.end(), ':');
|
|
|
|
if (sep != header.end())
|
|
{
|
|
std::string key(header.begin(), sep);
|
|
std::string value(sep + 1, header.end());
|
|
|
|
key = utf8str_tolower(utf8str_trim(key));
|
|
value = utf8str_trim(value);
|
|
|
|
AICurlResponderBuffer_wat(*lockobj)->received_header(key, value);
|
|
}
|
|
else
|
|
{
|
|
LLStringUtil::trim(header);
|
|
if (!header.empty())
|
|
{
|
|
llwarns << "Unable to parse header: " << header << llendl;
|
|
}
|
|
}
|
|
|
|
return header_len;
|
|
}
|
|
|
|
void CurlResponderBuffer::setStatusAndReason(U32 status, std::string const& reason)
|
|
{
|
|
mStatus = status;
|
|
mReason = reason;
|
|
}
|
|
|
|
void CurlResponderBuffer::added_to_multi_handle(AICurlEasyRequest_wat& curl_easy_request_w)
|
|
{
|
|
llerrs << "Unexpected call to added_to_multi_handle()." << llendl;
|
|
}
|
|
|
|
void CurlResponderBuffer::finished(AICurlEasyRequest_wat& curl_easy_request_w)
|
|
{
|
|
llerrs << "Unexpected call to finished()." << llendl;
|
|
}
|
|
|
|
void CurlResponderBuffer::removed_from_multi_handle(AICurlEasyRequest_wat& curl_easy_request_w)
|
|
{
|
|
DoutCurlEasy("Calling CurlResponderBuffer::removed_from_multi_handle(@" << (void*)&*curl_easy_request_w << ") for this = " << (void*)this);
|
|
|
|
// Lock self.
|
|
ThreadSafeBufferedCurlEasyRequest* lockobj = get_lockobj();
|
|
llassert(dynamic_cast<ThreadSafeBufferedCurlEasyRequest*>(static_cast<ThreadSafeCurlEasyRequest*>(ThreadSafeCurlEasyRequest::wrapper_cast(&*curl_easy_request_w))) == lockobj);
|
|
AICurlResponderBuffer_wat buffer_w(*lockobj);
|
|
llassert(&*buffer_w == this);
|
|
|
|
processOutput(curl_easy_request_w);
|
|
}
|
|
|
|
void CurlResponderBuffer::processOutput(AICurlEasyRequest_wat& curl_easy_request_w)
|
|
{
|
|
U32 responseCode = 0;
|
|
std::string responseReason;
|
|
|
|
CURLcode code;
|
|
curl_easy_request_w->getResult(&code);
|
|
if (code == CURLE_OK)
|
|
{
|
|
curl_easy_request_w->getinfo(CURLINFO_RESPONSE_CODE, &responseCode);
|
|
// If getResult code is CURLE_OK then we should have decoded the first header line ourselves.
|
|
llassert(responseCode == mStatus);
|
|
if (responseCode == mStatus)
|
|
responseReason = mReason;
|
|
else
|
|
responseReason = "Unknown reason.";
|
|
}
|
|
else
|
|
{
|
|
responseCode = 499;
|
|
responseReason = AICurlInterface::strerror(code);
|
|
curl_easy_request_w->setopt(CURLOPT_FRESH_CONNECT, TRUE);
|
|
}
|
|
|
|
if (mResponder)
|
|
{
|
|
if (code == CURLE_OPERATION_TIMEDOUT)
|
|
{
|
|
curl_easy_request_w->timeout_print_diagnostics(mResponder->getHTTPTimeoutPolicy());
|
|
}
|
|
if (mEventsTarget)
|
|
{
|
|
// Only the responder registers for these events.
|
|
llassert(mEventsTarget == mResponder.get());
|
|
// Allow clients to parse headers before we attempt to parse
|
|
// the body and provide completed/result/error calls.
|
|
mEventsTarget->completed_headers(responseCode, responseReason);
|
|
}
|
|
mResponder->completedRaw(responseCode, responseReason, sChannels, mOutput);
|
|
mResponder = NULL;
|
|
}
|
|
|
|
resetState(curl_easy_request_w);
|
|
}
|
|
|
|
void CurlResponderBuffer::received_HTTP_header(void)
|
|
{
|
|
if (mEventsTarget)
|
|
mEventsTarget->received_HTTP_header();
|
|
}
|
|
|
|
void CurlResponderBuffer::received_header(std::string const& key, std::string const& value)
|
|
{
|
|
if (mEventsTarget)
|
|
mEventsTarget->received_header(key, value);
|
|
}
|
|
|
|
void CurlResponderBuffer::completed_headers(U32 status, std::string const& reason)
|
|
{
|
|
if (mEventsTarget)
|
|
mEventsTarget->completed_headers(status, reason);
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// CurlMultiHandle
|
|
|
|
LLAtomicU32 CurlMultiHandle::sTotalMultiHandles;
|
|
|
|
CurlMultiHandle::CurlMultiHandle(void)
|
|
{
|
|
DoutEntering(dc::curl, "CurlMultiHandle::CurlMultiHandle() [" << (void*)this << "].");
|
|
mMultiHandle = curl_multi_init();
|
|
Stats::multi_calls++;
|
|
if (!mMultiHandle)
|
|
{
|
|
Stats::multi_errors++;
|
|
throw AICurlNoMultiHandle("curl_multi_init() returned NULL");
|
|
}
|
|
sTotalMultiHandles++;
|
|
}
|
|
|
|
CurlMultiHandle::~CurlMultiHandle()
|
|
{
|
|
curl_multi_cleanup(mMultiHandle);
|
|
Stats::multi_calls++;
|
|
#ifdef CWDEBUG
|
|
int total = --sTotalMultiHandles;
|
|
Dout(dc::curl, "Called CurlMultiHandle::~CurlMultiHandle() [" << (void*)this << "], " << total << " remaining.");
|
|
#else
|
|
--sTotalMultiHandles;
|
|
#endif
|
|
}
|
|
|
|
} // namespace AICurlPrivate
|