/** * @file _httplibcurl.cpp * @brief Internal definitions of the Http libcurl thread * * $LicenseInfo:firstyear=2012&license=viewerlgpl$ * Second Life Viewer Source Code * Copyright (C) 2012-2014, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License only. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA * $/LicenseInfo$ */ #include "_httplibcurl.h" #include "httpheaders.h" #include "bufferarray.h" #include "_httpoprequest.h" #include "_httppolicy.h" #include "llhttpconstants.h" namespace { // Error testing and reporting for libcurl status codes void check_curl_multi_code(CURLMcode code); void check_curl_multi_code(CURLMcode code, int curl_setopt_option); // This is a template because different 'option' values require different // types for 'ARG'. Just pass them through unchanged (by value). template <typename ARG> void check_curl_multi_setopt(CURLM* handle, CURLMoption option, ARG argument) { CURLMcode code = curl_multi_setopt(handle, option, argument); check_curl_multi_code(code, option); } static const char * const LOG_CORE("CoreHttp"); } // end anonymous namespace namespace LLCore { HttpLibcurl::HttpLibcurl(HttpService * service) : mService(service), mHandleCache(), mPolicyCount(0), mMultiHandles(NULL), mActiveHandles(NULL), mDirtyPolicy(NULL) {} HttpLibcurl::~HttpLibcurl() { shutdown(); mService = NULL; } void HttpLibcurl::shutdown() { while (! mActiveOps.empty()) { HttpOpRequest::ptr_t op(* mActiveOps.begin()); mActiveOps.erase(mActiveOps.begin()); cancelRequest(op); } if (mMultiHandles) { for (int policy_class(0); policy_class < mPolicyCount; ++policy_class) { if (mMultiHandles[policy_class]) { curl_multi_cleanup(mMultiHandles[policy_class]); mMultiHandles[policy_class] = 0; } } delete [] mMultiHandles; mMultiHandles = NULL; delete [] mActiveHandles; mActiveHandles = NULL; delete [] mDirtyPolicy; mDirtyPolicy = NULL; } mPolicyCount = 0; } void HttpLibcurl::start(int policy_count) { LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK; llassert_always(policy_count <= HTTP_POLICY_CLASS_LIMIT); llassert_always(! mMultiHandles); // One-time call only mPolicyCount = policy_count; mMultiHandles = new CURLM * [mPolicyCount]; mActiveHandles = new int [mPolicyCount]; mDirtyPolicy = new bool [mPolicyCount]; for (int policy_class(0); policy_class < mPolicyCount; ++policy_class) { if (NULL == (mMultiHandles[policy_class] = curl_multi_init())) { LL_ERRS(LOG_CORE) << "Failed to allocate multi handle in libcurl." << LL_ENDL; } mActiveHandles[policy_class] = 0; mDirtyPolicy[policy_class] = false; policyUpdated(policy_class); } } // Give libcurl some cycles, invoke it's callbacks, process // completed requests finalizing or issuing retries as needed. // // If active list goes empty *and* we didn't queue any // requests for retry, we return a request for a hard // sleep otherwise ask for a normal polling interval. HttpService::ELoopSpeed HttpLibcurl::processTransport() { LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK; HttpService::ELoopSpeed ret(HttpService::REQUEST_SLEEP); // Give libcurl some cycles to do I/O & callbacks for (int policy_class(0); policy_class < mPolicyCount; ++policy_class) { if (! mMultiHandles[policy_class]) { // No handle, nothing to do. continue; } if (! mActiveHandles[policy_class]) { // If we've gone quiet and there's a dirty update, apply it, // otherwise we're done. if (mDirtyPolicy[policy_class]) { policyUpdated(policy_class); } continue; } int running(0); CURLMcode status(CURLM_CALL_MULTI_PERFORM); do { LL_PROFILE_ZONE_NAMED_CATEGORY_NETWORK("httppt - curl_multi_perform"); running = 0; status = curl_multi_perform(mMultiHandles[policy_class], &running); } while (0 != running && CURLM_CALL_MULTI_PERFORM == status); // Run completion on anything done CURLMsg * msg(NULL); int msgs_in_queue(0); { LL_PROFILE_ZONE_NAMED_CATEGORY_NETWORK("httppt - curl_multi_info_read"); while ((msg = curl_multi_info_read(mMultiHandles[policy_class], &msgs_in_queue))) { if (CURLMSG_DONE == msg->msg) { CURL* handle(msg->easy_handle); CURLcode result(msg->data.result); completeRequest(mMultiHandles[policy_class], handle, result); handle = NULL; // No longer valid on return ret = HttpService::NORMAL; // If anything completes, we may have a free slot. // Turning around quickly reduces connection gap by 7-10mS. } else if (CURLMSG_NONE == msg->msg) { // Ignore this... it shouldn't mean anything. ; } else { LL_WARNS_ONCE(LOG_CORE) << "Unexpected message from libcurl. Msg code: " << msg->msg << LL_ENDL; } msgs_in_queue = 0; } } } if (! mActiveOps.empty()) { ret = HttpService::NORMAL; } return ret; } // Caller has provided us with a ref count on op. void HttpLibcurl::addOp(const HttpOpRequest::ptr_t &op) { LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK; llassert_always(op->mReqPolicy < mPolicyCount); llassert_always(mMultiHandles[op->mReqPolicy] != NULL); // Create standard handle if (! op->prepareRequest(mService)) { // Couldn't issue request, fail with notification // *TODO: Need failure path return; } // Make the request live CURLMcode code; code = curl_multi_add_handle(mMultiHandles[op->mReqPolicy], op->mCurlHandle); if (CURLM_OK != code) { // *TODO: Better cleanup and recovery but not much we can do here. check_curl_multi_code(code); return; } op->mCurlActive = true; mActiveOps.insert(op); ++mActiveHandles[op->mReqPolicy]; if (op->mTracing > HTTP_TRACE_OFF) { HttpPolicy & policy(mService->getPolicy()); LL_INFOS(LOG_CORE) << "TRACE, ToActiveQueue, Handle: " << op->getHandle() << ", Actives: " << mActiveOps.size() << ", Readies: " << policy.getReadyCount(op->mReqPolicy) << LL_ENDL; } } // Implements the transport part of any cancel operation. // See if the handle is an active operation and if so, // use the more complicated transport-based cancellation // method to kill the request. bool HttpLibcurl::cancel(HttpHandle handle) { LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK; HttpOpRequest::ptr_t op = HttpOpRequest::fromHandle<HttpOpRequest>(handle); active_set_t::iterator it(mActiveOps.find(op)); if (mActiveOps.end() == it) { return false; } // Cancel request cancelRequest(op); // Drop references mActiveOps.erase(it); --mActiveHandles[op->mReqPolicy]; return true; } // *NOTE: cancelRequest logic parallels completeRequest logic. // Keep them synchronized as necessary. Caller is expected to // remove the op from the active list and release the op *after* // calling this method. It must be called first to deliver the // op to the reply queue with refcount intact. void HttpLibcurl::cancelRequest(const HttpOpRequest::ptr_t &op) { LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK; // Deactivate request op->mCurlActive = false; // Detach from multi and recycle handle curl_multi_remove_handle(mMultiHandles[op->mReqPolicy], op->mCurlHandle); mHandleCache.freeHandle(op->mCurlHandle); op->mCurlHandle = NULL; // Tracing if (op->mTracing > HTTP_TRACE_OFF) { LL_INFOS(LOG_CORE) << "TRACE, RequestCanceled, Handle: " << op->getHandle() << ", Status: " << op->mStatus.toTerseString() << LL_ENDL; } // Cancel op and deliver for notification op->cancel(); } // *NOTE: cancelRequest logic parallels completeRequest logic. // Keep them synchronized as necessary. bool HttpLibcurl::completeRequest(CURLM * multi_handle, CURL * handle, CURLcode status) { LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK; HttpHandle ophandle(NULL); CURLcode ccode(CURLE_OK); ccode = curl_easy_getinfo(handle, CURLINFO_PRIVATE, &ophandle); if (ccode) { LL_WARNS(LOG_CORE) << "libcurl error: " << ccode << " Unable to retrieve operation handle from CURL handle" << LL_ENDL; return false; } HttpOpRequest::ptr_t op(HttpOpRequest::fromHandle<HttpOpRequest>(ophandle)); if (!op) { LL_WARNS() << "Unable to locate operation by handle. May have expired!" << LL_ENDL; return false; } if (handle != op->mCurlHandle || ! op->mCurlActive) { LL_WARNS(LOG_CORE) << "libcurl handle and HttpOpRequest handle in disagreement or inactive request." << " Handle: " << static_cast<HttpHandle>(handle) << LL_ENDL; return false; } active_set_t::iterator it(mActiveOps.find(op)); if (mActiveOps.end() == it) { LL_WARNS(LOG_CORE) << "libcurl completion for request not on active list. Continuing." << " Handle: " << static_cast<HttpHandle>(handle) << LL_ENDL; return false; } // Deactivate request mActiveOps.erase(it); --mActiveHandles[op->mReqPolicy]; op->mCurlActive = false; // Set final status of request if it hasn't failed by other mechanisms yet if (op->mStatus) { op->mStatus = HttpStatus(HttpStatus::EXT_CURL_EASY, status); } if (op->mStatus) { // note: CURLINFO_RESPONSE_CODE requires a long - https://curl.haxx.se/libcurl/c/CURLINFO_RESPONSE_CODE.html long http_status(HTTP_OK); if (handle) { ccode = curl_easy_getinfo(handle, CURLINFO_RESPONSE_CODE, &http_status); if (ccode == CURLE_OK) { if (http_status >= 100 && http_status <= 999) { char * cont_type(NULL); ccode = curl_easy_getinfo(handle, CURLINFO_CONTENT_TYPE, &cont_type); if (ccode == CURLE_OK) { if (cont_type) { op->mReplyConType = cont_type; } } else { LL_WARNS(LOG_CORE) << "CURL error:" << ccode << " Attempting to get content type." << LL_ENDL; } op->mStatus = HttpStatus(http_status); } else { LL_WARNS(LOG_CORE) << "Invalid HTTP response code (" << http_status << ") received from server." << LL_ENDL; op->mStatus = HttpStatus(HttpStatus::LLCORE, HE_INVALID_HTTP_STATUS); } } else { op->mStatus = HttpStatus(HttpStatus::LLCORE, HE_INVALID_HTTP_STATUS); } } else { LL_WARNS(LOG_CORE) << "Attempt to retrieve status from NULL handle!" << LL_ENDL; } } if (multi_handle && handle) { // Detach from multi and recycle handle curl_multi_remove_handle(multi_handle, handle); mHandleCache.freeHandle(op->mCurlHandle); } else { LL_WARNS(LOG_CORE) << "Curl multi_handle or handle is NULL on remove! multi:" << std::hex << multi_handle << " h:" << std::hex << handle << std::dec << LL_ENDL; } op->mCurlHandle = NULL; // Tracing if (op->mTracing > HTTP_TRACE_OFF) { LL_INFOS(LOG_CORE) << "TRACE, RequestComplete, Handle: " << op->getHandle() << ", Status: " << op->mStatus.toTerseString() << LL_ENDL; } // Dispatch to next stage HttpPolicy & policy(mService->getPolicy()); bool still_active(policy.stageAfterCompletion(op)); return still_active; } int HttpLibcurl::getActiveCount() const { return mActiveOps.size(); } int HttpLibcurl::getActiveCountInClass(int policy_class) const { llassert_always(policy_class < mPolicyCount); return mActiveHandles ? mActiveHandles[policy_class] : 0; } void HttpLibcurl::policyUpdated(int policy_class) { LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK; if (policy_class < 0 || policy_class >= mPolicyCount || ! mMultiHandles) { return; } HttpPolicy & policy(mService->getPolicy()); if (! mActiveHandles[policy_class]) { // Clear to set options. As of libcurl 7.37.0, if a pipelining // multi handle has active requests and you try to set the // multi handle to non-pipelining, the library gets very angry // and goes off the rails corrupting memory. A clue that you're // about to crash is that you'll get a missing server response // error (curl code 9). So, if options are to be set, we let // the multi handle run out of requests, then set options, and // re-enable request processing. // // All of this stall mechanism exists for this reason. If // libcurl becomes more resilient later, it should be possible // to remove all of this. The connection limit settings are fine, // it's just that pipelined-to-non-pipelined transition that // is fatal at the moment. HttpPolicyClass & options(policy.getClassOptions(policy_class)); CURLM * multi_handle(mMultiHandles[policy_class]); // Enable policy if stalled policy.stallPolicy(policy_class, false); mDirtyPolicy[policy_class] = false; if (options.mPipelining > 1) { // We'll try to do pipelining on this multihandle check_curl_multi_setopt(multi_handle, CURLMOPT_PIPELINING, 1L); check_curl_multi_setopt(multi_handle, CURLMOPT_MAX_PIPELINE_LENGTH, long(options.mPipelining)); check_curl_multi_setopt(multi_handle, CURLMOPT_MAX_HOST_CONNECTIONS, long(options.mPerHostConnectionLimit)); check_curl_multi_setopt(multi_handle, CURLMOPT_MAX_TOTAL_CONNECTIONS, long(options.mConnectionLimit)); } else { check_curl_multi_setopt(multi_handle, CURLMOPT_PIPELINING, 0L); check_curl_multi_setopt(multi_handle, CURLMOPT_MAX_HOST_CONNECTIONS, 0L); check_curl_multi_setopt(multi_handle, CURLMOPT_MAX_TOTAL_CONNECTIONS, long(options.mConnectionLimit)); } } else if (! mDirtyPolicy[policy_class]) { // Mark policy dirty and request a stall in the policy. // When policy goes idle, we'll re-invoke this method // and perform the change. Don't allow this thread to // sleep while we're waiting for quiescence, we'll just // stop processing. mDirtyPolicy[policy_class] = true; policy.stallPolicy(policy_class, true); } } // --------------------------------------- // HttpLibcurl::HandleCache // --------------------------------------- HttpLibcurl::HandleCache::HandleCache() : mHandleTemplate(NULL) { mCache.reserve(50); } HttpLibcurl::HandleCache::~HandleCache() { if (mHandleTemplate) { curl_easy_cleanup(mHandleTemplate); mHandleTemplate = NULL; } for (handle_cache_t::iterator it(mCache.begin()); mCache.end() != it; ++it) { curl_easy_cleanup(*it); } mCache.clear(); } CURL * HttpLibcurl::HandleCache::getHandle() { CURL * ret(NULL); if (! mCache.empty()) { // Fastest path to handle ret = mCache.back(); mCache.pop_back(); } else if (mHandleTemplate) { // Still fast path ret = curl_easy_duphandle(mHandleTemplate); } else { // When all else fails ret = curl_easy_init(); } return ret; } void HttpLibcurl::HandleCache::freeHandle(CURL * handle) { if (! handle) { return; } curl_easy_reset(handle); if (! mHandleTemplate) { // Save the first freed handle as a template. mHandleTemplate = handle; } else { // Otherwise add it to the cache if (mCache.size() >= mCache.capacity()) { mCache.reserve(mCache.capacity() + 50); } mCache.push_back(handle); } } // --------------------------------------- // Free functions // --------------------------------------- struct curl_slist * append_headers_to_slist(const HttpHeaders::ptr_t &headers, struct curl_slist * slist) { const HttpHeaders::const_iterator end(headers->end()); for (HttpHeaders::const_iterator it(headers->begin()); end != it; ++it) { static const char sep[] = ": "; std::string header; header.reserve((*it).first.size() + (*it).second.size() + sizeof(sep)); header.append((*it).first); header.append(sep); header.append((*it).second); slist = curl_slist_append(slist, header.c_str()); } return slist; } } // end namespace LLCore namespace { void check_curl_multi_code(CURLMcode code, int curl_setopt_option) { if (CURLM_OK != code) { LL_WARNS(LOG_CORE) << "libcurl multi error detected: " << curl_multi_strerror(code) << ", curl_multi_setopt option: " << curl_setopt_option << LL_ENDL; } } void check_curl_multi_code(CURLMcode code) { if (CURLM_OK != code) { LL_WARNS(LOG_CORE) << "libcurl multi error detected: " << curl_multi_strerror(code) << LL_ENDL; } } } // end anonymous namespace