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