1/*
2 * LegacyClonk
3 * Copyright (c) 2013-2016, The OpenClonk Team and contributors
4 * Copyright (c) 2023, The LegacyClonk Team and contributors
5 *
6 * Distributed under the terms of the ISC license; see accompanying file
7 * "COPYING" for details.
8 *
9 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
10 * See accompanying file "TRADEMARK" for details.
11 *
12 * To redistribute this file separately, substitute the full license texts
13 * for the above references.
14 */
15
16#include "C4Application.h"
17#include "C4Awaiter.h"
18#include "C4HTTPClient.h"
19#include "C4Version.h"
20
21#include <format>
22
23#define CURL_STRICTER
24#include <curl/curl.h>
25
26static constexpr long C4HTTPQueryTimeout{20}; // (s)
27
28template<typename T, typename... Args> requires (sizeof...(Args) >= 1)
29static decltype(auto) ThrowIfFailed(T &&result, Args &&...args)
30{
31 if (!result)
32 {
33 if constexpr (sizeof...(Args) == 1)
34 {
35 throw C4HTTPClient::Exception{std::forward<Args>(args)...};
36 }
37 else
38 {
39 throw C4HTTPClient::Exception{std::format(std::forward<Args>(args)...)};
40 }
41 }
42
43 return std::forward<T>(result);
44}
45
46C4HTTPClient::Uri::String::~String()
47{
48 if (ptr)
49 {
50 curl_free(p: ptr);
51 }
52}
53
54C4HTTPClient::Uri::String::String(String &&other) noexcept
55 : ptr{std::exchange(obj&: other.ptr, new_val: nullptr)}
56{
57}
58
59C4HTTPClient::Uri::String &C4HTTPClient::Uri::String::operator=(String &&other) noexcept
60{
61 ptr = std::exchange(obj&: other.ptr, new_val: nullptr);
62 return *this;
63}
64
65void C4HTTPClient::CURLSDeleter::operator()(CURLS *const share)
66{
67 curl_share_cleanup(share);
68}
69
70void C4HTTPClient::Uri::CURLUDeleter::operator()(CURLU * const uri)
71{
72 if (uri)
73 {
74 curl_url_cleanup(handle: uri);
75 }
76}
77
78C4HTTPClient::Uri::Uri(const std::string &serverAddress, const std::uint16_t port)
79 : uri{ThrowIfFailed(result: curl_url(), args: "curl_url failed")}
80{
81 ThrowIfFailed(result: curl_url_set(handle: uri.get(), what: CURLUPART_URL, part: serverAddress.c_str(), flags: 0) == CURLUE_OK, args: "malformed URL");
82
83 if (port > 0)
84 {
85 SetPort(port);
86 }
87}
88
89C4HTTPClient::Uri::Uri(std::unique_ptr<CURLU, CURLUDeleter> uri, const std::uint16_t port)
90 : uri{std::move(uri)}
91{
92 if (port > 0)
93 {
94 SetPort(port);
95 }
96}
97
98C4HTTPClient::Uri::String C4HTTPClient::Uri::GetPart(const Part part) const
99{
100 String result;
101 ThrowIfFailed(result: curl_url_get(handle: uri.get(), what: static_cast<CURLUPart>(part), part: &result, flags: 0) == CURLUE_OK, args: "curl_url_get failed");
102 return result;
103}
104
105C4HTTPClient::Uri C4HTTPClient::Uri::ParseOldStyle(const std::string &serverAddress, const std::uint16_t port)
106{
107 std::unique_ptr<CURLU, CURLUDeleter> uri{ThrowIfFailed(result: curl_url(), args: "curl_url failed")};
108
109 CURLUcode result{curl_url_set(handle: uri.get(), what: CURLUPART_URL, part: serverAddress.c_str(), flags: 0)};
110
111 if (result == CURLUE_BAD_SCHEME || result == CURLUE_UNSUPPORTED_SCHEME)
112 {
113 result = curl_url_set(handle: uri.get(), what: CURLUPART_URL, part: std::format(fmt: "http://{}", args: serverAddress).c_str(), flags: 0);
114 }
115
116 ThrowIfFailed(result: result == CURLUE_OK, args: "malformed URL");
117
118 return {std::move(uri), port};
119}
120
121void C4HTTPClient::Uri::SetPort(const std::uint16_t port)
122{
123 String portString;
124 const auto errGetPort = curl_url_get(handle: uri.get(), what: CURLUPART_PORT, part: &portString, flags: 0);
125
126 if (errGetPort == CURLUE_NO_PORT)
127 {
128 ThrowIfFailed(result: curl_url_set(handle: uri.get(), what: CURLUPART_PORT, part: std::to_string(val: port).c_str(), flags: 0) == CURLUE_OK, args: "curl_url_set PORT failed");
129 }
130 else
131 {
132 ThrowIfFailed(result: errGetPort == CURLUE_OK, args: "curl_url_get PORT failed");
133 }
134}
135
136C4HTTPClient::C4HTTPClient()
137 : shareHandle{ThrowIfFailed(result: curl_share_init(), args: "curl_share_init failed")}
138{
139 curl_share_setopt(shareHandle.get(), CURLSHOPT_SHARE, CURL_LOCK_DATA_COOKIE);
140 curl_share_setopt(shareHandle.get(), CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION);
141 curl_share_setopt(shareHandle.get(), CURLSHOPT_LOCKFUNC, &C4HTTPClient::LockFunction);
142 curl_share_setopt(shareHandle.get(), CURLSHOPT_UNLOCKFUNC, &C4HTTPClient::UnlockFunction);
143 curl_share_setopt(shareHandle.get(), CURLSHOPT_USERDATA, &shareMutexes);
144}
145
146C4Task::Hot<C4HTTPClient::Result> C4HTTPClient::GetAsync(Request request, ProgressCallback &&progressCallback, Headers headers)
147{
148 return RequestAsync(request: std::move(request), progressCallback: std::move(progressCallback), headers: std::move(headers), post: false);
149}
150
151C4Task::Hot<C4HTTPClient::Result> C4HTTPClient::PostAsync(Request request, ProgressCallback &&progressCallback, Headers headers)
152{
153 return RequestAsync(request: std::move(request), progressCallback: std::move(progressCallback), headers: std::move(headers), post: true);
154}
155
156C4Task::Hot<C4HTTPClient::Result> C4HTTPClient::RequestAsync(Request request, const ProgressCallback progressCallback, Headers headers, const bool post)
157{
158 auto curl = PrepareRequest(request, headers: std::move(headers), post);
159
160 StdBuf result;
161
162 curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, &C4HTTPClient::WriteFunction);
163 curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &result);
164
165 if (progressCallback)
166 {
167 curl_easy_setopt(curl.get(), CURLOPT_NOPROGRESS, 0L);
168 curl_easy_setopt(curl.get(), CURLOPT_XFERINFOFUNCTION, &C4HTTPClient::XferInfoFunction);
169 curl_easy_setopt(curl.get(), CURLOPT_XFERINFODATA, &progressCallback);
170 }
171
172 try
173 {
174 auto awaitResult = co_await Application.CurlSystem->WaitForEasyAsync(easyHandle: std::move(curl));
175 co_return {.Buffer: std::move(result), .ServerAddress: std::move(awaitResult)};
176 }
177 catch (const C4CurlSystem::Exception &e)
178 {
179 throw Exception{e.what()};
180 }
181}
182
183C4CurlSystem::EasyHandle C4HTTPClient::PrepareRequest(const Request &request, Headers headers, const bool post)
184{
185 C4CurlSystem::EasyHandle curl{ThrowIfFailed(result: curl_easy_init(), args: "curl_easy_init failed")};
186
187 curl_easy_setopt(curl.get(), CURLOPT_CURLU, request.Uri.get());
188 curl_easy_setopt(curl.get(), CURLOPT_ACCEPT_ENCODING, "gzip");
189 curl_easy_setopt(curl.get(), CURLOPT_USERAGENT, C4ENGINENAME "/" C4VERSION );
190 curl_easy_setopt(curl.get(), CURLOPT_TIMEOUT, C4HTTPQueryTimeout);
191 curl_easy_setopt(curl.get(), CURLOPT_FAILONERROR, 1L);
192 curl_easy_setopt(curl.get(), CURLOPT_COOKIEFILE, "");
193 curl_easy_setopt(curl.get(), CURLOPT_SHARE, shareHandle.get());
194 curl_easy_setopt(curl.get(), CURLOPT_FOLLOWLOCATION, 1L);
195
196 const char *const charset{C4Config::GetCharsetCodeName(charset: Config.General.LanguageCharset)};
197
198 headers["Accept-Charset"] = charset;
199 headers["Accept-Language"] = Config.General.LanguageEx;
200 headers.erase(x: "User-Agent");
201
202 curl_slist *headerList{nullptr};
203
204 if (post)
205 {
206 curl_easy_setopt(curl.get(), CURLOPT_POST, 1L);
207 curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDS, request.Data.data());
208 curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDSIZE, static_cast<long>(request.Data.size()));
209
210 if (!headers.count(x: "Content-Type"))
211 {
212 static constexpr auto defaultType = "text/plain; encoding=";
213 headerList = curl_slist_append(list: headerList, data: std::format(fmt: "Content-Type: {}{}", args: defaultType, args: charset).c_str());
214 }
215
216 // Disable the Expect: 100-Continue header which curl automaticallY
217 // adds for POST requests.
218 headerList = curl_slist_append(list: headerList, data: "Expect:");
219 }
220
221 for (const auto &[key, value] : headers)
222 {
223 std::string header{key};
224 header += ": ";
225 header += value;
226 headerList = curl_slist_append(list: headerList, data: header.c_str());
227 }
228
229 curl_easy_setopt(curl.get(), CURLOPT_HTTPHEADER, headerList);
230
231 return curl;
232}
233
234std::size_t C4HTTPClient::WriteFunction(char *const ptr, std::size_t, const std::size_t nmemb, void *const userData)
235{
236 auto &buf = *reinterpret_cast<StdBuf *>(userData);
237
238 const std::size_t oldSize{buf.getSize()};
239 buf.Append(pnData: ptr, inSize: nmemb);
240 return buf.getSize() - oldSize;
241}
242
243int C4HTTPClient::XferInfoFunction(void *const userData, const std::int64_t downloadTotal, const std::int64_t downloadNow, std::int64_t, std::int64_t)
244{
245 const auto &callback = *reinterpret_cast<ProgressCallback *>(userData);
246 if (callback && !callback(downloadTotal, downloadNow))
247 {
248 return 1;
249 }
250
251 return 0;
252}
253
254void C4HTTPClient::LockFunction(CURL *, const int data, const int access, void *const userData)
255{
256 static_assert(sizeof(curl_lock_data) == sizeof(int) && alignof(curl_lock_data) == alignof(int));
257 static_assert(sizeof(curl_lock_access) == sizeof(int) && alignof(curl_lock_access) == alignof(int));
258 static_assert(std::tuple_size_v<decltype(C4HTTPClient::shareMutexes)> == CURL_LOCK_DATA_LAST + 1, "Invalid mutex array size");
259
260 auto &[mutex, exclusive] = (*reinterpret_cast<decltype(C4HTTPClient::shareMutexes) *>(userData))[data];
261
262 switch (static_cast<curl_lock_access>(access))
263 {
264 case CURL_LOCK_ACCESS_SHARED:
265 mutex.lock_shared();
266 exclusive = false;
267 break;
268
269 case CURL_LOCK_ACCESS_SINGLE:
270 mutex.lock();
271 exclusive = true;
272 break;
273
274 default:
275 assert(false);
276 std::unreachable();
277 }
278}
279
280void C4HTTPClient::UnlockFunction(CURL *, const int data, void *const userData)
281{
282 auto &[mutex, exclusive] = (*reinterpret_cast<decltype(C4HTTPClient::shareMutexes) *>(userData))[data];
283
284 if (exclusive)
285 {
286 mutex.unlock();
287 }
288 else
289 {
290 mutex.unlock_shared();
291 }
292}
293