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