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