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/storage.h>
15
16#include <game/version.h>
17
18#include <limits>
19
20#if !defined(CONF_FAMILY_WINDOWS)
21#include <csignal>
22#endif
23
24#include <curl/curl.h>
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.has_value())
110 {
111 SHA256_DIGEST Sha256;
112 if(CalculateSha256(pAbsoluteFilename: m_aDestAbsolute, pSha256: &Sha256) && Sha256 == m_ExpectedSha256.value())
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_CAPATH, "/system/etc/security/cacerts");
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.has_value() && m_ActualSha256.value() != m_ExpectedSha256.value())
406 {
407 if(g_Config.m_DbgCurl || m_LogProgress >= HTTPLOG::FAILURE)
408 {
409 char aActualSha256[SHA256_MAXSTRSIZE];
410 sha256_str(digest: m_ActualSha256.value(), str: aActualSha256, max_len: sizeof(aActualSha256));
411 char aExpectedSha256[SHA256_MAXSTRSIZE];
412 sha256_str(digest: m_ExpectedSha256.value(), 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 dbg_assert(m_ActualSha256.has_value(), "Result SHA256 missing");
564 return m_ActualSha256.value();
565}
566
567int CHttpRequest::StatusCode() const
568{
569 dbg_assert(State() == EHttpState::DONE, "Request not done");
570 return m_StatusCode;
571}
572
573std::optional<int64_t> CHttpRequest::ResultAgeSeconds() const
574{
575 dbg_assert(State() == EHttpState::DONE, "Request not done");
576 if(!m_ResultDate || !m_ResultLastModified)
577 {
578 return {};
579 }
580 return *m_ResultDate - *m_ResultLastModified;
581}
582
583std::optional<int64_t> CHttpRequest::ResultLastModified() const
584{
585 dbg_assert(State() == EHttpState::DONE, "Request not done");
586 return m_ResultLastModified;
587}
588
589bool CHttp::Init(std::chrono::milliseconds ShutdownDelay)
590{
591 m_ShutdownDelay = ShutdownDelay;
592
593#if !defined(CONF_FAMILY_WINDOWS)
594 // As a multithreaded application we have to tell curl to not install signal
595 // handlers and instead ignore SIGPIPE from OpenSSL ourselves.
596 signal(SIGPIPE, SIG_IGN);
597#endif
598 m_pThread = thread_init(threadfunc: CHttp::ThreadMain, user: this, name: "http");
599
600 std::unique_lock Lock(m_Lock);
601 m_Cv.wait(lock&: Lock, p: [this]() { return m_State != CHttp::UNINITIALIZED; });
602 if(m_State != CHttp::RUNNING)
603 {
604 return false;
605 }
606
607 return true;
608}
609
610void CHttp::ThreadMain(void *pUser)
611{
612 CHttp *pHttp = static_cast<CHttp *>(pUser);
613 pHttp->RunLoop();
614}
615
616void CHttp::RunLoop()
617{
618 std::unique_lock Lock(m_Lock);
619 if(curl_global_init(CURL_GLOBAL_DEFAULT))
620 {
621 log_error("http", "curl_global_init failed");
622 m_State = CHttp::ERROR;
623 m_Cv.notify_all();
624 return;
625 }
626
627 m_pMultiH = curl_multi_init();
628 if(!m_pMultiH)
629 {
630 log_error("http", "curl_multi_init failed");
631 m_State = CHttp::ERROR;
632 m_Cv.notify_all();
633 return;
634 }
635
636 // print curl version
637 {
638 curl_version_info_data *pVersion = curl_version_info(CURLVERSION_NOW);
639 log_info("http", "libcurl version %s (compiled = " LIBCURL_VERSION ")", pVersion->version);
640 }
641
642 m_State = CHttp::RUNNING;
643 m_Cv.notify_all();
644 Lock.unlock();
645
646 while(m_State == CHttp::RUNNING)
647 {
648 static int s_NextTimeout = std::numeric_limits<int>::max();
649 int Events = 0;
650 const CURLMcode PollCode = curl_multi_poll(multi_handle: m_pMultiH, extra_fds: nullptr, extra_nfds: 0, timeout_ms: s_NextTimeout, ret: &Events);
651
652 // We may have been woken up for a shutdown
653 if(m_Shutdown)
654 {
655 if(m_RunningRequests.empty() && m_PendingRequests.empty())
656 break;
657
658 auto Now = std::chrono::steady_clock::now();
659 if(!m_ShutdownTime.has_value())
660 {
661 m_ShutdownTime = Now + m_ShutdownDelay;
662 s_NextTimeout = m_ShutdownDelay.count();
663 }
664 else if(m_ShutdownTime < Now)
665 {
666 break;
667 }
668 }
669
670 if(PollCode != CURLM_OK)
671 {
672 Lock.lock();
673 log_error("http", "curl_multi_poll failed: %s", curl_multi_strerror(PollCode));
674 m_State = CHttp::ERROR;
675 break;
676 }
677
678 const CURLMcode PerformCode = curl_multi_perform(multi_handle: m_pMultiH, running_handles: &Events);
679 if(PerformCode != CURLM_OK)
680 {
681 Lock.lock();
682 log_error("http", "curl_multi_perform failed: %s", curl_multi_strerror(PerformCode));
683 m_State = CHttp::ERROR;
684 break;
685 }
686
687 struct CURLMsg *pMsg;
688 while((pMsg = curl_multi_info_read(multi_handle: m_pMultiH, msgs_in_queue: &Events)))
689 {
690 if(pMsg->msg == CURLMSG_DONE)
691 {
692 auto RequestIt = m_RunningRequests.find(x: pMsg->easy_handle);
693 dbg_assert(RequestIt != m_RunningRequests.end(), "Running handle not added to map");
694 auto pRequest = std::move(RequestIt->second);
695 m_RunningRequests.erase(position: RequestIt);
696
697 pRequest->OnCompletionInternal(pHandle: pMsg->easy_handle, Result: pMsg->data.result);
698 curl_multi_remove_handle(multi_handle: m_pMultiH, curl_handle: pMsg->easy_handle);
699 curl_easy_cleanup(curl: pMsg->easy_handle);
700 }
701 }
702
703 decltype(m_PendingRequests) NewRequests = {};
704 Lock.lock();
705 std::swap(x&: m_PendingRequests, y&: NewRequests);
706 Lock.unlock();
707
708 while(!NewRequests.empty())
709 {
710 auto &pRequest = NewRequests.front();
711 if(g_Config.m_DbgCurl)
712 log_debug("http", "task: %s %s", CHttpRequest::GetRequestType(pRequest->m_Type), pRequest->m_aUrl);
713
714 if(pRequest->ShouldSkipRequest())
715 {
716 pRequest->OnCompletion(State: EHttpState::DONE);
717 {
718 std::unique_lock WaitLock(pRequest->m_WaitMutex);
719 pRequest->m_State = EHttpState::DONE;
720 }
721 pRequest->m_WaitCondition.notify_all();
722 NewRequests.pop_front();
723 continue;
724 }
725
726 CURL *pEH = curl_easy_init();
727 if(!pEH)
728 {
729 log_error("http", "curl_easy_init failed");
730 goto error_init;
731 }
732
733 if(!pRequest->ConfigureHandle(pHandle: pEH))
734 {
735 curl_easy_cleanup(curl: pEH);
736 str_copy(dst&: pRequest->m_aErr, src: "Failed to initialize request");
737 pRequest->OnCompletionInternal(pHandle: nullptr, Result: CURLE_ABORTED_BY_CALLBACK);
738 NewRequests.pop_front();
739 continue;
740 }
741
742 if(curl_multi_add_handle(multi_handle: m_pMultiH, curl_handle: pEH) != CURLM_OK)
743 {
744 log_error("http", "curl_multi_add_handle failed");
745 goto error_configure;
746 }
747
748 {
749 std::unique_lock WaitLock(pRequest->m_WaitMutex);
750 pRequest->m_State = EHttpState::RUNNING;
751 }
752 m_RunningRequests.emplace(args&: pEH, args: std::move(pRequest));
753 NewRequests.pop_front();
754 continue;
755
756 error_configure:
757 curl_easy_cleanup(curl: pEH);
758 error_init:
759 Lock.lock();
760 m_State = CHttp::ERROR;
761 break;
762 }
763
764 // Only happens if m_State == ERROR, thus we already hold the lock
765 if(!NewRequests.empty())
766 {
767 m_PendingRequests.insert(position: m_PendingRequests.end(), first: std::make_move_iterator(i: NewRequests.begin()), last: std::make_move_iterator(i: NewRequests.end()));
768 break;
769 }
770 }
771
772 if(!Lock.owns_lock())
773 Lock.lock();
774
775 bool Cleanup = m_State != CHttp::ERROR;
776 for(auto &pRequest : m_PendingRequests)
777 {
778 str_copy(dst&: pRequest->m_aErr, src: "Shutting down");
779 pRequest->OnCompletionInternal(pHandle: nullptr, Result: CURLE_ABORTED_BY_CALLBACK);
780 }
781
782 for(auto &ReqPair : m_RunningRequests)
783 {
784 auto &[pHandle, pRequest] = ReqPair;
785
786 str_copy(dst&: pRequest->m_aErr, src: "Shutting down");
787 pRequest->OnCompletionInternal(pHandle, Result: CURLE_ABORTED_BY_CALLBACK);
788
789 if(Cleanup)
790 {
791 curl_multi_remove_handle(multi_handle: m_pMultiH, curl_handle: pHandle);
792 curl_easy_cleanup(curl: pHandle);
793 }
794 }
795
796 if(Cleanup)
797 {
798 curl_multi_cleanup(multi_handle: m_pMultiH);
799 curl_global_cleanup();
800 }
801}
802
803void CHttp::Run(std::shared_ptr<IHttpRequest> pRequest)
804{
805 std::shared_ptr<CHttpRequest> pRequestImpl = std::static_pointer_cast<CHttpRequest>(r: pRequest);
806 std::unique_lock Lock(m_Lock);
807 if(m_Shutdown || m_State == CHttp::ERROR)
808 {
809 str_copy(dst&: pRequestImpl->m_aErr, src: "Shutting down");
810 pRequestImpl->OnCompletionInternal(pHandle: nullptr, Result: CURLE_ABORTED_BY_CALLBACK);
811 return;
812 }
813 m_Cv.wait(lock&: Lock, p: [this]() { return m_State != CHttp::UNINITIALIZED; });
814 m_PendingRequests.emplace_back(args&: pRequestImpl);
815 curl_multi_wakeup(multi_handle: m_pMultiH);
816}
817
818void CHttp::Shutdown()
819{
820 std::unique_lock Lock(m_Lock);
821 if(m_Shutdown || m_State != CHttp::RUNNING)
822 return;
823
824 m_Shutdown = true;
825 curl_multi_wakeup(multi_handle: m_pMultiH);
826}
827
828CHttp::~CHttp()
829{
830 if(!m_pThread)
831 return;
832
833 Shutdown();
834 thread_wait(thread: m_pThread);
835}
836