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