Bump version.
[MUtilities.git] / src / UpdateChecker.cpp
blobbd3f00c6fb2617b8354a13a2ff422855905893b6
1 ///////////////////////////////////////////////////////////////////////////////
2 // MuldeR's Utilities for Qt
3 // Copyright (C) 2004-2023 LoRd_MuldeR <MuldeR2@GMX.de>
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU Lesser General Public
7 // License as published by the Free Software Foundation; either
8 // version 2.1 of the License, or (at your option) any later version.
9 //
10 // This library is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 // Lesser General Public License for more details.
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with this library; if not, write to the Free Software
17 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19 // http://www.gnu.org/licenses/lgpl-2.1.txt
20 //////////////////////////////////////////////////////////////////////////////////
22 #include <MUtils/Global.h>
23 #include <MUtils/UpdateChecker.h>
24 #include <MUtils/OSSupport.h>
25 #include <MUtils/Exception.h>
27 #include <QStringList>
28 #include <QFile>
29 #include <QFileInfo>
30 #include <QDir>
31 #include <QProcess>
32 #include <QUrl>
33 #include <QEventLoop>
34 #include <QTimer>
35 #include <QElapsedTimer>
36 #include <QSet>
37 #include <QHash>
38 #include <QQueue>
40 #include "Mirrors.h"
42 ///////////////////////////////////////////////////////////////////////////////
43 // CONSTANTS
44 ///////////////////////////////////////////////////////////////////////////////
46 static const char *GLOBALHEADER_ID = "!Update";
48 static const char *MIRROR_URL_POSTFIX[] =
50 "update.ver",
51 "update_beta.ver",
52 NULL
55 static const int MIN_CONNSCORE = 5;
56 static const int QUICK_MIRRORS = 3;
57 static const int MAX_CONN_TIMEOUT = 16000;
58 static const int DOWNLOAD_TIMEOUT = 30000;
60 static const int VERSION_INFO_EXPIRES_MONTHS = 6;
61 static char *USER_AGENT_STR = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:52.0) Gecko/20100101 Firefox/52.0"; /*use something innocuous*/
63 ////////////////////////////////////////////////////////////
64 // Utility Macros
65 ////////////////////////////////////////////////////////////
67 #define CHECK_CANCELLED() do \
68 { \
69 if(MUTILS_BOOLIFY(m_cancelled)) \
70 { \
71 m_success.fetchAndStoreOrdered(0); \
72 log("", "Update check has been cancelled by user!"); \
73 setProgress(m_maxProgress); \
74 setStatus(UpdateStatus_CancelledByUser); \
75 return; \
76 } \
77 } \
78 while(0)
80 #define LOG_MESSAGE_HELPER(X) do \
81 { \
82 if (!(X).isNull()) \
83 { \
84 emit messageLogged((X)); \
85 } \
86 } \
87 while(0)
89 #define STRICMP(X,Y) ((X).compare((Y), Qt::CaseInsensitive) == 0)
91 ////////////////////////////////////////////////////////////
92 // Helper Functions
93 ////////////////////////////////////////////////////////////
95 static QQueue<QString> buildRandomList(const char *const *values)
97 QQueue<QString> list;
98 while(*values)
100 list.insert(MUtils::next_rand_u32(list.size() + 1), QString::fromLatin1(*(values++)));
102 return list;
105 static const QHash<QString, QString> *initEnvVars(const QString &binCurl)
107 QHash<QString, QString> *const environment = new QHash<QString, QString>();
108 const QString tempfolder = QDir::toNativeSeparators(MUtils::temp_folder());
109 environment->insert(QLatin1String("CURL_HOME"), tempfolder);
110 const QFileInfo curlFile(binCurl);
111 environment->insert(QLatin1String("CURL_CA_BUNDLE"), QDir::toNativeSeparators(curlFile.absoluteDir().absoluteFilePath(QString("%1.crt").arg(curlFile.completeBaseName()))));
112 return environment;
115 ////////////////////////////////////////////////////////////
116 // Update Info Class
117 ////////////////////////////////////////////////////////////
119 MUtils::UpdateCheckerInfo::UpdateCheckerInfo(void)
121 resetInfo();
124 void MUtils::UpdateCheckerInfo::resetInfo(void)
126 m_buildNo = 0;
127 m_buildDate.setDate(1900, 1, 1);
128 m_downloadSite.clear();
129 m_downloadAddress.clear();
130 m_downloadFilename.clear();
131 m_downloadFilecode.clear();
132 m_downloadChecksum.clear();
135 bool MUtils::UpdateCheckerInfo::isComplete(void)
137 return (this->m_buildNo > 0) &&
138 (this->m_buildDate.year() >= 2010) &&
139 (!this->m_downloadSite.isEmpty()) &&
140 (!this->m_downloadAddress.isEmpty()) &&
141 (!this->m_downloadFilename.isEmpty()) &&
142 (!this->m_downloadFilecode.isEmpty()) &&
143 (!this->m_downloadChecksum.isEmpty());
146 ////////////////////////////////////////////////////////////
147 // Constructor & Destructor
148 ////////////////////////////////////////////////////////////
150 MUtils::UpdateChecker::UpdateChecker(const QString &binCurl, const QString &binVerify, const QString &applicationId, const quint32 &installedBuildNo, const bool betaUpdates, const bool testMode)
152 m_updateInfo(new UpdateCheckerInfo()),
153 m_binaryCurl(binCurl),
154 m_binaryVerify(binVerify),
155 m_environment(initEnvVars(binCurl)),
156 m_applicationId(applicationId),
157 m_installedBuildNo(installedBuildNo),
158 m_betaUpdates(betaUpdates),
159 m_testMode(testMode),
160 m_maxProgress(MIN_CONNSCORE + 5)
162 m_status = UpdateStatus_NotStartedYet;
163 m_progress = 0;
165 if(m_binaryCurl.isEmpty() || m_binaryVerify.isEmpty())
167 MUTILS_THROW("Required tools not initialized correctly!");
171 MUtils::UpdateChecker::~UpdateChecker(void)
175 ////////////////////////////////////////////////////////////
176 // Public slots
177 ////////////////////////////////////////////////////////////
179 void MUtils::UpdateChecker::start(Priority priority)
181 m_success.fetchAndStoreOrdered(0);
182 m_cancelled.fetchAndStoreOrdered(0);
183 QThread::start(priority);
186 ////////////////////////////////////////////////////////////
187 // Protected functions
188 ////////////////////////////////////////////////////////////
190 void MUtils::UpdateChecker::run(void)
192 qDebug("Update checker thread started!");
193 MUTILS_EXCEPTION_HANDLER(m_testMode ? testMirrorsList() : checkForUpdates());
194 qDebug("Update checker thread completed.");
197 void MUtils::UpdateChecker::checkForUpdates(void)
199 // ----- Initialization ----- //
201 m_updateInfo->resetInfo();
202 setProgress(0);
204 // ----- Test Internet Connection ----- //
206 log("Checking your Internet connection...", "");
207 setStatus(UpdateStatus_CheckingConnection);
209 const int networkStatus = OS::network_status();
210 if(networkStatus == OS::NETWORK_TYPE_NON)
212 if (!MUtils::OS::arguments().contains("ignore-network-status"))
214 log("Operating system reports that the computer is currently offline !!!");
215 setProgress(m_maxProgress);
216 setStatus(UpdateStatus_ErrorNoConnection);
217 return;
221 msleep(333);
222 setProgress(1);
224 // ----- Test Known Hosts Connectivity ----- //
226 int connectionScore = 0;
227 QQueue<QString> mirrorList = buildRandomList(known_hosts);
229 for(int connectionTimeout = 1000; connectionTimeout <= MAX_CONN_TIMEOUT; connectionTimeout *= 2)
231 QElapsedTimer elapsedTimer;
232 elapsedTimer.start();
233 const int globalTimeout = 2 * MIN_CONNSCORE * connectionTimeout, count = mirrorList.count();
234 for(int i = 0; i < count; ++i)
236 Q_ASSERT(!mirrorList.isEmpty());
237 const QString hostName = mirrorList.dequeue();
238 if (tryContactHost(hostName, connectionTimeout))
240 setProgress(1 + (++connectionScore));
241 if (connectionScore >= MIN_CONNSCORE)
243 goto endLoop; /*success*/
246 else
248 mirrorList.enqueue(hostName);
249 if(elapsedTimer.hasExpired(globalTimeout))
251 break; /*timer expired*/
254 CHECK_CANCELLED();
258 endLoop:
259 if(connectionScore < MIN_CONNSCORE)
261 log("", "Connectivity test has failed: Internet connection appears to be broken!");
262 setProgress(m_maxProgress);
263 setStatus(UpdateStatus_ErrorConnectionTestFailed);
264 return;
267 // ----- Fetch Update Info From Server ----- //
269 log("----", "", "Internet connection is operational, checking for updates online...");
270 setStatus(UpdateStatus_FetchingUpdates);
272 int mirrorCount = 0;
273 mirrorList = buildRandomList(update_mirrors);
275 while(!mirrorList.isEmpty())
277 const QString currentMirror = mirrorList.takeFirst();
278 const bool isQuick = (mirrorCount++ < QUICK_MIRRORS);
279 if(tryUpdateMirror(m_updateInfo.data(), currentMirror, isQuick))
281 m_success.ref(); /*success*/
282 break;
284 if (isQuick)
286 mirrorList.append(currentMirror); /*re-schedule*/
288 CHECK_CANCELLED();
289 msleep(1);
292 msleep(333);
293 setProgress(MIN_CONNSCORE + 5);
295 // ----- Generate final result ----- //
297 if(MUTILS_BOOLIFY(m_success))
299 if(m_updateInfo->m_buildNo > m_installedBuildNo)
301 setStatus(UpdateStatus_CompletedUpdateAvailable);
303 else if(m_updateInfo->m_buildNo == m_installedBuildNo)
305 setStatus(UpdateStatus_CompletedNoUpdates);
307 else
309 setStatus(UpdateStatus_CompletedNewVersionOlder);
312 else
314 setStatus(UpdateStatus_ErrorFetchUpdateInfo);
318 void MUtils::UpdateChecker::testMirrorsList(void)
320 QQueue<QString> mirrorList;
321 for(int i = 0; update_mirrors[i]; i++)
323 mirrorList.enqueue(QString::fromLatin1(update_mirrors[i]));
326 // ----- Test update mirrors ----- //
328 qDebug("\n[Mirror Sites]");
329 log("Testing all known mirror sites...", "", "---");
331 UpdateCheckerInfo updateInfo;
332 while (!mirrorList.isEmpty())
334 const QString currentMirror = mirrorList.dequeue();
335 bool success = false;
336 qDebug("Testing: %s", MUTILS_L1STR(currentMirror));
337 log("", "Testing mirror:", currentMirror, "");
338 for (quint8 attempt = 0; attempt < 3; ++attempt)
340 updateInfo.resetInfo();
341 if (tryUpdateMirror(&updateInfo, currentMirror, (!attempt)))
343 success = true;
344 break;
347 if (!success)
349 qWarning("\nUpdate mirror seems to be unavailable:\n%s\n", MUTILS_L1STR(currentMirror));
351 log("", "---");
354 // ----- Test known hosts ----- //
356 mirrorList.clear();
357 for (int i = 0; known_hosts[i]; i++)
359 mirrorList.enqueue(QString::fromLatin1(known_hosts[i]));
362 qDebug("\n[Known Hosts]");
363 log("Testing all known hosts...", "", "---");
365 while(!mirrorList.isEmpty())
367 const QString currentHost = mirrorList.dequeue();
368 qDebug("Testing: %s", MUTILS_L1STR(currentHost));
369 log(QLatin1String(""), "Testing host:", currentHost, "");
370 if (!tryContactHost(currentHost, DOWNLOAD_TIMEOUT))
372 qWarning("\nConnectivity test FAILED on the following host:\n%s\n", MUTILS_L1STR(currentHost));
374 log("---");
378 ////////////////////////////////////////////////////////////
379 // PRIVATE FUNCTIONS
380 ////////////////////////////////////////////////////////////
382 void MUtils::UpdateChecker::setStatus(const int status)
384 if(m_status != status)
386 m_status = status;
387 emit statusChanged(status);
391 void MUtils::UpdateChecker::setProgress(const int progress)
393 const int value = qBound(0, progress, m_maxProgress);
394 if(m_progress != value)
396 emit progressChanged(m_progress = value);
400 void MUtils::UpdateChecker::log(const QString &str1, const QString &str2, const QString &str3, const QString &str4)
402 LOG_MESSAGE_HELPER(str1);
403 LOG_MESSAGE_HELPER(str2);
404 LOG_MESSAGE_HELPER(str3);
405 LOG_MESSAGE_HELPER(str4);
408 bool MUtils::UpdateChecker::tryUpdateMirror(UpdateCheckerInfo *updateInfo, const QString &url, const bool &quick)
410 bool success = false;
411 log("", "Trying update mirror:", url, "");
413 if (quick)
415 setProgress(MIN_CONNSCORE + 1);
416 if (!tryContactHost(QUrl(url).host(), (MAX_CONN_TIMEOUT / 8)))
418 log("", "Mirror is too slow, skipping!");
419 return false;
423 const QString randPart = next_rand_str();
424 const QString outFileVers = QString("%1/%2.ver").arg(temp_folder(), randPart);
425 const QString outFileSign = QString("%1/%2.sig").arg(temp_folder(), randPart);
427 if (!getUpdateInfo(url, outFileVers, outFileSign))
429 log("", "Oops: Download of update information has failed!");
430 goto cleanUp;
433 log("Download completed, verifying signature:", "");
434 setProgress(MIN_CONNSCORE + 4);
435 if (!checkSignature(outFileVers, outFileSign))
437 log("", "Bad signature detected, take care !!!");
438 goto cleanUp;
441 log("", "Signature is valid, parsing update information:", "");
442 success = parseVersionInfo(outFileVers, updateInfo);
444 cleanUp:
445 QFile::remove(outFileVers);
446 QFile::remove(outFileSign);
447 return success;
450 bool MUtils::UpdateChecker::getUpdateInfo(const QString &url, const QString &outFileVers, const QString &outFileSign)
452 log("Downloading update information:", "");
453 setProgress(MIN_CONNSCORE + 2);
454 if(getFile(QUrl(QString("%1%2").arg(url, MIRROR_URL_POSTFIX[m_betaUpdates ? 1 : 0])), outFileVers))
456 if (!m_cancelled)
458 log( "Downloading signature file:", "");
459 setProgress(MIN_CONNSCORE + 3);
460 if (getFile(QUrl(QString("%1%2.rsa").arg(url, MIRROR_URL_POSTFIX[m_betaUpdates ? 1 : 0])), outFileSign))
462 return true; /*completed*/
466 return false;
469 //----------------------------------------------------------
470 // PARSE UPDATE INFO
471 //----------------------------------------------------------
473 #define _CHECK_HEADER(ID,NAME) \
474 if (STRICMP(name, (NAME))) \
476 sectionId = (ID); \
477 continue; \
480 #define _PARSE_TEXT(OUT,KEY) \
481 if (STRICMP(key, (KEY))) \
483 (OUT) = val; \
484 break; \
487 #define _PARSE_UINT(OUT,KEY) \
488 if (STRICMP(key, (KEY))) \
490 bool _ok = false; \
491 const unsigned int _tmp = val.toUInt(&_ok); \
492 if (_ok) \
494 (OUT) = _tmp; \
495 break; \
499 #define _PARSE_DATE(OUT,KEY) \
500 if (STRICMP(key, (KEY))) \
502 const QDate _tmp = QDate::fromString(val, Qt::ISODate); \
503 if (_tmp.isValid()) \
505 (OUT) = _tmp; \
506 break; \
510 bool MUtils::UpdateChecker::parseVersionInfo(const QString &file, UpdateCheckerInfo *const updateInfo)
512 updateInfo->resetInfo();
514 QFile data(file);
515 if(!data.open(QIODevice::ReadOnly))
517 qWarning("Cannot open update info file for reading!");
518 return false;
521 QDate updateInfoDate;
522 int sectionId = 0;
523 QRegExp regex_sec("^\\[(.+)\\]$"), regex_val("^([^=]+)=(.+)$");
525 while(!data.atEnd())
527 QString line = QString::fromLatin1(data.readLine()).simplified();
528 if (regex_sec.indexIn(line) >= 0)
530 sectionId = 0; /*unknown section*/
531 const QString name = regex_sec.cap(1).trimmed();
532 log(QString("Sec: [%1]").arg(name));
533 _CHECK_HEADER(1, GLOBALHEADER_ID)
534 _CHECK_HEADER(2, m_applicationId)
535 continue;
537 if (regex_val.indexIn(line) >= 0)
539 const QString key = regex_val.cap(1).trimmed();
540 const QString val = regex_val.cap(2).trimmed();
541 log(QString("Val: \"%1\" = \"%2\"").arg(key, val));
542 switch (sectionId)
544 case 1:
545 _PARSE_DATE(updateInfoDate, "TimestampCreated")
546 break;
547 case 2:
548 _PARSE_UINT(updateInfo->m_buildNo, "BuildNo")
549 _PARSE_DATE(updateInfo->m_buildDate, "BuildDate")
550 _PARSE_TEXT(updateInfo->m_downloadSite, "DownloadSite")
551 _PARSE_TEXT(updateInfo->m_downloadAddress, "DownloadAddress")
552 _PARSE_TEXT(updateInfo->m_downloadFilename, "DownloadFilename")
553 _PARSE_TEXT(updateInfo->m_downloadFilecode, "DownloadFilecode")
554 _PARSE_TEXT(updateInfo->m_downloadChecksum, "DownloadChecksum")
555 break;
560 if (!updateInfo->isComplete())
562 log("", "WARNING: Update information is incomplete!");
563 goto failure;
566 if(updateInfoDate.isValid())
568 const QDate expiredDate = updateInfoDate.addMonths(VERSION_INFO_EXPIRES_MONTHS);
569 if (expiredDate < OS::current_date())
571 log("", QString("WARNING: Update information has expired at %1!").arg(expiredDate.toString(Qt::ISODate)));
572 goto failure;
575 else
577 log("", "WARNING: Timestamp is missing from update information header!");
578 goto failure;
581 log("", "Success: Update information is complete.");
582 return true; /*success*/
584 failure:
585 updateInfo->resetInfo();
586 return false;
589 //----------------------------------------------------------
590 // EXTERNAL TOOLS
591 //----------------------------------------------------------
593 bool MUtils::UpdateChecker::getFile(const QUrl &url, const QString &outFile, const unsigned int maxRedir)
595 QFileInfo output(outFile);
596 output.setCaching(false);
598 if (output.exists())
600 QFile::remove(output.canonicalFilePath());
601 if (output.exists())
603 qWarning("Existing output file could not be found!");
604 return false;
608 QStringList args(QLatin1String("-vsSNqfL"));
609 args << "-m" << QString::number(DOWNLOAD_TIMEOUT / 1000);
610 args << "--max-redirs" << QString::number(maxRedir);
611 args << "-A" << USER_AGENT_STR;
612 args << "-e" << QString("%1://%2/;auto").arg(url.scheme(), url.host());
613 args << "-o" << output.fileName() << url.toString();
615 return execCurl(args, output.absolutePath(), DOWNLOAD_TIMEOUT);
618 bool MUtils::UpdateChecker::tryContactHost(const QString &hostname, const int &timeoutMsec)
620 log(QString("Connecting to host: %1").arg(hostname), "");
622 QStringList args(QLatin1String("-vsSNqkI"));
623 args << "-m" << QString::number(qMax(1, timeoutMsec / 1000));
624 args << "-A" << USER_AGENT_STR;
625 args << "-o" << OS::null_device() << QString("http://%1/").arg(hostname);
627 return execCurl(args, temp_folder(), timeoutMsec);
630 bool MUtils::UpdateChecker::checkSignature(const QString &file, const QString &signature)
632 if (QFileInfo(file).absolutePath().compare(QFileInfo(signature).absolutePath(), Qt::CaseInsensitive) != 0)
634 qWarning("CheckSignature: File and signature should be in same folder!");
635 return false;
638 QStringList args;
639 args << QDir::toNativeSeparators(file);
640 args << QDir::toNativeSeparators(signature);
642 const int exitCode = execProcess(m_binaryVerify, args, QFileInfo(file).absolutePath(), DOWNLOAD_TIMEOUT);
643 if (exitCode != INT_MAX)
645 log(QString().sprintf("Exited with code %d", exitCode));
648 return (exitCode == 0); /*completed*/
651 bool MUtils::UpdateChecker::execCurl(const QStringList &args, const QString &workingDir, const int timeout)
653 const int exitCode = execProcess(m_binaryCurl, args, workingDir, timeout + (timeout / 2));
654 if (exitCode != INT_MAX)
656 switch (exitCode)
658 case 0: log(QLatin1String("DONE: Transfer completed successfully."), ""); break;
659 case 6: log(QLatin1String("ERROR: Remote host could not be resolved!"), ""); break;
660 case 7: log(QLatin1String("ERROR: Connection to remote host could not be established!"), ""); break;
661 case 22: log(QLatin1String("ERROR: Requested URL was not found or returned an error!"), ""); break;
662 case 28: log(QLatin1String("ERROR: Operation timed out !!!"), ""); break;
663 default: log(QString().sprintf("ERROR: Terminated with unknown code %d", exitCode), ""); break;
667 return (exitCode == 0); /*completed*/
670 int MUtils::UpdateChecker::execProcess(const QString &programFile, const QStringList &args, const QString &workingDir, const int timeout)
672 QProcess process;
673 init_process(process, workingDir, true, NULL, m_environment.data());
675 QEventLoop loop;
676 connect(&process, SIGNAL(error(QProcess::ProcessError)), &loop, SLOT(quit()));
677 connect(&process, SIGNAL(finished(int, QProcess::ExitStatus)), &loop, SLOT(quit()));
678 connect(&process, SIGNAL(readyRead()), &loop, SLOT(quit()));
680 QTimer timer;
681 timer.setSingleShot(true);
682 connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit()));
684 process.start(programFile, args);
685 if (!process.waitForStarted())
687 log("PROCESS FAILED TO START !!!", "");
688 qWarning("WARNING: %s process could not be created!", MUTILS_UTF8(QFileInfo(programFile).fileName()));
689 return INT_MAX; /*failed to start*/
692 bool bAborted = false;
693 timer.start(qMax(timeout, 1500));
695 while (process.state() != QProcess::NotRunning)
697 loop.exec();
698 while (process.canReadLine())
700 const QString line = QString::fromLatin1(process.readLine()).simplified();
701 if (line.length() > 1)
703 log(line);
706 const bool bCancelled = MUTILS_BOOLIFY(m_cancelled);
707 if (bAborted = (bCancelled || ((!timer.isActive()) && (!process.waitForFinished(125)))))
709 log(bCancelled ? "CANCELLED BY USER !!!" : "PROCESS TIMEOUT !!!", "");
710 qWarning("WARNING: %s process %s!", MUTILS_UTF8(QFileInfo(programFile).fileName()), bCancelled ? "cancelled" : "timed out");
711 break; /*abort process*/
715 timer.stop();
716 timer.disconnect(&timer, SIGNAL(timeout()), &loop, SLOT(quit()));
718 if (bAborted)
720 process.kill();
721 process.waitForFinished(-1);
724 while (process.canReadLine())
726 const QString line = QString::fromLatin1(process.readLine()).simplified();
727 if (line.length() > 1)
729 log(line);
733 return bAborted ? INT_MAX : process.exitCode();
736 ////////////////////////////////////////////////////////////
737 // SLOTS
738 ////////////////////////////////////////////////////////////
740 /*NONE*/
742 ////////////////////////////////////////////////////////////
743 // EVENTS
744 ////////////////////////////////////////////////////////////
746 /*NONE*/