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