| 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 | |
| 26 | static constexpr long C4HTTPQueryTimeout{20}; // (s) |
| 27 | |
| 28 | template<typename T, typename... Args> requires (sizeof...(Args) >= 1) |
| 29 | static 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 | |
| 46 | C4HTTPClient::Uri::String::~String() |
| 47 | { |
| 48 | if (ptr) |
| 49 | { |
| 50 | curl_free(p: ptr); |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | C4HTTPClient::Uri::String::String(String &&other) noexcept |
| 55 | : ptr{std::exchange(obj&: other.ptr, new_val: nullptr)} |
| 56 | { |
| 57 | } |
| 58 | |
| 59 | C4HTTPClient::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 | |
| 65 | void C4HTTPClient::CURLSDeleter::operator()(CURLS *const share) |
| 66 | { |
| 67 | curl_share_cleanup(share); |
| 68 | } |
| 69 | |
| 70 | void C4HTTPClient::Uri::CURLUDeleter::operator()(CURLU * const uri) |
| 71 | { |
| 72 | if (uri) |
| 73 | { |
| 74 | curl_url_cleanup(handle: uri); |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | C4HTTPClient::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 | |
| 89 | C4HTTPClient::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 | |
| 98 | C4HTTPClient::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 | |
| 105 | C4HTTPClient::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 | |
| 121 | void 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 | |
| 136 | C4HTTPClient::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 | |
| 146 | C4Task::Hot<C4HTTPClient::Result> C4HTTPClient::GetAsync(Request request, ProgressCallback &&progressCallback, Headers ) |
| 147 | { |
| 148 | return RequestAsync(request: std::move(request), progressCallback: std::move(progressCallback), headers: std::move(headers), post: false); |
| 149 | } |
| 150 | |
| 151 | C4Task::Hot<C4HTTPClient::Result> C4HTTPClient::PostAsync(Request request, ProgressCallback &&progressCallback, Headers ) |
| 152 | { |
| 153 | return RequestAsync(request: std::move(request), progressCallback: std::move(progressCallback), headers: std::move(headers), post: true); |
| 154 | } |
| 155 | |
| 156 | C4Task::Hot<C4HTTPClient::Result> C4HTTPClient::RequestAsync(Request request, const ProgressCallback progressCallback, 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 | |
| 183 | C4CurlSystem::EasyHandle C4HTTPClient::PrepareRequest(const Request &request, 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 *{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 {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 | |
| 234 | std::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 | |
| 243 | int 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 | |
| 254 | void 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 | |
| 280 | void 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 | |