1 ///////////////////////////////////////////////////////////////////////////////
2 // MuldeR's Utilities for Qt
3 // Copyright (C) 2004-2019 LoRd_MuldeR <MuldeR2@GMX.de>
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.
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>
35 #include <QElapsedTimer>
42 ///////////////////////////////////////////////////////////////////////////////
44 ///////////////////////////////////////////////////////////////////////////////
46 static const char *GLOBALHEADER_ID
= "!Update";
48 static const char *MIRROR_URL_POSTFIX
[] =
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 ////////////////////////////////////////////////////////////
65 ////////////////////////////////////////////////////////////
67 #define CHECK_CANCELLED() do \
69 if(MUTILS_BOOLIFY(m_cancelled)) \
71 m_success.fetchAndStoreOrdered(0); \
72 log("", "Update check has been cancelled by user!"); \
73 setProgress(m_maxProgress); \
74 setStatus(UpdateStatus_CancelledByUser); \
80 #define LOG_MESSAGE_HELPER(X) do \
84 emit messageLogged((X)); \
89 #define STRICMP(X,Y) ((X).compare((Y), Qt::CaseInsensitive) == 0)
91 ////////////////////////////////////////////////////////////
93 ////////////////////////////////////////////////////////////
95 static QQueue
<QString
> buildRandomList(const char *const *values
)
100 list
.insert(MUtils::next_rand_u32(list
.size() + 1), QString::fromLatin1(*(values
++)));
105 static const QHash
<QString
, QString
> *initEnvVars(void)
107 const QString tempfolder
= QDir::toNativeSeparators(MUtils::temp_folder());
108 QHash
<QString
, QString
> *const environment
= new QHash
<QString
, QString
>();
109 environment
->insert(QLatin1String("CURL_HOME"), tempfolder
);
110 environment
->insert(QLatin1String("GNUPGHOME"), tempfolder
);
114 ////////////////////////////////////////////////////////////
116 ////////////////////////////////////////////////////////////
118 MUtils::UpdateCheckerInfo::UpdateCheckerInfo(void)
123 void MUtils::UpdateCheckerInfo::resetInfo(void)
126 m_buildDate
.setDate(1900, 1, 1);
127 m_downloadSite
.clear();
128 m_downloadAddress
.clear();
129 m_downloadFilename
.clear();
130 m_downloadFilecode
.clear();
131 m_downloadChecksum
.clear();
134 bool MUtils::UpdateCheckerInfo::isComplete(void)
136 return (this->m_buildNo
> 0) &&
137 (this->m_buildDate
.year() >= 2010) &&
138 (!this->m_downloadSite
.isEmpty()) &&
139 (!this->m_downloadAddress
.isEmpty()) &&
140 (!this->m_downloadFilename
.isEmpty()) &&
141 (!this->m_downloadFilecode
.isEmpty()) &&
142 (!this->m_downloadChecksum
.isEmpty());
145 ////////////////////////////////////////////////////////////
146 // Constructor & Destructor
147 ////////////////////////////////////////////////////////////
149 MUtils::UpdateChecker::UpdateChecker(const QString
&binCurl
, const QString
&binGnuPG
, const QString
&binKeys
, const QString
&applicationId
, const quint32
&installedBuildNo
, const bool betaUpdates
, const bool testMode
)
151 m_updateInfo(new UpdateCheckerInfo()),
152 m_binaryCurl(binCurl
),
153 m_binaryGnuPG(binGnuPG
),
154 m_binaryKeys(binKeys
),
155 m_applicationId(applicationId
),
156 m_installedBuildNo(installedBuildNo
),
157 m_betaUpdates(betaUpdates
),
158 m_testMode(testMode
),
159 m_maxProgress(MIN_CONNSCORE
+ 5),
160 m_environment(initEnvVars())
162 m_status
= UpdateStatus_NotStartedYet
;
165 if(m_binaryCurl
.isEmpty() || m_binaryGnuPG
.isEmpty() || m_binaryKeys
.isEmpty())
167 MUTILS_THROW("Tools not initialized correctly!");
171 MUtils::UpdateChecker::~UpdateChecker(void)
175 ////////////////////////////////////////////////////////////
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();
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
);
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*/
248 mirrorList
.enqueue(hostName
);
249 if(elapsedTimer
.hasExpired(globalTimeout
))
251 break; /*timer expired*/
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
);
267 // ----- Fetch Update Info From Server ----- //
269 log("----", "", "Internet connection is operational, checking for updates online...");
270 setStatus(UpdateStatus_FetchingUpdates
);
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*/
286 mirrorList
.append(currentMirror
); /*re-schedule*/
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
);
309 setStatus(UpdateStatus_CompletedNewVersionOlder
);
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
)))
349 qWarning("\nUpdate mirror seems to be unavailable:\n%s\n", MUTILS_L1STR(currentMirror
));
354 // ----- Test known hosts ----- //
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
));
378 ////////////////////////////////////////////////////////////
380 ////////////////////////////////////////////////////////////
382 void MUtils::UpdateChecker::setStatus(const int status
)
384 if(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
, "");
415 setProgress(MIN_CONNSCORE
+ 1);
416 if (!tryContactHost(QUrl(url
).host(), (MAX_CONN_TIMEOUT
/ 8)))
418 log("", "Mirror is too slow, skipping!");
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!");
433 log("Download completed, verifying signature:", "");
434 setProgress(MIN_CONNSCORE
+ 4);
435 if (!checkSignature(outFileVers
, outFileSign
))
437 log("", "Bad signature detected, take care !!!");
441 log("", "Signature is valid, parsing update information:", "");
442 success
= parseVersionInfo(outFileVers
, updateInfo
);
445 QFile::remove(outFileVers
);
446 QFile::remove(outFileSign
);
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
))
458 log( "Downloading signature file:", "");
459 setProgress(MIN_CONNSCORE
+ 3);
460 if (getFile(QUrl(QString("%1%2.sig2").arg(url
, MIRROR_URL_POSTFIX
[m_betaUpdates
? 1 : 0])), outFileSign
))
462 return true; /*completed*/
469 //----------------------------------------------------------
471 //----------------------------------------------------------
473 #define _CHECK_HEADER(ID,NAME) \
474 if (STRICMP(name, (NAME))) \
480 #define _PARSE_TEXT(OUT,KEY) \
481 if (STRICMP(key, (KEY))) \
487 #define _PARSE_UINT(OUT,KEY) \
488 if (STRICMP(key, (KEY))) \
491 const unsigned int _tmp = val.toUInt(&_ok); \
499 #define _PARSE_DATE(OUT,KEY) \
500 if (STRICMP(key, (KEY))) \
502 const QDate _tmp = QDate::fromString(val, Qt::ISODate); \
503 if (_tmp.isValid()) \
510 bool MUtils::UpdateChecker::parseVersionInfo(const QString
&file
, UpdateCheckerInfo
*const updateInfo
)
512 updateInfo
->resetInfo();
515 if(!data
.open(QIODevice::ReadOnly
))
517 qWarning("Cannot open update info file for reading!");
521 QDate updateInfoDate
;
523 QRegExp
regex_sec("^\\[(.+)\\]$"), regex_val("^([^=]+)=(.+)$");
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
)
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
));
545 _PARSE_DATE(updateInfoDate
, "TimestampCreated")
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")
560 if (!updateInfo
->isComplete())
562 log("", "WARNING: Update information is incomplete!");
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
)));
577 log("", "WARNING: Timestamp is missing from update information header!");
581 log("", "Success: Update information is complete.");
582 return true; /*success*/
585 updateInfo
->resetInfo();
589 //----------------------------------------------------------
591 //----------------------------------------------------------
593 bool MUtils::UpdateChecker::getFile(const QUrl
&url
, const QString
&outFile
, const unsigned int maxRedir
)
595 QFileInfo
output(outFile
);
596 output
.setCaching(false);
600 QFile::remove(output
.canonicalFilePath());
603 qWarning("Existing output file could not be found!");
608 QStringList
args(QLatin1String("-vsSNqkfL"));
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!");
638 QString
keyRingPath(m_binaryKeys
);
639 bool removeKeyring
= false;
640 if (QFileInfo(file
).absolutePath().compare(QFileInfo(m_binaryKeys
).absolutePath(), Qt::CaseInsensitive
) != 0)
642 keyRingPath
= make_temp_file(QFileInfo(file
).absolutePath(), "gpg");
643 removeKeyring
= true;
644 if (!QFile::copy(m_binaryKeys
, keyRingPath
))
646 qWarning("CheckSignature: Failed to copy the key-ring file!");
652 args
<< QStringList() << "--homedir" << ".";
653 args
<< "--keyring" << QFileInfo(keyRingPath
).fileName();
654 args
<< QFileInfo(signature
).fileName();
655 args
<< QFileInfo(file
).fileName();
657 const int exitCode
= execProcess(m_binaryGnuPG
, args
, QFileInfo(file
).absolutePath(), DOWNLOAD_TIMEOUT
);
658 if (exitCode
!= INT_MAX
)
660 log(QString().sprintf("Exited with code %d", exitCode
));
665 remove_file(keyRingPath
);
668 return (exitCode
== 0); /*completed*/
671 bool MUtils::UpdateChecker::execCurl(const QStringList
&args
, const QString
&workingDir
, const int timeout
)
673 const int exitCode
= execProcess(m_binaryCurl
, args
, workingDir
, timeout
+ (timeout
/ 2));
674 if (exitCode
!= INT_MAX
)
678 case 0: log(QLatin1String("DONE: Transfer completed successfully."), ""); break;
679 case 6: log(QLatin1String("ERROR: Remote host could not be resolved!"), ""); break;
680 case 7: log(QLatin1String("ERROR: Connection to remote host could not be established!"), ""); break;
681 case 22: log(QLatin1String("ERROR: Requested URL was not found or returned an error!"), ""); break;
682 case 28: log(QLatin1String("ERROR: Operation timed out !!!"), ""); break;
683 default: log(QString().sprintf("ERROR: Terminated with unknown code %d", exitCode
), ""); break;
687 return (exitCode
== 0); /*completed*/
690 int MUtils::UpdateChecker::execProcess(const QString
&programFile
, const QStringList
&args
, const QString
&workingDir
, const int timeout
)
693 init_process(process
, workingDir
, true, NULL
, m_environment
.data());
696 connect(&process
, SIGNAL(error(QProcess::ProcessError
)), &loop
, SLOT(quit()));
697 connect(&process
, SIGNAL(finished(int, QProcess::ExitStatus
)), &loop
, SLOT(quit()));
698 connect(&process
, SIGNAL(readyRead()), &loop
, SLOT(quit()));
701 timer
.setSingleShot(true);
702 connect(&timer
, SIGNAL(timeout()), &loop
, SLOT(quit()));
704 process
.start(programFile
, args
);
705 if (!process
.waitForStarted())
707 log("PROCESS FAILED TO START !!!", "");
708 qWarning("WARNING: %s process could not be created!", MUTILS_UTF8(QFileInfo(programFile
).fileName()));
709 return INT_MAX
; /*failed to start*/
712 bool bAborted
= false;
713 timer
.start(qMax(timeout
, 1500));
715 while (process
.state() != QProcess::NotRunning
)
718 while (process
.canReadLine())
720 const QString line
= QString::fromLatin1(process
.readLine()).simplified();
721 if (line
.length() > 1)
726 const bool bCancelled
= MUTILS_BOOLIFY(m_cancelled
);
727 if (bAborted
= (bCancelled
|| ((!timer
.isActive()) && (!process
.waitForFinished(125)))))
729 log(bCancelled
? "CANCELLED BY USER !!!" : "PROCESS TIMEOUT !!!", "");
730 qWarning("WARNING: %s process %s!", MUTILS_UTF8(QFileInfo(programFile
).fileName()), bCancelled
? "cancelled" : "timed out");
731 break; /*abort process*/
736 timer
.disconnect(&timer
, SIGNAL(timeout()), &loop
, SLOT(quit()));
741 process
.waitForFinished(-1);
744 while (process
.canReadLine())
746 const QString line
= QString::fromLatin1(process
.readLine()).simplified();
747 if (line
.length() > 1)
753 return bAborted
? INT_MAX
: process
.exitCode();
756 ////////////////////////////////////////////////////////////
758 ////////////////////////////////////////////////////////////
762 ////////////////////////////////////////////////////////////
764 ////////////////////////////////////////////////////////////