// Copyright Epic Games, Inc. All Rights Reserved. #include #include #include #include #include ZEN_THIRD_PARTY_INCLUDES_START #include ZEN_THIRD_PARTY_INCLUDES_END namespace zen { static_assert(CURL_LOCK_DATA_LAST <= 8, "HttpClientShare m_Locks array too small for this libcurl version"); namespace { void LockCb(CURL* /*Easy*/, curl_lock_data Data, curl_lock_access /*Access*/, void* UserPtr) { // Unlock callback receives only Data, not Access, so we use plain mutexes. // Critical sections (DNS / connect-cache lookups) are microsecond-scale; // for hub localhost workload the lost read parallelism is not measurable. static_cast(UserPtr)->Lock(static_cast(Data)); } void UnlockCb(CURL* /*Easy*/, curl_lock_data Data, void* UserPtr) { static_cast(UserPtr)->Unlock(static_cast(Data)); } } // namespace HttpClientShare::HttpClientShare(uint32_t InFlags) { ZEN_ASSERT_FORMAT(InFlags != 0, "HttpClientShare with no flags is a callback-overhead no-op"); CURLSH* Share = curl_share_init(); if (Share == nullptr) { throw runtime_error("HttpClientShare: curl_share_init failed"); } auto SetOpt = [Share](CURLSHoption Option, auto Value) { const CURLSHcode Code = curl_share_setopt(Share, Option, Value); if (Code != CURLSHE_OK) { curl_share_cleanup(Share); throw runtime_error("HttpClientShare: curl_share_setopt failed: {} ({})", curl_share_strerror(Code), static_cast(Code)); } }; if (InFlags & ShareDns) { SetOpt(CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS); } if (InFlags & ShareConnect) { SetOpt(CURLSHOPT_SHARE, CURL_LOCK_DATA_CONNECT); } if (InFlags & ShareSslSession) { SetOpt(CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION); } SetOpt(CURLSHOPT_LOCKFUNC, &LockCb); SetOpt(CURLSHOPT_UNLOCKFUNC, &UnlockCb); SetOpt(CURLSHOPT_USERDATA, this); m_Share = Share; } HttpClientShare::~HttpClientShare() { if (m_Share) { const CURLSHcode Code = curl_share_cleanup(static_cast(m_Share)); if (Code != CURLSHE_OK) { // Precondition violation: an HttpClient referencing this share is still alive. // CURLSH leaks here; remaining requests on the live client may UAF the share's // lock callbacks. Log loudly so the lifetime bug surfaces in operator output. ZEN_ERROR( "HttpClientShare: curl_share_cleanup failed: {} ({}). Lifetime bug: an HttpClient referencing this share is still alive.", curl_share_strerror(Code), static_cast(Code)); } m_Share = nullptr; } } void HttpClientShare::Lock(int Index) { ZEN_ASSERT_FORMAT(Index >= 0 && Index < static_cast(std::size(m_Locks)), "curl share lock Data={} outside [0,{}); libcurl version drifted past static_assert", Index, std::size(m_Locks)); m_Locks[Index].lock(); } void HttpClientShare::Unlock(int Index) { ZEN_ASSERT_FORMAT(Index >= 0 && Index < static_cast(std::size(m_Locks)), "curl share unlock Data={} outside [0,{}); libcurl version drifted past static_assert", Index, std::size(m_Locks)); m_Locks[Index].unlock(); } } // namespace zen