aboutsummaryrefslogtreecommitdiff
path: root/src/zenhttp/clients/httpclientshare.cpp
blob: 8aadb87b382f62ba8ef52df7a6a2445f89a4e2fb (plain) (blame)
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