1#include "updater.h"
2
3#include <base/dbg.h>
4#include <base/str.h>
5
6#include <engine/client.h>
7#include <engine/engine.h>
8#include <engine/external/json-parser/json.h>
9#include <engine/shared/http.h>
10#include <engine/shared/json.h>
11#include <engine/storage.h>
12
13#include <game/version.h>
14
15#include <cstdlib> // system
16#include <unordered_set>
17
18using std::string;
19
20class CUpdaterFetchTask : public CHttpRequest
21{
22 char m_aBuf[256];
23 char m_aBuf2[256];
24 CUpdater *m_pUpdater;
25
26 void OnProgress() override;
27
28protected:
29 void OnCompletion(EHttpState State) override;
30
31public:
32 CUpdaterFetchTask(CUpdater *pUpdater, const char *pFile, const char *pDestPath);
33};
34
35// addition of '/' to keep paths intact, because EscapeUrl() (using curl_easy_escape) doesn't do this
36static inline bool IsUnreserved(unsigned char c)
37{
38 return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
39 (c >= '0' && c <= '9') || c == '-' || c == '_' ||
40 c == '.' || c == '~' || c == '/';
41}
42
43static void UrlEncodePath(const char *pIn, char *pOut, size_t OutSize)
44{
45 if(!pIn || !pOut || OutSize == 0)
46 return;
47 static const char HEX[] = "0123456789ABCDEF";
48 size_t WriteIndex = 0;
49 for(size_t i = 0; pIn[i] != '\0'; ++i)
50 {
51 unsigned char c = static_cast<unsigned char>(pIn[i]);
52 if(IsUnreserved(c))
53 {
54 if(OutSize - WriteIndex < 2) // require 1 byte + NUL
55 break;
56 pOut[WriteIndex++] = static_cast<char>(c);
57 }
58 else
59 {
60 if(OutSize - WriteIndex < 4) // require 3 bytes + NUL
61 break;
62 pOut[WriteIndex++] = '%';
63 pOut[WriteIndex++] = HEX[c >> 4]; // upper 4 bits of c
64 pOut[WriteIndex++] = HEX[c & 0x0F]; // lower 4 bits of c
65 }
66 }
67 pOut[WriteIndex] = '\0';
68}
69
70static const char *GetUpdaterUrl(char *pBuf, int BufSize, const char *pFile)
71{
72 char aBuf[1024];
73 UrlEncodePath(pIn: pFile, pOut: aBuf, OutSize: sizeof(aBuf));
74 str_format(buffer: pBuf, buffer_size: BufSize, format: "https://update.ddnet.org/%s", aBuf);
75 return pBuf;
76}
77
78static const char *GetUpdaterDestPath(char *pBuf, int BufSize, const char *pFile, const char *pDestPath)
79{
80 if(!pDestPath)
81 {
82 pDestPath = pFile;
83 }
84 str_format(buffer: pBuf, buffer_size: BufSize, format: "update/%s", pDestPath);
85 return pBuf;
86}
87
88CUpdaterFetchTask::CUpdaterFetchTask(CUpdater *pUpdater, const char *pFile, const char *pDestPath) :
89 CHttpRequest(GetUpdaterUrl(pBuf: m_aBuf, BufSize: sizeof(m_aBuf), pFile)),
90 m_pUpdater(pUpdater)
91{
92 WriteToFile(pStorage: pUpdater->m_pStorage, pDest: GetUpdaterDestPath(pBuf: m_aBuf2, BufSize: sizeof(m_aBuf2), pFile, pDestPath), StorageType: -2);
93}
94
95void CUpdaterFetchTask::OnProgress()
96{
97 const CLockScope LockScope(m_pUpdater->m_Lock);
98 m_pUpdater->m_Percent = Progress();
99}
100
101void CUpdaterFetchTask::OnCompletion(EHttpState State)
102{
103 const char *pFilename = nullptr;
104 for(const char *pPath = Dest(); *pPath; pPath++)
105 if(*pPath == '/')
106 pFilename = pPath + 1;
107 pFilename = pFilename ? pFilename : Dest();
108 if(!str_comp(a: pFilename, b: "update.json"))
109 {
110 if(State == EHttpState::DONE)
111 m_pUpdater->SetCurrentState(IUpdater::GOT_MANIFEST);
112 else if(State == EHttpState::ERROR)
113 m_pUpdater->SetCurrentState(IUpdater::FAIL);
114 }
115}
116
117CUpdater::CUpdater()
118{
119 m_pClient = nullptr;
120 m_pStorage = nullptr;
121 m_pEngine = nullptr;
122 m_pHttp = nullptr;
123 m_State = CLEAN;
124 m_Percent = 0;
125 m_pCurrentTask = nullptr;
126
127 m_ClientUpdate = m_ServerUpdate = m_ClientFetched = m_ServerFetched = false;
128
129 IStorage::FormatTmpPath(aBuf: m_aClientExecTmp, BufSize: sizeof(m_aClientExecTmp), CLIENT_EXEC);
130 IStorage::FormatTmpPath(aBuf: m_aServerExecTmp, BufSize: sizeof(m_aServerExecTmp), SERVER_EXEC);
131}
132
133void CUpdater::Init(CHttp *pHttp)
134{
135 m_pClient = Kernel()->RequestInterface<IClient>();
136 m_pStorage = Kernel()->RequestInterface<IStorage>();
137 m_pEngine = Kernel()->RequestInterface<IEngine>();
138 m_pHttp = pHttp;
139}
140
141void CUpdater::SetCurrentState(EUpdaterState NewState)
142{
143 const CLockScope LockScope(m_Lock);
144 m_State = NewState;
145}
146
147IUpdater::EUpdaterState CUpdater::GetCurrentState()
148{
149 const CLockScope LockScope(m_Lock);
150 return m_State;
151}
152
153void CUpdater::GetCurrentFile(char *pBuf, int BufSize)
154{
155 const CLockScope LockScope(m_Lock);
156 str_copy(dst: pBuf, src: m_aStatus, dst_size: BufSize);
157}
158
159int CUpdater::GetCurrentPercent()
160{
161 const CLockScope LockScope(m_Lock);
162 return m_Percent;
163}
164
165void CUpdater::FetchFile(const char *pFile, const char *pDestPath)
166{
167 const CLockScope LockScope(m_Lock);
168 m_pCurrentTask = std::make_shared<CUpdaterFetchTask>(args: this, args&: pFile, args&: pDestPath);
169 str_copy(dst&: m_aStatus, src: m_pCurrentTask->Dest());
170 m_pHttp->Run(pRequest: m_pCurrentTask);
171}
172
173bool CUpdater::MoveFile(const char *pFile)
174{
175 char aBuf[256];
176 const size_t Length = str_length(str: pFile);
177 bool Success = true;
178
179#if !defined(CONF_FAMILY_WINDOWS)
180 if(!str_comp_nocase(a: pFile + Length - 4, b: ".dll"))
181 return Success;
182#endif
183
184#if !defined(CONF_PLATFORM_LINUX)
185 if(!str_comp_nocase(pFile + Length - 3, ".so"))
186 return Success;
187#endif
188
189 if(!str_comp_nocase(a: pFile + Length - 4, b: ".dll") || !str_comp_nocase(a: pFile + Length - 4, b: ".ttf") || !str_comp_nocase(a: pFile + Length - 3, b: ".so"))
190 {
191 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s.old", pFile);
192 m_pStorage->RenameBinaryFile(pOldFilename: pFile, pNewFilename: aBuf);
193 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "update/%s", pFile);
194 Success &= m_pStorage->RenameBinaryFile(pOldFilename: aBuf, pNewFilename: pFile);
195 }
196 else
197 {
198 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "update/%s", pFile);
199 Success &= m_pStorage->RenameBinaryFile(pOldFilename: aBuf, pNewFilename: pFile);
200 }
201
202 return Success;
203}
204
205void CUpdater::Update()
206{
207 switch(GetCurrentState())
208 {
209 case IUpdater::GOT_MANIFEST:
210 PerformUpdate();
211 break;
212 case IUpdater::DOWNLOADING:
213 RunningUpdate();
214 break;
215 case IUpdater::MOVE_FILES:
216 CommitUpdate();
217 break;
218 default:
219 return;
220 }
221}
222
223void CUpdater::AddFileJob(const char *pFile, bool Job)
224{
225 m_FileJobs.emplace_front(args&: pFile, args&: Job);
226}
227
228bool CUpdater::ReplaceClient()
229{
230 dbg_msg(sys: "updater", fmt: "replacing " PLAT_CLIENT_EXEC);
231 bool Success = true;
232 char aPath[IO_MAX_PATH_LENGTH];
233
234 // Replace running executable by renaming twice...
235 m_pStorage->RemoveBinaryFile(CLIENT_EXEC ".old");
236 Success &= m_pStorage->RenameBinaryFile(PLAT_CLIENT_EXEC, CLIENT_EXEC ".old");
237 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "update/%s", m_aClientExecTmp);
238 Success &= m_pStorage->RenameBinaryFile(pOldFilename: aPath, PLAT_CLIENT_EXEC);
239#if !defined(CONF_FAMILY_WINDOWS)
240 m_pStorage->GetBinaryPath(PLAT_CLIENT_EXEC, pBuffer: aPath, BufferSize: sizeof(aPath));
241 char aBuf[512];
242 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "chmod +x %s", aPath);
243 if(system(command: aBuf))
244 {
245 dbg_msg(sys: "updater", fmt: "ERROR: failed to set client executable bit");
246 Success = false;
247 }
248#endif
249 return Success;
250}
251
252bool CUpdater::ReplaceServer()
253{
254 dbg_msg(sys: "updater", fmt: "replacing " PLAT_SERVER_EXEC);
255 bool Success = true;
256 char aPath[IO_MAX_PATH_LENGTH];
257
258 // Replace running executable by renaming twice...
259 m_pStorage->RemoveBinaryFile(SERVER_EXEC ".old");
260 Success &= m_pStorage->RenameBinaryFile(PLAT_SERVER_EXEC, SERVER_EXEC ".old");
261 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "update/%s", m_aServerExecTmp);
262 Success &= m_pStorage->RenameBinaryFile(pOldFilename: aPath, PLAT_SERVER_EXEC);
263#if !defined(CONF_FAMILY_WINDOWS)
264 m_pStorage->GetBinaryPath(PLAT_SERVER_EXEC, pBuffer: aPath, BufferSize: sizeof(aPath));
265 char aBuf[512];
266 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "chmod +x %s", aPath);
267 if(system(command: aBuf))
268 {
269 dbg_msg(sys: "updater", fmt: "ERROR: failed to set server executable bit");
270 Success = false;
271 }
272#endif
273 return Success;
274}
275
276void CUpdater::ParseUpdate()
277{
278 char aPath[IO_MAX_PATH_LENGTH];
279 void *pBuf;
280 unsigned Length;
281 if(!m_pStorage->ReadFile(pFilename: m_pStorage->GetBinaryPath(pFilename: "update/update.json", pBuffer: aPath, BufferSize: sizeof(aPath)), Type: IStorage::TYPE_ABSOLUTE, ppResult: &pBuf, pResultLen: &Length))
282 return;
283
284 json_value *pVersions = json_parse(json: (json_char *)pBuf, length: Length);
285 free(ptr: pBuf);
286
287 if(!pVersions || pVersions->type != json_array)
288 {
289 if(pVersions)
290 json_value_free(pVersions);
291 return;
292 }
293
294 // if we're already downloading a file, or it's been deleted in the latest version, we skip it if it comes up again
295 std::unordered_set<std::string> SkipSet;
296
297 for(int i = 0; i < json_array_length(pArray: pVersions); i++)
298 {
299 const json_value *pCurrent = json_array_get(pArray: pVersions, Index: i);
300 if(!pCurrent || pCurrent->type != json_object)
301 continue;
302
303 const char *pVersion = json_string_get(pString: json_object_get(pObject: pCurrent, pIndex: "version"));
304 if(!pVersion)
305 continue;
306
307 if(str_comp(a: pVersion, GAME_RELEASE_VERSION) == 0)
308 break;
309
310 if(json_boolean_get(pBoolean: json_object_get(pObject: pCurrent, pIndex: "client")))
311 m_ClientUpdate = true;
312 if(json_boolean_get(pBoolean: json_object_get(pObject: pCurrent, pIndex: "server")))
313 m_ServerUpdate = true;
314
315 const json_value *pDownload = json_object_get(pObject: pCurrent, pIndex: "download");
316 if(pDownload && pDownload->type == json_array)
317 {
318 for(int j = 0; j < json_array_length(pArray: pDownload); j++)
319 {
320 const char *pName = json_string_get(pString: json_array_get(pArray: pDownload, Index: j));
321 if(!pName)
322 continue;
323
324 if(SkipSet.insert(x: pName).second)
325 {
326 AddFileJob(pFile: pName, Job: true);
327 }
328 }
329 }
330
331 const json_value *pRemove = json_object_get(pObject: pCurrent, pIndex: "remove");
332 if(pRemove && pRemove->type == json_array)
333 {
334 for(int j = 0; j < json_array_length(pArray: pRemove); j++)
335 {
336 const char *pName = json_string_get(pString: json_array_get(pArray: pRemove, Index: j));
337 if(!pName)
338 continue;
339
340 if(SkipSet.insert(x: pName).second)
341 {
342 AddFileJob(pFile: pName, Job: false);
343 }
344 }
345 }
346 }
347 json_value_free(pVersions);
348}
349
350void CUpdater::InitiateUpdate()
351{
352 SetCurrentState(IUpdater::GETTING_MANIFEST);
353 FetchFile(pFile: "update.json");
354}
355
356void CUpdater::PerformUpdate()
357{
358 SetCurrentState(IUpdater::PARSING_UPDATE);
359 dbg_msg(sys: "updater", fmt: "parsing update.json");
360 ParseUpdate();
361 m_CurrentJob = m_FileJobs.begin();
362 SetCurrentState(IUpdater::DOWNLOADING);
363}
364
365void CUpdater::RunningUpdate()
366{
367 if(m_pCurrentTask)
368 {
369 if(!m_pCurrentTask->Done())
370 {
371 return;
372 }
373 else if(m_pCurrentTask->State() == EHttpState::ERROR || m_pCurrentTask->State() == EHttpState::ABORTED)
374 {
375 SetCurrentState(IUpdater::FAIL);
376 }
377 }
378
379 if(m_CurrentJob != m_FileJobs.end())
380 {
381 auto &Job = *m_CurrentJob;
382 if(Job.second)
383 {
384 const char *pFile = Job.first.c_str();
385 const size_t Length = str_length(str: pFile);
386 if(!str_comp_nocase(a: pFile + Length - 4, b: ".dll"))
387 {
388#if defined(CONF_FAMILY_WINDOWS)
389 char aBuf[512];
390 str_copy(aBuf, pFile, sizeof(aBuf)); // SDL
391 str_copy(aBuf + Length - 4, "-" PLAT_NAME, sizeof(aBuf) - Length + 4); // -win32
392 str_append(aBuf, pFile + Length - 4); // .dll
393 FetchFile(aBuf, pFile);
394#endif
395 // Ignore DLL downloads on other platforms
396 }
397 else if(!str_comp_nocase(a: pFile + Length - 3, b: ".so"))
398 {
399#if defined(CONF_PLATFORM_LINUX)
400 char aBuf[512];
401 str_copy(dst: aBuf, src: pFile, dst_size: sizeof(aBuf)); // libsteam_api
402 str_copy(dst: aBuf + Length - 3, src: "-" PLAT_NAME, dst_size: sizeof(aBuf) - Length + 3); // -linux-x86_64
403 str_append(dst&: aBuf, src: pFile + Length - 3); // .so
404 FetchFile(pFile: aBuf, pDestPath: pFile);
405#endif
406 // Ignore DLL downloads on other platforms, on Linux we statically link anyway
407 }
408 else
409 {
410 FetchFile(pFile);
411 }
412 }
413 m_CurrentJob++;
414 }
415 else
416 {
417 if(m_ServerUpdate && !m_ServerFetched)
418 {
419 FetchFile(PLAT_SERVER_DOWN, pDestPath: m_aServerExecTmp);
420 m_ServerFetched = true;
421 return;
422 }
423
424 if(m_ClientUpdate && !m_ClientFetched)
425 {
426 FetchFile(PLAT_CLIENT_DOWN, pDestPath: m_aClientExecTmp);
427 m_ClientFetched = true;
428 return;
429 }
430
431 SetCurrentState(IUpdater::MOVE_FILES);
432 }
433}
434
435void CUpdater::CommitUpdate()
436{
437 bool Success = true;
438
439 for(auto &FileJob : m_FileJobs)
440 if(FileJob.second)
441 Success &= MoveFile(pFile: FileJob.first.c_str());
442
443 if(m_ClientUpdate)
444 Success &= ReplaceClient();
445 if(m_ServerUpdate)
446 Success &= ReplaceServer();
447
448 if(Success)
449 {
450 for(const auto &[Filename, JobSuccess] : m_FileJobs)
451 if(!JobSuccess)
452 m_pStorage->RemoveBinaryFile(pFilename: Filename.c_str());
453 }
454
455 if(!Success)
456 SetCurrentState(IUpdater::FAIL);
457 else if(m_pClient->State() == IClient::STATE_ONLINE || m_pClient->EditorHasUnsavedData())
458 SetCurrentState(IUpdater::NEED_RESTART);
459 else
460 {
461 m_pClient->Restart();
462 }
463}
464