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