1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
|
// Copyright Epic Games, Inc. All Rights Reserved.
#include <zenhttp/httpclientshare.h>
#include <zencore/assertfmt.h>
#include <zencore/except.h>
#include <zencore/except_fmt.h>
#include <zencore/logging.h>
ZEN_THIRD_PARTY_INCLUDES_START
#include <curl/curl.h>
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<HttpClientShare*>(UserPtr)->Lock(static_cast<int>(Data));
}
void UnlockCb(CURL* /*Easy*/, curl_lock_data Data, void* UserPtr)
{
static_cast<HttpClientShare*>(UserPtr)->Unlock(static_cast<int>(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<int>(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<CURLSH*>(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<int>(Code));
}
m_Share = nullptr;
}
}
void
HttpClientShare::Lock(int Index)
{
ZEN_ASSERT_FORMAT(Index >= 0 && Index < static_cast<int>(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<int>(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
|