1#include "http_emscripten.h"
2#if defined(CONF_PLATFORM_EMSCRIPTEN)
3
4#include <base/dbg.h>
5#include <base/log.h>
6#include <base/str.h>
7#include <base/thread.h>
8
9#include <engine/shared/config.h>
10#include <engine/storage.h>
11
12#include <emscripten/emscripten.h>
13#include <emscripten/fetch.h>
14
15#include <limits>
16#include <thread>
17
18CHttpRequestEmscripten::CHttpRequestEmscripten(const char *pUrl) :
19 IHttpRequest(pUrl)
20{
21}
22
23CHttpRequestEmscripten::~CHttpRequestEmscripten()
24{
25 dbg_assert(m_pFetch == nullptr, "HTTP request fetch handle was not closed");
26}
27
28void CHttpRequestEmscripten::Header(const char *pNameColonValue)
29{
30 const char *pColon = str_find(pNameColonValue, ":");
31 dbg_assert(pColon != nullptr, "Header name and value not separated with colon: '%s'", pNameColonValue);
32 std::string Name = std::string(pNameColonValue, pColon - pNameColonValue);
33 std::string Value = std::string(str_skip_whitespaces_const(pColon + 1));
34 m_vRequestHeaders.emplace_back(std::move(Name), std::move(Value));
35}
36
37void CHttpRequestEmscripten::Abort()
38{
39 if(m_Abort)
40 {
41 return;
42 }
43
44 IHttpRequest::Abort();
45
46 if(m_pFetch == nullptr)
47 {
48 return;
49 }
50
51 m_pHttp->AddPendingStateChange(m_pFetch, EHttpState::ABORTED);
52}
53
54EM_JS(void, FormatTimestampJsImpl, (char *pBuf, size_t Size, time_t Timestamp), {
55 const timestampString = new Date(Number(Timestamp) * 1000).toUTCString();
56 stringToUTF8(timestampString, pBuf, Size);
57});
58
59bool CHttpRequestEmscripten::ConfigureAndRun()
60{
61 if(!BeforeInit())
62 {
63 return false;
64 }
65
66 HeaderString("User-Agent", USER_AGENT_STRING);
67
68 if(m_Type == REQUEST::POST_JSON)
69 {
70 Header("Content-Type: application/json");
71 }
72 else if(m_Type == REQUEST::POST)
73 {
74 Header("Content-Type:");
75 }
76
77 if(m_IfModifiedSince >= 0)
78 {
79 char aTimestamp[64];
80 FormatTimestampJsImpl(aTimestamp, sizeof(aTimestamp), m_IfModifiedSince);
81 HeaderString("If-Modified-Since", aTimestamp);
82 }
83
84 std::vector<const char *> vpPackedHeaders;
85 vpPackedHeaders.reserve(2 * m_vRequestHeaders.size() + 1);
86 for(const auto &[Name, Value] : m_vRequestHeaders)
87 {
88 vpPackedHeaders.push_back(Name.c_str());
89 vpPackedHeaders.push_back(Value.c_str());
90 }
91 vpPackedHeaders.push_back(nullptr);
92
93 emscripten_fetch_attr_t FetchAttributes;
94 emscripten_fetch_attr_init(&FetchAttributes);
95 str_copy(FetchAttributes.requestMethod, GetRequestType(m_Type));
96 FetchAttributes.userData = this;
97 FetchAttributes.onsuccess = FetchCallbackSuccess;
98 FetchAttributes.onerror = FetchCallbackFailure;
99 FetchAttributes.onprogress = FetchCallbackProgress;
100 FetchAttributes.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
101 // Using low speed limit/time properties of timeout is not supported.
102 FetchAttributes.timeoutMSecs = m_Timeout.m_ConnectTimeoutMs + m_Timeout.m_TimeoutMs;
103 FetchAttributes.requestHeaders = vpPackedHeaders.data();
104 FetchAttributes.requestData = reinterpret_cast<const char *>(m_pBody);
105 FetchAttributes.requestDataSize = m_BodyLength;
106
107 {
108 std::unique_lock WaitLock(m_WaitMutex);
109 m_State = EHttpState::RUNNING;
110 }
111 m_pFetch = emscripten_fetch(&FetchAttributes, m_aUrl);
112 dbg_assert(m_pFetch != nullptr, "emscripten_fetch failure");
113
114 return true;
115}
116
117void CHttpRequestEmscripten::OnSuccess()
118{
119 dbg_assert(!m_CallbackFinished, "OnSuccess was called multiple times");
120 dbg_assert(m_pFetch != nullptr, "OnSuccess was called with an unset fetch handle");
121 m_CallbackFinished = true;
122
123 OnData(m_pFetch->data, m_pFetch->numBytes);
124
125 m_pHttp->AddPendingStateChange(m_pFetch, EHttpState::DONE);
126}
127
128void CHttpRequestEmscripten::OnFailure()
129{
130 if(m_CallbackFinished || m_pFetch == nullptr)
131 {
132 // Failure callback may be called several times after the fetch handle has already been closed.
133 return;
134 }
135 m_CallbackFinished = true;
136
137 m_pHttp->AddPendingStateChange(m_pFetch, !m_FailOnErrorStatus && m_pFetch->status >= 400 ? EHttpState::DONE : EHttpState::ERROR);
138}
139
140void CHttpRequestEmscripten::OnProgress()
141{
142 m_Current.store(m_pFetch->dataOffset, std::memory_order_relaxed);
143 m_Size.store(m_pFetch->totalBytes, std::memory_order_relaxed);
144 m_Progress.store(m_pFetch->totalBytes == 0 ? 0 : (100 * m_pFetch->dataOffset) / m_pFetch->totalBytes, std::memory_order_relaxed);
145 if(m_pProgressCallback != nullptr)
146 {
147 m_pProgressCallback->OnProgress();
148 }
149}
150
151EM_JS(int64_t, ParseDateJsImpl, (const char *pStr), {
152 const parsedDate = Date.parse(UTF8ToString(pStr));
153 if(isNaN(parsedDate)) // Invalid dates are represented as NaN for some reason
154 {
155 return -1;
156 }
157 return BigInt(Math.floor(parsedDate / 1000));
158});
159
160void CHttpRequestEmscripten::OnHeader(const char *pName, const char *pValue)
161{
162 if(str_comp_nocase(pName, "Date") == 0)
163 {
164 const int64_t Value = ParseDateJsImpl(pValue);
165 if(Value != -1)
166 {
167 m_ResultDate = Value;
168 }
169 }
170 else if(str_comp_nocase(pName, "Last-Modified") == 0)
171 {
172 const int64_t Value = ParseDateJsImpl(pValue);
173 if(Value != -1)
174 {
175 m_ResultLastModified = Value;
176 }
177 }
178}
179
180void CHttpRequestEmscripten::OnCompletionInternal(EHttpState State, const char *pErrorDetail)
181{
182 if(m_pFetch != nullptr)
183 {
184 const size_t HeadersLength = emscripten_fetch_get_response_headers_length(m_pFetch);
185 if(HeadersLength != 0)
186 {
187 char *pHeaders = static_cast<char *>(malloc(HeadersLength + 1));
188 dbg_assert(emscripten_fetch_get_response_headers(m_pFetch, pHeaders, HeadersLength + 1) == HeadersLength + 1, "emscripten_fetch_get_response_headers failure");
189 char **ppUnpackedHeaders = emscripten_fetch_unpack_response_headers(pHeaders);
190
191 int HeaderIndex = 0;
192 while(ppUnpackedHeaders[HeaderIndex] != nullptr)
193 {
194 const char *pName = ppUnpackedHeaders[HeaderIndex];
195 ++HeaderIndex;
196 const char *pValue = ppUnpackedHeaders[HeaderIndex];
197 ++HeaderIndex;
198 dbg_assert(pValue != nullptr, "emscripten_fetch_unpack_response_headers result unexpected: value is nullptr for header '%s'", pName);
199 OnHeader(pName, pValue);
200 }
201
202 emscripten_fetch_free_unpacked_response_headers(ppUnpackedHeaders);
203 free(pHeaders);
204 }
205
206 m_StatusCode = m_pFetch->status;
207 }
208
209 if(State == EHttpState::DONE)
210 {
211 if(g_Config.m_DbgHttp || m_LogProgress >= HTTPLOG::ALL)
212 {
213 log_info("http", "task done: %s", m_aUrl);
214 }
215 }
216 else
217 {
218 if(g_Config.m_DbgHttp || m_LogProgress >= HTTPLOG::FAILURE)
219 {
220 const char *pError =
221 pErrorDetail != nullptr ? pErrorDetail :
222 State == EHttpState::ABORTED ? "Request aborted" :
223 (m_pFetch != nullptr && m_pFetch->statusText[0] != '\0') ? m_pFetch->statusText :
224 "Unknown (check the web browser console)";
225 log_error("http", "%s failed. fetch error: %s", m_aUrl, pError);
226 }
227 }
228
229 if(m_pFetch != nullptr)
230 {
231 // Unset member variable before closing handle so completion callbacks are not called again.
232 emscripten_fetch_t *pFetch = m_pFetch;
233 m_pFetch = nullptr;
234 dbg_assert(emscripten_fetch_close(pFetch) == EMSCRIPTEN_RESULT_SUCCESS, "emscripten_fetch_close failure");
235 }
236
237 IHttpRequest::OnCompletionInternal(State);
238}
239
240void CHttpRequestEmscripten::FetchCallbackSuccess(emscripten_fetch_t *pFetch)
241{
242 static_cast<CHttpRequestEmscripten *>(pFetch->userData)->OnSuccess();
243}
244
245void CHttpRequestEmscripten::FetchCallbackFailure(emscripten_fetch_t *pFetch)
246{
247 static_cast<CHttpRequestEmscripten *>(pFetch->userData)->OnFailure();
248}
249
250void CHttpRequestEmscripten::FetchCallbackProgress(emscripten_fetch_t *pFetch)
251{
252 static_cast<CHttpRequestEmscripten *>(pFetch->userData)->OnProgress();
253}
254
255std::unique_ptr<IHttpRequest> CreateHttpRequest(const char *pUrl)
256{
257 return std::make_unique<CHttpRequestEmscripten>(pUrl);
258}
259
260bool CHttpEmscripten::Init(std::chrono::milliseconds ShutdownDelay)
261{
262 m_ShutdownDelay = ShutdownDelay;
263
264 m_pThread = thread_init(CHttpEmscripten::ThreadMain, this, "http");
265
266 std::unique_lock Lock(m_Lock);
267 m_ConditionVariableInit.wait(Lock, [this]() { return m_Initialized.load(std::memory_order_seq_cst); });
268
269 return true;
270}
271
272void CHttpEmscripten::Shutdown()
273{
274 {
275 std::unique_lock Lock(m_Lock);
276 if(m_Shutdown || !m_Initialized)
277 {
278 return;
279 }
280 m_Shutdown = true;
281 }
282 m_ConditionVariableLoop.notify_all();
283}
284
285CHttpEmscripten::~CHttpEmscripten()
286{
287 if(!m_pThread)
288 {
289 return;
290 }
291
292 Shutdown();
293 thread_wait(m_pThread);
294}
295
296void CHttpEmscripten::Run(std::shared_ptr<IHttpRequest> pRequest)
297{
298 {
299 std::unique_lock Lock(m_Lock);
300 dbg_assert(m_Initialized, "HTTP not initialized");
301 std::shared_ptr<CHttpRequestEmscripten> pRequestImpl = std::static_pointer_cast<CHttpRequestEmscripten>(pRequest);
302 pRequestImpl->m_pHttp = this;
303 if(m_Shutdown)
304 {
305 pRequestImpl->OnCompletionInternal(EHttpState::ABORTED, "Shutting down");
306 return;
307 }
308 m_PendingRequests.emplace_back(pRequestImpl);
309 }
310 m_ConditionVariableLoop.notify_all();
311}
312
313EM_JS(void, EscapeUrlJsImpl, (char *pBuf, size_t Size, const char *pStr), {
314 const escapedString = encodeURIComponent(UTF8ToString(pStr));
315 stringToUTF8(escapedString, pBuf, Size);
316});
317
318void EscapeUrl(char *pBuf, size_t Size, const char *pStr)
319{
320 EscapeUrlJsImpl(pBuf, Size, pStr);
321}
322
323bool CHttpEmscripten::HasIpresolveBug() const
324{
325 return false;
326}
327
328void CHttpEmscripten::ThreadMain(void *pUser)
329{
330 static_cast<CHttpEmscripten *>(pUser)->RunLoop();
331}
332
333void CHttpEmscripten::RunLoop()
334{
335 {
336 std::unique_lock Lock(m_Lock);
337 m_Initialized = true;
338 }
339 m_ConditionVariableInit.notify_all();
340
341 while(true)
342 {
343 {
344 std::unique_lock Lock(m_Lock);
345 if(m_Shutdown)
346 {
347 if(m_RunningRequests.empty() && m_PendingRequests.empty())
348 break;
349
350 const auto Now = std::chrono::steady_clock::now();
351 if(!m_ShutdownTime.has_value())
352 {
353 m_ShutdownTime = Now + m_ShutdownDelay;
354 }
355 else if(m_ShutdownTime < Now)
356 {
357 if(m_StartedShutdown)
358 {
359 break;
360 }
361 else
362 {
363 for(auto &[_, pRequest] : m_RunningRequests)
364 {
365 auto [ExistingElement, Inserted] = m_PendingFetchChanges.emplace(pRequest->m_pFetch, EHttpState::ABORTED);
366 if(!Inserted)
367 {
368 ExistingElement->second = EHttpState::ABORTED;
369 }
370 }
371 m_StartedShutdown = true;
372 m_ShutdownTime = Now + m_ShutdownDelay;
373 }
374 }
375 }
376 }
377
378 decltype(m_PendingRequests) PendingRequests = {};
379 decltype(m_PendingFetchChanges) PendingFetchChanges = {};
380 {
381 std::unique_lock Lock(m_Lock);
382 std::swap(m_PendingRequests, PendingRequests);
383 std::swap(m_PendingFetchChanges, PendingFetchChanges);
384 }
385
386 while(!PendingRequests.empty())
387 {
388 auto &pRequest = PendingRequests.front();
389 if(g_Config.m_DbgHttp)
390 {
391 log_debug("http", "task: %s %s", CHttpRequestEmscripten::GetRequestType(pRequest->m_Type), pRequest->m_aUrl);
392 }
393
394 if(pRequest->ShouldSkipRequest())
395 {
396 if(pRequest->m_pProgressCallback != nullptr)
397 {
398 pRequest->m_pProgressCallback->OnCompletion(EHttpState::DONE);
399 }
400 {
401 std::unique_lock WaitLock(pRequest->m_WaitMutex);
402 pRequest->m_State = EHttpState::DONE;
403 }
404 pRequest->m_WaitCondition.notify_all();
405 PendingRequests.pop_front();
406 continue;
407 }
408
409 if(m_StartedShutdown || pRequest->IsAbortRequested())
410 {
411 pRequest->OnCompletionInternal(EHttpState::ABORTED, m_StartedShutdown ? "Shutting down" : "Request aborted");
412 PendingRequests.pop_front();
413 continue;
414 }
415
416 if(!pRequest->ConfigureAndRun())
417 {
418 pRequest->OnCompletionInternal(EHttpState::ABORTED, "Failed to initialize request");
419 PendingRequests.pop_front();
420 continue;
421 }
422
423 {
424 emscripten_fetch_t *pFetch = pRequest->m_pFetch;
425 auto [_, Inserted] = m_RunningRequests.emplace(pFetch, std::move(pRequest));
426 dbg_assert(Inserted, "Request with same fetch handle already running");
427 }
428 PendingRequests.pop_front();
429 }
430
431 for(const auto &[pFetch, NewState] : PendingFetchChanges)
432 {
433 auto pRequest = m_RunningRequests.find(pFetch);
434 if(pRequest == m_RunningRequests.end())
435 {
436 // Requests can be aborted even if they are not in m_RunningRequests anymore.
437 // We only hold the lock to swap the pending fetch changes above, so another
438 // pending state change to abort a request can be added while the HTTP thread
439 // is removing the running request in the branch below.
440 dbg_assert(NewState == EHttpState::ABORTED, "Request for pending fetch state change not found");
441 }
442 else
443 {
444 pRequest->second->OnCompletionInternal(NewState, nullptr);
445 m_RunningRequests.erase(pRequest);
446 }
447 }
448 PendingFetchChanges.clear();
449
450 // Return control to the browser so the created fetch handles are serviced.
451 // This will cause the success, failure and progress callbacks to be called.
452 emscripten_sleep(0);
453
454 // Wait a bit for state changes, but also wake up periodically because we
455 // need to call emscripten_sleep to service the handles.
456 std::unique_lock Lock(m_Lock);
457 const auto &&WaitPredicate = [this]() { return m_Shutdown || !m_PendingRequests.empty() || !m_PendingFetchChanges.empty(); };
458 const auto WaitTime = std::chrono::milliseconds(100);
459 const auto Now = std::chrono::steady_clock::now();
460 if(m_ShutdownTime.has_value() && m_ShutdownTime.value() - Now < WaitTime)
461 {
462 m_ConditionVariableLoop.wait_until(Lock, m_ShutdownTime.value(), WaitPredicate);
463 }
464 else
465 {
466 m_ConditionVariableLoop.wait_for(Lock, WaitTime, WaitPredicate);
467 }
468 }
469
470 std::unique_lock Lock(m_Lock);
471 for(auto &pRequest : m_PendingRequests)
472 {
473 pRequest->OnCompletionInternal(EHttpState::ABORTED, "Shutting down");
474 }
475 m_PendingRequests.clear();
476
477 for(auto &[_, pRequest] : m_RunningRequests)
478 {
479 pRequest->OnCompletionInternal(EHttpState::ABORTED, "Shutting down");
480 }
481 m_RunningRequests.clear();
482}
483
484void CHttpEmscripten::AddPendingStateChange(emscripten_fetch_t *pFetch, EHttpState State)
485{
486 {
487 std::unique_lock Lock(m_Lock);
488 auto [ExistingElement, Inserted] = m_PendingFetchChanges.emplace(pFetch, State);
489 if(!Inserted && ExistingElement->second != EHttpState::ABORTED)
490 {
491 ExistingElement->second = State;
492 }
493 }
494 m_ConditionVariableLoop.notify_all();
495}
496
497IEngineHttp *CreateEngineHttp()
498{
499 return new CHttpEmscripten;
500}
501
502#endif // CONF_PLATFORM_EMSCRIPTEN
503