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