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/mem.h>
8#include <base/str.h>
9
10#include <engine/external/json-parser/json.h>
11#include <engine/shared/config.h>
12#include <engine/shared/json.h>
13#include <engine/storage.h>
14
15#include <game/version.h>
16
17#include <algorithm>
18
19IHttpRequest::IHttpRequest(const char *pUrl)
20{
21 str_copy(dst&: m_aUrl, src: pUrl);
22 sha256_init(ctxt: &m_ActualSha256Ctx);
23}
24
25IHttpRequest::~IHttpRequest()
26{
27 dbg_assert(m_File == nullptr, "HTTP request file was not closed");
28 free(ptr: m_pBody);
29 free(ptr: m_pBuffer);
30 if(m_State == EHttpState::DONE && m_ValidateBeforeOverwrite)
31 {
32 OnValidation(Success: false);
33 }
34}
35
36void IHttpRequest::WriteToMemory()
37{
38 m_WriteToMemory = true;
39 m_WriteToFile = false;
40}
41
42void IHttpRequest::WriteToFile(IStorage *pStorage, const char *pDest, int StorageType)
43{
44 m_WriteToMemory = false;
45 m_WriteToFile = true;
46 str_copy(dst&: m_aDest, src: pDest);
47 if(StorageType == -2)
48 {
49 pStorage->GetBinaryPath(pFilename: m_aDest, pBuffer: m_aDestAbsolute, BufferSize: sizeof(m_aDestAbsolute));
50 }
51 else
52 {
53 pStorage->GetCompletePath(Type: StorageType, pDir: m_aDest, pBuffer: m_aDestAbsolute, BufferSize: sizeof(m_aDestAbsolute));
54 }
55 IStorage::FormatTmpPath(aBuf: m_aDestAbsoluteTmp, BufSize: sizeof(m_aDestAbsoluteTmp), pPath: m_aDestAbsolute);
56}
57
58void IHttpRequest::WriteToFileAndMemory(IStorage *pStorage, const char *pDest, int StorageType)
59{
60 WriteToFile(pStorage, pDest, StorageType);
61 m_WriteToMemory = true;
62}
63
64void IHttpRequest::Head()
65{
66 m_Type = REQUEST::HEAD;
67}
68
69void IHttpRequest::Post(const unsigned char *pData, size_t DataLength)
70{
71 m_Type = REQUEST::POST;
72 m_BodyLength = DataLength;
73 m_pBody = (unsigned char *)malloc(size: std::max(a: (size_t)1, b: DataLength));
74 mem_copy(dest: m_pBody, source: pData, size: DataLength);
75}
76
77void IHttpRequest::PostJson(const char *pJson)
78{
79 m_Type = REQUEST::POST_JSON;
80 m_BodyLength = str_length(str: pJson);
81 m_pBody = (unsigned char *)malloc(size: m_BodyLength);
82 mem_copy(dest: m_pBody, source: pJson, size: m_BodyLength);
83}
84
85void IHttpRequest::HeaderString(const char *pName, const char *pValue)
86{
87 char aHeader[256];
88 str_format(buffer: aHeader, buffer_size: sizeof(aHeader), format: "%s: %s", pName, pValue);
89 Header(pNameColonValue: aHeader);
90}
91
92void IHttpRequest::HeaderInt(const char *pName, int Value)
93{
94 char aHeader[256];
95 str_format(buffer: aHeader, buffer_size: sizeof(aHeader), format: "%s: %d", pName, Value);
96 Header(pNameColonValue: aHeader);
97}
98
99const char *IHttpRequest::Dest() const
100{
101 if(m_WriteToFile)
102 {
103 return m_aDest;
104 }
105 else
106 {
107 return nullptr;
108 }
109}
110
111void IHttpRequest::Wait()
112{
113 std::unique_lock Lock(m_WaitMutex);
114 m_WaitCondition.wait(lock&: Lock, p: [this]() {
115 EHttpState State = m_State.load(m: std::memory_order_seq_cst);
116 return State != EHttpState::QUEUED && State != EHttpState::RUNNING;
117 });
118}
119
120void IHttpRequest::OnValidation(bool Success)
121{
122 dbg_assert(m_ValidateBeforeOverwrite, "this function is illegal to call without having set ValidateBeforeOverwrite");
123 m_ValidateBeforeOverwrite = false;
124 if(Success)
125 {
126 if(m_IfModifiedSince >= 0 && m_StatusCode == 304) // 304 Not Modified
127 {
128 (void)fs_remove(filename: m_aDestAbsoluteTmp);
129 return;
130 }
131 if(fs_rename(oldname: m_aDestAbsoluteTmp, newname: m_aDestAbsolute))
132 {
133 log_error("http", "i/o error, cannot move file: %s", m_aDest);
134 m_State = EHttpState::ERROR;
135 (void)fs_remove(filename: m_aDestAbsoluteTmp);
136 }
137 }
138 else
139 {
140 m_State = EHttpState::ERROR;
141 (void)fs_remove(filename: m_aDestAbsoluteTmp);
142 }
143}
144
145void IHttpRequest::Result(unsigned char **ppResult, size_t *pResultLength) const
146{
147 dbg_assert(State() == EHttpState::DONE, "Request not done");
148 dbg_assert(m_WriteToMemory, "Result only usable when written to memory");
149 *ppResult = m_pBuffer;
150 *pResultLength = m_ResponseLength;
151}
152
153json_value *IHttpRequest::ResultJson() const
154{
155 unsigned char *pResult;
156 size_t ResultLength;
157 Result(ppResult: &pResult, pResultLength: &ResultLength);
158 return JsonParse(pJson: (char *)pResult, Length: ResultLength);
159}
160
161const SHA256_DIGEST &IHttpRequest::ResultSha256() const
162{
163 dbg_assert(State() == EHttpState::DONE, "Request not done");
164 dbg_assert(m_ActualSha256.has_value(), "Result SHA256 missing");
165 return m_ActualSha256.value();
166}
167
168int IHttpRequest::StatusCode() const
169{
170 dbg_assert(State() == EHttpState::DONE, "Request not done");
171 return m_StatusCode;
172}
173
174std::optional<int64_t> IHttpRequest::ResultAgeSeconds() const
175{
176 dbg_assert(State() == EHttpState::DONE, "Request not done");
177 if(!m_ResultDate || !m_ResultLastModified)
178 {
179 return {};
180 }
181 return *m_ResultDate - *m_ResultLastModified;
182}
183
184std::optional<int64_t> IHttpRequest::ResultLastModified() const
185{
186 dbg_assert(State() == EHttpState::DONE, "Request not done");
187 return m_ResultLastModified;
188}
189
190static bool CalculateSha256(const char *pAbsoluteFilename, SHA256_DIGEST *pSha256)
191{
192 IOHANDLE File = io_open(filename: pAbsoluteFilename, flags: IOFLAG_READ);
193 if(!File)
194 {
195 return false;
196 }
197 SHA256_CTX Sha256Ctxt;
198 sha256_init(ctxt: &Sha256Ctxt);
199 unsigned char aBuffer[64 * 1024];
200 while(true)
201 {
202 unsigned Bytes = io_read(io: File, buffer: aBuffer, size: sizeof(aBuffer));
203 if(Bytes == 0)
204 break;
205 sha256_update(ctxt: &Sha256Ctxt, data: aBuffer, data_len: Bytes);
206 }
207 io_close(io: File);
208 *pSha256 = sha256_finish(ctxt: &Sha256Ctxt);
209 return true;
210}
211
212const char *const IHttpRequest::USER_AGENT_STRING = GAME_NAME " " GAME_RELEASE_VERSION " (" CONF_PLATFORM_STRING "; " CONF_ARCH_STRING ")";
213
214const char *IHttpRequest::GetRequestType(REQUEST Type)
215{
216 switch(Type)
217 {
218 case REQUEST::GET:
219 return "GET";
220 case REQUEST::HEAD:
221 return "HEAD";
222 case REQUEST::POST:
223 case REQUEST::POST_JSON:
224 return "POST";
225 default:
226 dbg_assert_failed("Invalid Type: %d", (int)Type);
227 }
228}
229
230bool IHttpRequest::ShouldSkipRequest()
231{
232 if(m_WriteToFile && m_ExpectedSha256.has_value())
233 {
234 SHA256_DIGEST Sha256;
235 if(CalculateSha256(pAbsoluteFilename: m_aDestAbsolute, pSha256: &Sha256) && Sha256 == m_ExpectedSha256.value())
236 {
237 log_debug("http", "skipping download because expected file already exists: %s", m_aDest);
238 return true;
239 }
240 }
241 return false;
242}
243
244bool IHttpRequest::BeforeInit()
245{
246 if(m_WriteToFile)
247 {
248 if(m_SkipByFileTime)
249 {
250 time_t FileCreatedTime, FileModifiedTime;
251 if(fs_file_time(name: m_aDestAbsolute, created: &FileCreatedTime, modified: &FileModifiedTime) == 0)
252 {
253 m_IfModifiedSince = FileModifiedTime;
254 }
255 }
256
257 if(fs_makedir_rec_for(path: m_aDestAbsoluteTmp) < 0)
258 {
259 log_error("http", "i/o error, cannot create folder for: %s", m_aDest);
260 return false;
261 }
262
263 m_File = io_open(filename: m_aDestAbsoluteTmp, flags: IOFLAG_WRITE);
264 if(!m_File)
265 {
266 log_error("http", "i/o error, cannot open file: %s", m_aDest);
267 return false;
268 }
269 }
270 return true;
271}
272
273size_t IHttpRequest::OnData(const char *pData, size_t DataSize)
274{
275 // Need to check for the maximum response size here as implementation may not support it,
276 // e.g. curl can only guarantee it if the server sets a Content-Length header.
277 if(m_MaxResponseSize >= 0 && m_ResponseLength + DataSize > (uint64_t)m_MaxResponseSize)
278 {
279 return 0;
280 }
281
282 if(DataSize == 0)
283 {
284 return DataSize;
285 }
286
287 sha256_update(ctxt: &m_ActualSha256Ctx, data: pData, data_len: DataSize);
288
289 size_t Result = DataSize;
290
291 if(m_WriteToMemory)
292 {
293 size_t NewBufferSize = std::max(a: (size_t)1024, b: m_BufferSize);
294 while(m_ResponseLength + DataSize > NewBufferSize)
295 {
296 NewBufferSize *= 2;
297 }
298 if(NewBufferSize != m_BufferSize)
299 {
300 m_pBuffer = (unsigned char *)realloc(ptr: m_pBuffer, size: NewBufferSize);
301 m_BufferSize = NewBufferSize;
302 }
303 mem_copy(dest: m_pBuffer + m_ResponseLength, source: pData, size: DataSize);
304 }
305 if(m_WriteToFile)
306 {
307 Result = io_write(io: m_File, buffer: pData, size: DataSize);
308 }
309 m_ResponseLength += DataSize;
310 return Result;
311}
312
313void IHttpRequest::OnCompletionInternal(EHttpState State)
314{
315 if(State == EHttpState::DONE)
316 {
317 m_ActualSha256 = sha256_finish(ctxt: &m_ActualSha256Ctx);
318 if(m_ExpectedSha256.has_value() && m_ActualSha256.value() != m_ExpectedSha256.value())
319 {
320 if(g_Config.m_DbgHttp || m_LogProgress >= HTTPLOG::FAILURE)
321 {
322 char aActualSha256[SHA256_MAXSTRSIZE];
323 sha256_str(digest: m_ActualSha256.value(), str: aActualSha256, max_len: sizeof(aActualSha256));
324 char aExpectedSha256[SHA256_MAXSTRSIZE];
325 sha256_str(digest: m_ExpectedSha256.value(), str: aExpectedSha256, max_len: sizeof(aExpectedSha256));
326 log_error("http", "SHA256 mismatch: got=%s, expected=%s, url=%s", aActualSha256, aExpectedSha256, m_aUrl);
327 }
328 State = EHttpState::ERROR;
329 }
330 }
331
332 if(m_WriteToFile)
333 {
334 if(m_File && io_close(io: m_File) != 0)
335 {
336 log_error("http", "i/o error, cannot close file: %s", m_aDest);
337 State = EHttpState::ERROR;
338 }
339 m_File = nullptr;
340
341 if(State == EHttpState::ERROR || State == EHttpState::ABORTED)
342 {
343 (void)fs_remove(filename: m_aDestAbsoluteTmp);
344 }
345 else if(m_IfModifiedSince >= 0 && m_StatusCode == 304) // 304 Not Modified
346 {
347 (void)fs_remove(filename: m_aDestAbsoluteTmp);
348 if(m_WriteToMemory)
349 {
350 free(ptr: m_pBuffer);
351 m_pBuffer = nullptr;
352 m_ResponseLength = 0;
353 void *pBuffer;
354 unsigned Length;
355 IOHANDLE File = io_open(filename: m_aDestAbsolute, flags: IOFLAG_READ);
356 bool Success = File && io_read_all(io: File, result: &pBuffer, result_len: &Length);
357 if(File)
358 {
359 io_close(io: File);
360 }
361 if(Success)
362 {
363 m_pBuffer = (unsigned char *)pBuffer;
364 m_ResponseLength = Length;
365 }
366 else
367 {
368 log_error("http", "i/o error, cannot read existing file: %s", m_aDest);
369 State = EHttpState::ERROR;
370 }
371 }
372 }
373 else if(!m_ValidateBeforeOverwrite)
374 {
375 if(fs_rename(oldname: m_aDestAbsoluteTmp, newname: m_aDestAbsolute))
376 {
377 log_error("http", "i/o error, cannot move file: %s", m_aDest);
378 State = EHttpState::ERROR;
379 (void)fs_remove(filename: m_aDestAbsoluteTmp);
380 }
381 }
382 }
383
384 // The globally visible state must be updated after OnCompletion has finished,
385 // or other threads may try to access the result of a completed HTTP request,
386 // before the result has been initialized/updated in OnCompletion.
387 if(m_pProgressCallback != nullptr)
388 {
389 m_pProgressCallback->OnCompletion(State);
390 }
391 {
392 std::unique_lock WaitLock(m_WaitMutex);
393 m_State = State;
394 }
395 m_WaitCondition.notify_all();
396}
397