1 | #include "http.h" |
2 | |
3 | #include <base/log.h> |
4 | #include <base/math.h> |
5 | #include <base/system.h> |
6 | #include <engine/external/json-parser/json.h> |
7 | #include <engine/shared/config.h> |
8 | #include <engine/storage.h> |
9 | #include <game/version.h> |
10 | |
11 | #include <limits> |
12 | #include <thread> |
13 | |
14 | #if !defined(CONF_FAMILY_WINDOWS) |
15 | #include <csignal> |
16 | #endif |
17 | |
18 | #include <curl/curl.h> |
19 | |
20 | // There is a stray constant on Windows/MSVC... |
21 | #ifdef ERROR |
22 | #undef ERROR |
23 | #endif |
24 | |
25 | int CurlDebug(CURL *pHandle, curl_infotype Type, char *pData, size_t DataSize, void *pUser) |
26 | { |
27 | char TypeChar; |
28 | switch(Type) |
29 | { |
30 | case CURLINFO_TEXT: |
31 | TypeChar = '*'; |
32 | break; |
33 | case CURLINFO_HEADER_OUT: |
34 | TypeChar = '<'; |
35 | break; |
36 | case CURLINFO_HEADER_IN: |
37 | TypeChar = '>'; |
38 | break; |
39 | default: |
40 | return 0; |
41 | } |
42 | while(const char *pLineEnd = (const char *)memchr(s: pData, c: '\n', n: DataSize)) |
43 | { |
44 | int LineLength = pLineEnd - pData; |
45 | log_debug("curl" , "%c %.*s" , TypeChar, LineLength, pData); |
46 | pData += LineLength + 1; |
47 | DataSize -= LineLength + 1; |
48 | } |
49 | return 0; |
50 | } |
51 | |
52 | void EscapeUrl(char *pBuf, int Size, const char *pStr) |
53 | { |
54 | char *pEsc = curl_easy_escape(handle: 0, string: pStr, length: 0); |
55 | str_copy(dst: pBuf, src: pEsc, dst_size: Size); |
56 | curl_free(p: pEsc); |
57 | } |
58 | |
59 | bool HttpHasIpresolveBug() |
60 | { |
61 | // curl < 7.77.0 doesn't use CURLOPT_IPRESOLVE correctly wrt. |
62 | // connection caches. |
63 | return curl_version_info(CURLVERSION_NOW)->version_num < 0x074d00; |
64 | } |
65 | |
66 | CHttpRequest::CHttpRequest(const char *pUrl) |
67 | { |
68 | str_copy(dst&: m_aUrl, src: pUrl); |
69 | sha256_init(ctxt: &m_ActualSha256Ctx); |
70 | } |
71 | |
72 | CHttpRequest::~CHttpRequest() |
73 | { |
74 | dbg_assert(m_File == nullptr, "HTTP request file was not closed" ); |
75 | free(ptr: m_pBuffer); |
76 | curl_slist_free_all(list: (curl_slist *)m_pHeaders); |
77 | free(ptr: m_pBody); |
78 | } |
79 | |
80 | bool CHttpRequest::BeforeInit() |
81 | { |
82 | if(m_WriteToFile) |
83 | { |
84 | if(fs_makedir_rec_for(path: m_aDestAbsolute) < 0) |
85 | { |
86 | log_error("http" , "i/o error, cannot create folder for: %s" , m_aDest); |
87 | return false; |
88 | } |
89 | |
90 | m_File = io_open(filename: m_aDestAbsolute, flags: IOFLAG_WRITE); |
91 | if(!m_File) |
92 | { |
93 | log_error("http" , "i/o error, cannot open file: %s" , m_aDest); |
94 | return false; |
95 | } |
96 | } |
97 | return true; |
98 | } |
99 | |
100 | bool CHttpRequest::ConfigureHandle(void *pHandle) |
101 | { |
102 | CURL *pH = (CURL *)pHandle; |
103 | if(!BeforeInit()) |
104 | { |
105 | return false; |
106 | } |
107 | |
108 | if(g_Config.m_DbgCurl) |
109 | { |
110 | curl_easy_setopt(pH, CURLOPT_VERBOSE, 1L); |
111 | curl_easy_setopt(pH, CURLOPT_DEBUGFUNCTION, CurlDebug); |
112 | } |
113 | long Protocols = CURLPROTO_HTTPS; |
114 | if(g_Config.m_HttpAllowInsecure) |
115 | { |
116 | Protocols |= CURLPROTO_HTTP; |
117 | } |
118 | |
119 | curl_easy_setopt(pH, CURLOPT_ERRORBUFFER, m_aErr); |
120 | |
121 | curl_easy_setopt(pH, CURLOPT_CONNECTTIMEOUT_MS, m_Timeout.ConnectTimeoutMs); |
122 | curl_easy_setopt(pH, CURLOPT_TIMEOUT_MS, m_Timeout.TimeoutMs); |
123 | curl_easy_setopt(pH, CURLOPT_LOW_SPEED_LIMIT, m_Timeout.LowSpeedLimit); |
124 | curl_easy_setopt(pH, CURLOPT_LOW_SPEED_TIME, m_Timeout.LowSpeedTime); |
125 | if(m_MaxResponseSize >= 0) |
126 | { |
127 | curl_easy_setopt(pH, CURLOPT_MAXFILESIZE_LARGE, (curl_off_t)m_MaxResponseSize); |
128 | } |
129 | |
130 | // ‘CURLOPT_PROTOCOLS’ is deprecated: since 7.85.0. Use CURLOPT_PROTOCOLS_STR |
131 | // Wait until all platforms have 7.85.0 |
132 | #ifdef __GNUC__ |
133 | #pragma GCC diagnostic push |
134 | #pragma GCC diagnostic ignored "-Wdeprecated-declarations" |
135 | #endif |
136 | curl_easy_setopt(pH, CURLOPT_PROTOCOLS, Protocols); |
137 | #ifdef __GNUC__ |
138 | #pragma GCC diagnostic pop |
139 | #endif |
140 | curl_easy_setopt(pH, CURLOPT_FOLLOWLOCATION, 1L); |
141 | curl_easy_setopt(pH, CURLOPT_MAXREDIRS, 4L); |
142 | if(m_FailOnErrorStatus) |
143 | { |
144 | curl_easy_setopt(pH, CURLOPT_FAILONERROR, 1L); |
145 | } |
146 | curl_easy_setopt(pH, CURLOPT_URL, m_aUrl); |
147 | curl_easy_setopt(pH, CURLOPT_NOSIGNAL, 1L); |
148 | curl_easy_setopt(pH, CURLOPT_USERAGENT, GAME_NAME " " GAME_RELEASE_VERSION " (" CONF_PLATFORM_STRING "; " CONF_ARCH_STRING ")" ); |
149 | curl_easy_setopt(pH, CURLOPT_ACCEPT_ENCODING, "" ); // Use any compression algorithm supported by libcurl. |
150 | |
151 | curl_easy_setopt(pH, CURLOPT_HEADERDATA, this); |
152 | curl_easy_setopt(pH, CURLOPT_HEADERFUNCTION, HeaderCallback); |
153 | curl_easy_setopt(pH, CURLOPT_WRITEDATA, this); |
154 | curl_easy_setopt(pH, CURLOPT_WRITEFUNCTION, WriteCallback); |
155 | curl_easy_setopt(pH, CURLOPT_NOPROGRESS, 0L); |
156 | curl_easy_setopt(pH, CURLOPT_PROGRESSDATA, this); |
157 | // ‘CURLOPT_PROGRESSFUNCTION’ is deprecated: since 7.32.0. Use CURLOPT_XFERINFOFUNCTION |
158 | // See problems with curl_off_t type in header file in https://github.com/ddnet/ddnet/pull/6185/ |
159 | #ifdef __GNUC__ |
160 | #pragma GCC diagnostic push |
161 | #pragma GCC diagnostic ignored "-Wdeprecated-declarations" |
162 | #endif |
163 | curl_easy_setopt(pH, CURLOPT_PROGRESSFUNCTION, ProgressCallback); |
164 | #ifdef __GNUC__ |
165 | #pragma GCC diagnostic pop |
166 | #endif |
167 | curl_easy_setopt(pH, CURLOPT_IPRESOLVE, m_IpResolve == IPRESOLVE::V4 ? CURL_IPRESOLVE_V4 : m_IpResolve == IPRESOLVE::V6 ? CURL_IPRESOLVE_V6 : CURL_IPRESOLVE_WHATEVER); |
168 | if(g_Config.m_Bindaddr[0] != '\0') |
169 | { |
170 | curl_easy_setopt(pH, CURLOPT_INTERFACE, g_Config.m_Bindaddr); |
171 | } |
172 | |
173 | if(curl_version_info(CURLVERSION_NOW)->version_num < 0x074400) |
174 | { |
175 | // Causes crashes, see https://github.com/ddnet/ddnet/issues/4342. |
176 | // No longer a problem in curl 7.68 and above, and 0x44 = 68. |
177 | curl_easy_setopt(pH, CURLOPT_FORBID_REUSE, 1L); |
178 | } |
179 | |
180 | #ifdef CONF_PLATFORM_ANDROID |
181 | curl_easy_setopt(pH, CURLOPT_CAINFO, "data/cacert.pem" ); |
182 | #endif |
183 | |
184 | switch(m_Type) |
185 | { |
186 | case REQUEST::GET: |
187 | break; |
188 | case REQUEST::HEAD: |
189 | curl_easy_setopt(pH, CURLOPT_NOBODY, 1L); |
190 | break; |
191 | case REQUEST::POST: |
192 | case REQUEST::POST_JSON: |
193 | if(m_Type == REQUEST::POST_JSON) |
194 | { |
195 | Header(pNameColonValue: "Content-Type: application/json" ); |
196 | } |
197 | else |
198 | { |
199 | Header(pNameColonValue: "Content-Type:" ); |
200 | } |
201 | curl_easy_setopt(pH, CURLOPT_POSTFIELDS, m_pBody); |
202 | curl_easy_setopt(pH, CURLOPT_POSTFIELDSIZE, m_BodyLength); |
203 | break; |
204 | } |
205 | |
206 | curl_easy_setopt(pH, CURLOPT_HTTPHEADER, m_pHeaders); |
207 | |
208 | return true; |
209 | } |
210 | |
211 | size_t CHttpRequest::(char *, size_t ) |
212 | { |
213 | // `pHeader` is NOT null-terminated. |
214 | // `pHeader` has a trailing newline. |
215 | |
216 | if(HeaderSize <= 1) |
217 | { |
218 | m_HeadersEnded = true; |
219 | return HeaderSize; |
220 | } |
221 | if(m_HeadersEnded) |
222 | { |
223 | // redirect, clear old headers |
224 | m_HeadersEnded = false; |
225 | m_ResultDate = {}; |
226 | m_ResultLastModified = {}; |
227 | } |
228 | |
229 | static const char DATE[] = "Date: " ; |
230 | static const char LAST_MODIFIED[] = "Last-Modified: " ; |
231 | |
232 | // Trailing newline and null termination evens out. |
233 | if(HeaderSize - 1 >= sizeof(DATE) - 1 && str_startswith_nocase(str: pHeader, prefix: DATE)) |
234 | { |
235 | char aValue[128]; |
236 | str_truncate(dst: aValue, dst_size: sizeof(aValue), src: pHeader + (sizeof(DATE) - 1), truncation_len: HeaderSize - (sizeof(DATE) - 1) - 1); |
237 | int64_t Value = curl_getdate(p: aValue, unused: nullptr); |
238 | if(Value != -1) |
239 | { |
240 | m_ResultDate = Value; |
241 | } |
242 | } |
243 | if(HeaderSize - 1 >= sizeof(LAST_MODIFIED) - 1 && str_startswith_nocase(str: pHeader, prefix: LAST_MODIFIED)) |
244 | { |
245 | char aValue[128]; |
246 | str_truncate(dst: aValue, dst_size: sizeof(aValue), src: pHeader + (sizeof(LAST_MODIFIED) - 1), truncation_len: HeaderSize - (sizeof(LAST_MODIFIED) - 1) - 1); |
247 | int64_t Value = curl_getdate(p: aValue, unused: nullptr); |
248 | if(Value != -1) |
249 | { |
250 | m_ResultLastModified = Value; |
251 | } |
252 | } |
253 | |
254 | return HeaderSize; |
255 | } |
256 | |
257 | size_t CHttpRequest::OnData(char *pData, size_t DataSize) |
258 | { |
259 | // Need to check for the maximum response size here as curl can only |
260 | // guarantee it if the server sets a Content-Length header. |
261 | if(m_MaxResponseSize >= 0 && m_ResponseLength + DataSize > (uint64_t)m_MaxResponseSize) |
262 | { |
263 | return 0; |
264 | } |
265 | |
266 | sha256_update(ctxt: &m_ActualSha256Ctx, data: pData, data_len: DataSize); |
267 | |
268 | if(!m_WriteToFile) |
269 | { |
270 | if(DataSize == 0) |
271 | { |
272 | return DataSize; |
273 | } |
274 | size_t NewBufferSize = maximum(a: (size_t)1024, b: m_BufferSize); |
275 | while(m_ResponseLength + DataSize > NewBufferSize) |
276 | { |
277 | NewBufferSize *= 2; |
278 | } |
279 | if(NewBufferSize != m_BufferSize) |
280 | { |
281 | m_pBuffer = (unsigned char *)realloc(ptr: m_pBuffer, size: NewBufferSize); |
282 | m_BufferSize = NewBufferSize; |
283 | } |
284 | mem_copy(dest: m_pBuffer + m_ResponseLength, source: pData, size: DataSize); |
285 | m_ResponseLength += DataSize; |
286 | return DataSize; |
287 | } |
288 | else |
289 | { |
290 | m_ResponseLength += DataSize; |
291 | return io_write(io: m_File, buffer: pData, size: DataSize); |
292 | } |
293 | } |
294 | |
295 | size_t CHttpRequest::(char *pData, size_t Size, size_t Number, void *pUser) |
296 | { |
297 | dbg_assert(Size == 1, "invalid size parameter passed to header callback" ); |
298 | return ((CHttpRequest *)pUser)->OnHeader(pHeader: pData, HeaderSize: Number); |
299 | } |
300 | |
301 | size_t CHttpRequest::WriteCallback(char *pData, size_t Size, size_t Number, void *pUser) |
302 | { |
303 | return ((CHttpRequest *)pUser)->OnData(pData, DataSize: Size * Number); |
304 | } |
305 | |
306 | int CHttpRequest::ProgressCallback(void *pUser, double DlTotal, double DlCurr, double UlTotal, double UlCurr) |
307 | { |
308 | CHttpRequest *pTask = (CHttpRequest *)pUser; |
309 | pTask->m_Current.store(i: DlCurr, m: std::memory_order_relaxed); |
310 | pTask->m_Size.store(i: DlTotal, m: std::memory_order_relaxed); |
311 | pTask->m_Progress.store(i: DlTotal == 0.0 ? 0 : (100 * DlCurr) / DlTotal, m: std::memory_order_relaxed); |
312 | pTask->OnProgress(); |
313 | return pTask->m_Abort ? -1 : 0; |
314 | } |
315 | |
316 | void CHttpRequest::OnCompletionInternal(void *pHandle, unsigned int Result) |
317 | { |
318 | if(pHandle) |
319 | { |
320 | CURL *pH = (CURL *)pHandle; |
321 | long StatusCode; |
322 | curl_easy_getinfo(pH, CURLINFO_RESPONSE_CODE, &StatusCode); |
323 | m_StatusCode = StatusCode; |
324 | } |
325 | |
326 | EHttpState State; |
327 | const CURLcode Code = static_cast<CURLcode>(Result); |
328 | if(Code != CURLE_OK) |
329 | { |
330 | if(g_Config.m_DbgCurl || m_LogProgress >= HTTPLOG::FAILURE) |
331 | { |
332 | log_error("http" , "%s failed. libcurl error (%u): %s" , m_aUrl, Code, m_aErr); |
333 | } |
334 | State = (Code == CURLE_ABORTED_BY_CALLBACK) ? EHttpState::ABORTED : EHttpState::ERROR; |
335 | } |
336 | else |
337 | { |
338 | if(g_Config.m_DbgCurl || m_LogProgress >= HTTPLOG::ALL) |
339 | { |
340 | log_info("http" , "task done: %s" , m_aUrl); |
341 | } |
342 | State = EHttpState::DONE; |
343 | } |
344 | |
345 | if(State == EHttpState::DONE) |
346 | { |
347 | m_ActualSha256 = sha256_finish(ctxt: &m_ActualSha256Ctx); |
348 | if(m_ExpectedSha256 != SHA256_ZEROED && m_ActualSha256 != m_ExpectedSha256) |
349 | { |
350 | if(g_Config.m_DbgCurl || m_LogProgress >= HTTPLOG::FAILURE) |
351 | { |
352 | char aActualSha256[SHA256_MAXSTRSIZE]; |
353 | sha256_str(digest: m_ActualSha256, str: aActualSha256, max_len: sizeof(aActualSha256)); |
354 | char aExpectedSha256[SHA256_MAXSTRSIZE]; |
355 | sha256_str(digest: m_ExpectedSha256, str: aExpectedSha256, max_len: sizeof(aExpectedSha256)); |
356 | log_error("http" , "SHA256 mismatch: got=%s, expected=%s, url=%s" , aActualSha256, aExpectedSha256, m_aUrl); |
357 | } |
358 | State = EHttpState::ERROR; |
359 | } |
360 | } |
361 | |
362 | if(m_WriteToFile) |
363 | { |
364 | if(m_File && io_close(io: m_File) != 0) |
365 | { |
366 | log_error("http" , "i/o error, cannot close file: %s" , m_aDest); |
367 | State = EHttpState::ERROR; |
368 | } |
369 | m_File = nullptr; |
370 | |
371 | if(State == EHttpState::ERROR || State == EHttpState::ABORTED) |
372 | { |
373 | fs_remove(filename: m_aDestAbsolute); |
374 | } |
375 | } |
376 | |
377 | // The globally visible state must be updated after OnCompletion has finished, |
378 | // or other threads may try to access the result of a completed HTTP request, |
379 | // before the result has been initialized/updated in OnCompletion. |
380 | OnCompletion(State); |
381 | m_State = State; |
382 | } |
383 | |
384 | void CHttpRequest::WriteToFile(IStorage *pStorage, const char *pDest, int StorageType) |
385 | { |
386 | m_WriteToFile = true; |
387 | str_copy(dst&: m_aDest, src: pDest); |
388 | if(StorageType == -2) |
389 | { |
390 | pStorage->GetBinaryPath(pFilename: m_aDest, pBuffer: m_aDestAbsolute, BufferSize: sizeof(m_aDestAbsolute)); |
391 | } |
392 | else |
393 | { |
394 | pStorage->GetCompletePath(Type: StorageType, pDir: m_aDest, pBuffer: m_aDestAbsolute, BufferSize: sizeof(m_aDestAbsolute)); |
395 | } |
396 | } |
397 | |
398 | void CHttpRequest::(const char *pNameColonValue) |
399 | { |
400 | m_pHeaders = curl_slist_append(list: (curl_slist *)m_pHeaders, data: pNameColonValue); |
401 | } |
402 | |
403 | void CHttpRequest::Wait() |
404 | { |
405 | using namespace std::chrono_literals; |
406 | |
407 | // This is so uncommon that polling just might work |
408 | for(;;) |
409 | { |
410 | EHttpState State = m_State.load(m: std::memory_order_seq_cst); |
411 | if(State != EHttpState::QUEUED && State != EHttpState::RUNNING) |
412 | { |
413 | return; |
414 | } |
415 | std::this_thread::sleep_for(rtime: 10ms); |
416 | } |
417 | } |
418 | |
419 | void CHttpRequest::Result(unsigned char **ppResult, size_t *pResultLength) const |
420 | { |
421 | dbg_assert(State() == EHttpState::DONE, "Request not done" ); |
422 | dbg_assert(!m_WriteToFile, "Result not usable together with WriteToFile" ); |
423 | *ppResult = m_pBuffer; |
424 | *pResultLength = m_ResponseLength; |
425 | } |
426 | |
427 | json_value *CHttpRequest::ResultJson() const |
428 | { |
429 | unsigned char *pResult; |
430 | size_t ResultLength; |
431 | Result(ppResult: &pResult, pResultLength: &ResultLength); |
432 | return json_parse(json: (char *)pResult, length: ResultLength); |
433 | } |
434 | |
435 | const SHA256_DIGEST &CHttpRequest::ResultSha256() const |
436 | { |
437 | dbg_assert(State() == EHttpState::DONE, "Request not done" ); |
438 | return m_ActualSha256; |
439 | } |
440 | |
441 | int CHttpRequest::StatusCode() const |
442 | { |
443 | dbg_assert(State() == EHttpState::DONE, "Request not done" ); |
444 | return m_StatusCode; |
445 | } |
446 | |
447 | std::optional<int64_t> CHttpRequest::ResultAgeSeconds() const |
448 | { |
449 | dbg_assert(State() == EHttpState::DONE, "Request not done" ); |
450 | if(!m_ResultDate || !m_ResultLastModified) |
451 | { |
452 | return {}; |
453 | } |
454 | return *m_ResultDate - *m_ResultLastModified; |
455 | } |
456 | |
457 | std::optional<int64_t> CHttpRequest::ResultLastModified() const |
458 | { |
459 | dbg_assert(State() == EHttpState::DONE, "Request not done" ); |
460 | return m_ResultLastModified; |
461 | } |
462 | |
463 | bool CHttp::Init(std::chrono::milliseconds ShutdownDelay) |
464 | { |
465 | m_ShutdownDelay = ShutdownDelay; |
466 | |
467 | #if !defined(CONF_FAMILY_WINDOWS) |
468 | // As a multithreaded application we have to tell curl to not install signal |
469 | // handlers and instead ignore SIGPIPE from OpenSSL ourselves. |
470 | signal(SIGPIPE, SIG_IGN); |
471 | #endif |
472 | m_pThread = thread_init(threadfunc: CHttp::ThreadMain, user: this, name: "http" ); |
473 | |
474 | std::unique_lock Lock(m_Lock); |
475 | m_Cv.wait(lock&: Lock, p: [this]() { return m_State != CHttp::UNINITIALIZED; }); |
476 | if(m_State != CHttp::RUNNING) |
477 | { |
478 | return false; |
479 | } |
480 | |
481 | return true; |
482 | } |
483 | |
484 | void CHttp::ThreadMain(void *pUser) |
485 | { |
486 | CHttp *pHttp = static_cast<CHttp *>(pUser); |
487 | pHttp->RunLoop(); |
488 | } |
489 | |
490 | void CHttp::RunLoop() |
491 | { |
492 | std::unique_lock Lock(m_Lock); |
493 | if(curl_global_init(CURL_GLOBAL_DEFAULT)) |
494 | { |
495 | log_error("http" , "curl_global_init failed" ); |
496 | m_State = CHttp::ERROR; |
497 | m_Cv.notify_all(); |
498 | return; |
499 | } |
500 | |
501 | m_pMultiH = curl_multi_init(); |
502 | if(!m_pMultiH) |
503 | { |
504 | log_error("http" , "curl_multi_init failed" ); |
505 | m_State = CHttp::ERROR; |
506 | m_Cv.notify_all(); |
507 | return; |
508 | } |
509 | |
510 | // print curl version |
511 | { |
512 | curl_version_info_data *pVersion = curl_version_info(CURLVERSION_NOW); |
513 | log_info("http" , "libcurl version %s (compiled = " LIBCURL_VERSION ")" , pVersion->version); |
514 | } |
515 | |
516 | m_State = CHttp::RUNNING; |
517 | m_Cv.notify_all(); |
518 | Lock.unlock(); |
519 | |
520 | while(m_State == CHttp::RUNNING) |
521 | { |
522 | static int s_NextTimeout = std::numeric_limits<int>::max(); |
523 | int Events = 0; |
524 | const CURLMcode PollCode = curl_multi_poll(multi_handle: m_pMultiH, extra_fds: nullptr, extra_nfds: 0, timeout_ms: s_NextTimeout, ret: &Events); |
525 | |
526 | // We may have been woken up for a shutdown |
527 | if(m_Shutdown) |
528 | { |
529 | auto Now = std::chrono::steady_clock::now(); |
530 | if(!m_ShutdownTime.has_value()) |
531 | { |
532 | m_ShutdownTime = Now + m_ShutdownDelay; |
533 | s_NextTimeout = m_ShutdownDelay.count(); |
534 | } |
535 | else if(m_ShutdownTime < Now || m_RunningRequests.empty()) |
536 | { |
537 | break; |
538 | } |
539 | } |
540 | |
541 | if(PollCode != CURLM_OK) |
542 | { |
543 | Lock.lock(); |
544 | log_error("http" , "curl_multi_poll failed: %s" , curl_multi_strerror(PollCode)); |
545 | m_State = CHttp::ERROR; |
546 | break; |
547 | } |
548 | |
549 | const CURLMcode PerformCode = curl_multi_perform(multi_handle: m_pMultiH, running_handles: &Events); |
550 | if(PerformCode != CURLM_OK) |
551 | { |
552 | Lock.lock(); |
553 | log_error("http" , "curl_multi_perform failed: %s" , curl_multi_strerror(PerformCode)); |
554 | m_State = CHttp::ERROR; |
555 | break; |
556 | } |
557 | |
558 | struct CURLMsg *pMsg; |
559 | while((pMsg = curl_multi_info_read(multi_handle: m_pMultiH, msgs_in_queue: &Events))) |
560 | { |
561 | if(pMsg->msg == CURLMSG_DONE) |
562 | { |
563 | auto RequestIt = m_RunningRequests.find(x: pMsg->easy_handle); |
564 | dbg_assert(RequestIt != m_RunningRequests.end(), "Running handle not added to map" ); |
565 | auto pRequest = std::move(RequestIt->second); |
566 | m_RunningRequests.erase(position: RequestIt); |
567 | |
568 | pRequest->OnCompletionInternal(pHandle: pMsg->easy_handle, Result: pMsg->data.result); |
569 | curl_multi_remove_handle(multi_handle: m_pMultiH, curl_handle: pMsg->easy_handle); |
570 | curl_easy_cleanup(curl: pMsg->easy_handle); |
571 | } |
572 | } |
573 | |
574 | decltype(m_PendingRequests) NewRequests = {}; |
575 | Lock.lock(); |
576 | std::swap(x&: m_PendingRequests, y&: NewRequests); |
577 | Lock.unlock(); |
578 | |
579 | while(!NewRequests.empty()) |
580 | { |
581 | auto &pRequest = NewRequests.front(); |
582 | if(g_Config.m_DbgCurl) |
583 | log_debug("http" , "task: %s %s" , CHttpRequest::GetRequestType(pRequest->m_Type), pRequest->m_aUrl); |
584 | |
585 | CURL *pEH = curl_easy_init(); |
586 | if(!pEH) |
587 | { |
588 | log_error("http" , "curl_easy_init failed" ); |
589 | goto error_init; |
590 | } |
591 | |
592 | if(!pRequest->ConfigureHandle(pHandle: pEH)) |
593 | { |
594 | curl_easy_cleanup(curl: pEH); |
595 | str_copy(dst&: pRequest->m_aErr, src: "Failed to initialize request" ); |
596 | pRequest->OnCompletionInternal(pHandle: nullptr, Result: CURLE_ABORTED_BY_CALLBACK); |
597 | NewRequests.pop_front(); |
598 | continue; |
599 | } |
600 | |
601 | if(curl_multi_add_handle(multi_handle: m_pMultiH, curl_handle: pEH) != CURLM_OK) |
602 | { |
603 | log_error("http" , "curl_multi_add_handle failed" ); |
604 | goto error_configure; |
605 | } |
606 | |
607 | m_RunningRequests.emplace(args&: pEH, args: std::move(pRequest)); |
608 | NewRequests.pop_front(); |
609 | continue; |
610 | |
611 | error_configure: |
612 | curl_easy_cleanup(curl: pEH); |
613 | error_init: |
614 | Lock.lock(); |
615 | m_State = CHttp::ERROR; |
616 | break; |
617 | } |
618 | |
619 | // Only happens if m_State == ERROR, thus we already hold the lock |
620 | if(!NewRequests.empty()) |
621 | { |
622 | m_PendingRequests.insert(position: m_PendingRequests.end(), first: std::make_move_iterator(i: NewRequests.begin()), last: std::make_move_iterator(i: NewRequests.end())); |
623 | break; |
624 | } |
625 | } |
626 | |
627 | if(!Lock.owns_lock()) |
628 | Lock.lock(); |
629 | |
630 | bool Cleanup = m_State != CHttp::ERROR; |
631 | for(auto &pRequest : m_PendingRequests) |
632 | { |
633 | str_copy(dst&: pRequest->m_aErr, src: "Shutting down" ); |
634 | pRequest->OnCompletionInternal(pHandle: nullptr, Result: CURLE_ABORTED_BY_CALLBACK); |
635 | } |
636 | |
637 | for(auto &ReqPair : m_RunningRequests) |
638 | { |
639 | auto &[pHandle, pRequest] = ReqPair; |
640 | |
641 | str_copy(dst&: pRequest->m_aErr, src: "Shutting down" ); |
642 | pRequest->OnCompletionInternal(pHandle, Result: CURLE_ABORTED_BY_CALLBACK); |
643 | |
644 | if(Cleanup) |
645 | { |
646 | curl_multi_remove_handle(multi_handle: m_pMultiH, curl_handle: pHandle); |
647 | curl_easy_cleanup(curl: pHandle); |
648 | } |
649 | } |
650 | |
651 | if(Cleanup) |
652 | { |
653 | curl_multi_cleanup(multi_handle: m_pMultiH); |
654 | curl_global_cleanup(); |
655 | } |
656 | } |
657 | |
658 | void CHttp::Run(std::shared_ptr<IHttpRequest> pRequest) |
659 | { |
660 | std::shared_ptr<CHttpRequest> pRequestImpl = std::static_pointer_cast<CHttpRequest>(r: pRequest); |
661 | std::unique_lock Lock(m_Lock); |
662 | if(m_Shutdown || m_State == CHttp::ERROR) |
663 | { |
664 | str_copy(dst&: pRequestImpl->m_aErr, src: "Shutting down" ); |
665 | pRequestImpl->OnCompletionInternal(pHandle: nullptr, Result: CURLE_ABORTED_BY_CALLBACK); |
666 | return; |
667 | } |
668 | m_Cv.wait(lock&: Lock, p: [this]() { return m_State != CHttp::UNINITIALIZED; }); |
669 | m_PendingRequests.emplace_back(args&: pRequestImpl); |
670 | curl_multi_wakeup(multi_handle: m_pMultiH); |
671 | } |
672 | |
673 | void CHttp::Shutdown() |
674 | { |
675 | std::unique_lock Lock(m_Lock); |
676 | if(m_Shutdown || m_State != CHttp::RUNNING) |
677 | return; |
678 | |
679 | m_Shutdown = true; |
680 | curl_multi_wakeup(multi_handle: m_pMultiH); |
681 | } |
682 | |
683 | CHttp::~CHttp() |
684 | { |
685 | if(!m_pThread) |
686 | return; |
687 | |
688 | Shutdown(); |
689 | thread_wait(thread: m_pThread); |
690 | } |
691 | |