1 ///////////////////////////////////////////////////////////////////////////////
2 // MuldeR's Utilities for Qt
3 // Copyright (C) 2004-2018 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>
41 ///////////////////////////////////////////////////////////////////////////////
43 ///////////////////////////////////////////////////////////////////////////////
45 static const char *HEADER_ID
= "!Update";
47 static const char *MIRROR_URL_POSTFIX
[] =
54 static const int MIN_CONNSCORE
= 5;
55 static const int QUICK_MIRRORS
= 3;
56 static const int MAX_CONN_TIMEOUT
= 16000;
57 static const int DOWNLOAD_TIMEOUT
= 30000;
59 static const int VERSION_INFO_EXPIRES_MONTHS
= 6;
60 static char *USER_AGENT_STR
= "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:52.0) Gecko/20100101 Firefox/52.0"; /*use something innocuous*/
62 #define CHECK_CANCELLED() do \
64 if(MUTILS_BOOLIFY(m_cancelled)) \
66 m_success.fetchAndStoreOrdered(0); \
67 log("", "Update check has been cancelled by user!"); \
68 setProgress(m_maxProgress); \
69 setStatus(UpdateStatus_CancelledByUser); \
75 #define LOG_MESSAGE_HELPER(X) do \
79 emit messageLogged((X)); \
84 #define STRICMP(X,Y) ((X).compare((Y), Qt::CaseInsensitive) == 0)
86 ////////////////////////////////////////////////////////////
88 ////////////////////////////////////////////////////////////
90 static QStringList
buildRandomList(const char *const values
[])
93 for (int index
= 0; values
[index
]; index
++)
95 const int pos
= MUtils::next_rand_u32() % (index
+ 1);
96 list
.insert(pos
, QString::fromLatin1(values
[index
]));
101 static const QHash
<QString
, QString
> *initEnvVars(void)
103 QHash
<QString
, QString
> *const environment
= new QHash
<QString
, QString
>();
104 environment
->insert(QLatin1String("CURL_HOME"), QDir::toNativeSeparators(MUtils::temp_folder()));
108 ////////////////////////////////////////////////////////////
110 ////////////////////////////////////////////////////////////
112 MUtils::UpdateCheckerInfo::UpdateCheckerInfo(void)
117 void MUtils::UpdateCheckerInfo::resetInfo(void)
120 m_buildDate
.setDate(1900, 1, 1);
121 m_downloadSite
.clear();
122 m_downloadAddress
.clear();
123 m_downloadFilename
.clear();
124 m_downloadFilecode
.clear();
125 m_downloadChecksum
.clear();
128 bool MUtils::UpdateCheckerInfo::isComplete(void)
130 return (this->m_buildNo
> 0) &&
131 (this->m_buildDate
.year() >= 2010) &&
132 (!this->m_downloadSite
.isEmpty()) &&
133 (!this->m_downloadAddress
.isEmpty()) &&
134 (!this->m_downloadFilename
.isEmpty()) &&
135 (!this->m_downloadFilecode
.isEmpty()) &&
136 (!this->m_downloadChecksum
.isEmpty());
139 ////////////////////////////////////////////////////////////
140 // Constructor & Destructor
141 ////////////////////////////////////////////////////////////
143 MUtils::UpdateChecker::UpdateChecker(const QString
&binCurl
, const QString
&binGnuPG
, const QString
&binKeys
, const QString
&applicationId
, const quint32
&installedBuildNo
, const bool betaUpdates
, const bool testMode
)
145 m_updateInfo(new UpdateCheckerInfo()),
146 m_binaryCurl(binCurl
),
147 m_binaryGnuPG(binGnuPG
),
148 m_binaryKeys(binKeys
),
149 m_applicationId(applicationId
),
150 m_installedBuildNo(installedBuildNo
),
151 m_betaUpdates(betaUpdates
),
152 m_testMode(testMode
),
153 m_maxProgress(MIN_CONNSCORE
+ 5),
154 m_environment(initEnvVars())
156 m_status
= UpdateStatus_NotStartedYet
;
159 if(m_binaryCurl
.isEmpty() || m_binaryGnuPG
.isEmpty() || m_binaryKeys
.isEmpty())
161 MUTILS_THROW("Tools not initialized correctly!");
165 MUtils::UpdateChecker::~UpdateChecker(void)
169 ////////////////////////////////////////////////////////////
171 ////////////////////////////////////////////////////////////
173 void MUtils::UpdateChecker::start(Priority priority
)
175 m_success
.fetchAndStoreOrdered(0);
176 m_cancelled
.fetchAndStoreOrdered(0);
177 QThread::start(priority
);
180 ////////////////////////////////////////////////////////////
181 // Protected functions
182 ////////////////////////////////////////////////////////////
184 void MUtils::UpdateChecker::run(void)
186 qDebug("Update checker thread started!");
187 MUTILS_EXCEPTION_HANDLER(m_testMode
? testMirrorsList() : checkForUpdates());
188 qDebug("Update checker thread completed.");
191 void MUtils::UpdateChecker::checkForUpdates(void)
193 // ----- Initialization ----- //
195 m_updateInfo
->resetInfo();
198 // ----- Test Internet Connection ----- //
200 log("Checking your Internet connection...", "");
201 setStatus(UpdateStatus_CheckingConnection
);
203 const int networkStatus
= OS::network_status();
204 if(networkStatus
== OS::NETWORK_TYPE_NON
)
206 if (!MUtils::OS::arguments().contains("ignore-network-status"))
208 log("Operating system reports that the computer is currently offline !!!");
209 setProgress(m_maxProgress
);
210 setStatus(UpdateStatus_ErrorNoConnection
);
218 // ----- Test Known Hosts Connectivity ----- //
220 int connectionScore
= 0;
221 QStringList mirrorList
= buildRandomList(known_hosts
);
223 for(int connectionTimout
= 1000; connectionTimout
<= MAX_CONN_TIMEOUT
; connectionTimout
*= 2)
225 QElapsedTimer elapsedTimer
;
226 elapsedTimer
.start();
227 const int globalTimout
= 2 * MIN_CONNSCORE
* connectionTimout
;
228 while (!elapsedTimer
.hasExpired(globalTimout
))
230 const QString hostName
= mirrorList
.takeFirst();
231 if (tryContactHost(hostName
, connectionTimout
))
233 setProgress(1 + (connectionScore
+= 1));
234 elapsedTimer
.restart();
235 if (connectionScore
>= MIN_CONNSCORE
)
237 goto endLoop
; /*success*/
242 mirrorList
.append(hostName
); /*re-schedule*/
250 if(connectionScore
< MIN_CONNSCORE
)
252 log("", "Connectivity test has failed: Internet connection appears to be broken!");
253 setProgress(m_maxProgress
);
254 setStatus(UpdateStatus_ErrorConnectionTestFailed
);
258 // ----- Fetch Update Info From Server ----- //
260 log("----", "", "Internet connection is operational, checking for updates online...");
261 setStatus(UpdateStatus_FetchingUpdates
);
264 mirrorList
= buildRandomList(update_mirrors
);
266 while(!mirrorList
.isEmpty())
268 const QString currentMirror
= mirrorList
.takeFirst();
269 const bool isQuick
= (mirrorCount
++ < QUICK_MIRRORS
);
270 if(tryUpdateMirror(m_updateInfo
.data(), currentMirror
, isQuick
))
272 m_success
.ref(); /*success*/
277 mirrorList
.append(currentMirror
); /*re-schedule*/
284 setProgress(MIN_CONNSCORE
+ 5);
286 // ----- Generate final result ----- //
288 if(MUTILS_BOOLIFY(m_success
))
290 if(m_updateInfo
->m_buildNo
> m_installedBuildNo
)
292 setStatus(UpdateStatus_CompletedUpdateAvailable
);
294 else if(m_updateInfo
->m_buildNo
== m_installedBuildNo
)
296 setStatus(UpdateStatus_CompletedNoUpdates
);
300 setStatus(UpdateStatus_CompletedNewVersionOlder
);
305 setStatus(UpdateStatus_ErrorFetchUpdateInfo
);
309 void MUtils::UpdateChecker::testMirrorsList(void)
311 QStringList mirrorList
;
312 for(int i
= 0; update_mirrors
[i
]; i
++)
314 mirrorList
<< QString::fromLatin1(update_mirrors
[i
]);
317 // ----- Test update mirrors ----- //
319 qDebug("\n[Mirror Sites]");
320 log("Testing all known mirror sites...", "", "---");
322 UpdateCheckerInfo updateInfo
;
323 while (!mirrorList
.isEmpty())
325 const QString currentMirror
= mirrorList
.takeFirst();
326 bool success
= false;
327 qDebug("Testing: %s", MUTILS_L1STR(currentMirror
));
328 log("", "Testing:", currentMirror
, "");
329 for (quint8 attempt
= 0; attempt
< 3; ++attempt
)
331 updateInfo
.resetInfo();
332 if (tryUpdateMirror(&updateInfo
, currentMirror
, (!attempt
)))
340 qWarning("\nUpdate mirror seems to be unavailable:\n%s\n", MUTILS_L1STR(currentMirror
));
345 // ----- Test known hosts ----- //
347 QStringList knownHostList
;
348 for (int i
= 0; known_hosts
[i
]; i
++)
350 knownHostList
<< QString::fromLatin1(known_hosts
[i
]);
353 qDebug("\n[Known Hosts]");
354 log("Testing all known hosts...", "", "---");
356 while(!knownHostList
.isEmpty())
358 const QString currentHost
= knownHostList
.takeFirst();
359 qDebug("Testing: %s", MUTILS_L1STR(currentHost
));
360 log(QLatin1String(""), "Testing:", currentHost
, "");
361 if (!tryContactHost(currentHost
, DOWNLOAD_TIMEOUT
))
363 qWarning("\nConnectivity test FAILED on the following host:\n%s\n", MUTILS_L1STR(currentHost
));
369 ////////////////////////////////////////////////////////////
371 ////////////////////////////////////////////////////////////
373 void MUtils::UpdateChecker::setStatus(const int status
)
375 if(m_status
!= status
)
378 emit
statusChanged(status
);
382 void MUtils::UpdateChecker::setProgress(const int progress
)
384 const int value
= qBound(0, progress
, m_maxProgress
);
385 if(m_progress
!= value
)
387 emit
progressChanged(m_progress
= value
);
391 void MUtils::UpdateChecker::log(const QString
&str1
, const QString
&str2
, const QString
&str3
, const QString
&str4
)
393 LOG_MESSAGE_HELPER(str1
);
394 LOG_MESSAGE_HELPER(str2
);
395 LOG_MESSAGE_HELPER(str3
);
396 LOG_MESSAGE_HELPER(str4
);
399 bool MUtils::UpdateChecker::tryUpdateMirror(UpdateCheckerInfo
*updateInfo
, const QString
&url
, const bool &quick
)
401 bool success
= false;
402 log("", "Trying update mirror:", url
, "");
406 setProgress(MIN_CONNSCORE
+ 1);
407 if (!tryContactHost(QUrl(url
).host(), (MAX_CONN_TIMEOUT
/ 8)))
409 log("", "Mirror is too slow, skipping!");
414 const QString randPart
= next_rand_str();
415 const QString outFileVers
= QString("%1/%2.ver").arg(temp_folder(), randPart
);
416 const QString outFileSign
= QString("%1/%2.sig").arg(temp_folder(), randPart
);
418 if (!getUpdateInfo(url
, outFileVers
, outFileSign
))
420 log("", "Oops: Download of update information has failed!");
424 log("Download completed, verifying signature:", "");
425 setProgress(MIN_CONNSCORE
+ 4);
426 if (!checkSignature(outFileVers
, outFileSign
))
428 log("", "Bad signature detected, take care !!!");
432 log("", "Signature is valid, parsing update information:", "");
433 success
= parseVersionInfo(outFileVers
, updateInfo
);
436 QFile::remove(outFileVers
);
437 QFile::remove(outFileSign
);
441 bool MUtils::UpdateChecker::getUpdateInfo(const QString
&url
, const QString
&outFileVers
, const QString
&outFileSign
)
443 log("Downloading update information:", "");
444 setProgress(MIN_CONNSCORE
+ 2);
445 if(getFile(QUrl(QString("%1%2").arg(url
, MIRROR_URL_POSTFIX
[m_betaUpdates
? 1 : 0])), outFileVers
))
449 log( "Downloading signature file:", "");
450 setProgress(MIN_CONNSCORE
+ 3);
451 if (getFile(QUrl(QString("%1%2.sig2").arg(url
, MIRROR_URL_POSTFIX
[m_betaUpdates
? 1 : 0])), outFileSign
))
453 return true; /*completed*/
460 //----------------------------------------------------------
462 //----------------------------------------------------------
464 bool MUtils::UpdateChecker::parseVersionInfo(const QString
&file
, UpdateCheckerInfo
*const updateInfo
)
466 updateInfo
->resetInfo();
469 if(!data
.open(QIODevice::ReadOnly
))
471 qWarning("Cannot open update info file for reading!");
475 QDate updateInfoDate
;
477 QRegExp
regex_sec("^\\[(.+)\\]$"), regex_val("^([^=]+)=(.+)$");
481 QString line
= QString::fromLatin1(data
.readLine()).trimmed();
482 if (regex_sec
.indexIn(line
) >= 0)
484 sectionId
= parseSectionHeaderStr(regex_sec
.cap(1).trimmed());
487 if (regex_val
.indexIn(line
) >= 0)
489 const QString key
= regex_val
.cap(1).trimmed();
490 const QString val
= regex_val
.cap(2).trimmed();
494 parseHeaderValue(key
, val
, updateInfoDate
);
497 parseUpdateInfoValue(key
, val
, updateInfo
);
503 if(updateInfoDate
.isValid())
505 const QDate expiredDate
= updateInfoDate
.addMonths(VERSION_INFO_EXPIRES_MONTHS
);
506 if (expiredDate
< OS::current_date())
508 log(QString("WARNING: Update information has expired at %1!").arg(expiredDate
.toString(Qt::ISODate
)));
514 log("WARNING: Timestamp is missing from update information header!");
518 if(!updateInfo
->isComplete())
520 log("WARNING: Update information is incomplete!");
524 log("", "Success: Update information is complete.");
525 return true; /*success*/
528 updateInfo
->resetInfo();
532 int MUtils::UpdateChecker::parseSectionHeaderStr(const QString
&name
)
534 log(QString("Sec: [%1]").arg(name
));
536 if (STRICMP(name
, HEADER_ID
))
540 if (STRICMP(name
, m_applicationId
))
545 //Unknonw section encountered!
549 void MUtils::UpdateChecker::parseHeaderValue(const QString
&key
, const QString
&val
, QDate
&updateInfoDate
)
551 log(QString("Hdr: \"%1\"=\"%2\"").arg(key
, val
));
553 if (STRICMP(key
, "TimestampCreated"))
555 const QDate temp
= QDate::fromString(val
, Qt::ISODate
);
558 updateInfoDate
= temp
;
563 //Unknown entry encountered!
564 qWarning("Unknown header value: %s", MUTILS_L1STR(key
));
567 void MUtils::UpdateChecker::parseUpdateInfoValue(const QString
&key
, const QString
&val
, UpdateCheckerInfo
*const updateInfo
)
569 log(QString("Val: \"%1\"=\"%2\"").arg(key
, val
));
571 if (STRICMP(key
, "BuildNo"))
574 const unsigned int temp
= val
.toUInt(&ok
);
577 updateInfo
->m_buildNo
= temp
;
581 if (STRICMP(key
, "BuildDate"))
583 const QDate temp
= QDate::fromString(val
, Qt::ISODate
);
586 updateInfo
->m_buildDate
= temp
;
590 if (STRICMP(key
, "DownloadSite"))
592 updateInfo
->m_downloadSite
= val
;
595 if (STRICMP(key
, "DownloadAddress"))
597 updateInfo
->m_downloadAddress
= val
;
600 if (STRICMP(key
, "DownloadFilename"))
602 updateInfo
->m_downloadFilename
= val
;
605 if (STRICMP(key
, "DownloadFilecode"))
607 updateInfo
->m_downloadFilecode
= val
;
610 if (STRICMP(key
, "DownloadChecksum"))
612 updateInfo
->m_downloadChecksum
= val
;
616 //Unknown entry encountered!
617 qWarning("Unknown update value: %s", MUTILS_L1STR(key
));
620 //----------------------------------------------------------
622 //----------------------------------------------------------
624 bool MUtils::UpdateChecker::getFile(const QUrl
&url
, const QString
&outFile
, const unsigned int maxRedir
)
626 QFileInfo
output(outFile
);
627 output
.setCaching(false);
631 QFile::remove(output
.canonicalFilePath());
634 qWarning("Existing output file could not be found!");
639 QStringList
args(QLatin1String("-vsSNqkfL"));
640 args
<< "-m" << QString::number(DOWNLOAD_TIMEOUT
/ 1000);
641 args
<< "--max-redirs" << QString::number(maxRedir
);
642 args
<< "-A" << USER_AGENT_STR
;
643 args
<< "-e" << QString("%1://%2/;auto").arg(url
.scheme(), url
.host());
644 args
<< "-o" << output
.fileName() << url
.toString();
646 return execCurl(args
, output
.absolutePath(), DOWNLOAD_TIMEOUT
);
649 bool MUtils::UpdateChecker::tryContactHost(const QString
&hostname
, const int &timeoutMsec
)
651 log(QString("Connecting to host: %1").arg(hostname
), "");
653 QStringList
args(QLatin1String("-vsSNqkI"));
654 args
<< "-m" << QString::number(qMax(1, timeoutMsec
/ 1000));
655 args
<< "-A" << USER_AGENT_STR
;
656 args
<< "-o" << OS::null_device() << QString("http://%1/").arg(hostname
);
658 return execCurl(args
, temp_folder(), timeoutMsec
);
661 bool MUtils::UpdateChecker::checkSignature(const QString
&file
, const QString
&signature
)
663 if (QFileInfo(file
).absolutePath().compare(QFileInfo(signature
).absolutePath(), Qt::CaseInsensitive
) != 0)
665 qWarning("CheckSignature: File and signature should be in same folder!");
669 QString
keyRingPath(m_binaryKeys
);
670 bool removeKeyring
= false;
671 if (QFileInfo(file
).absolutePath().compare(QFileInfo(m_binaryKeys
).absolutePath(), Qt::CaseInsensitive
) != 0)
673 keyRingPath
= make_temp_file(QFileInfo(file
).absolutePath(), "gpg");
674 removeKeyring
= true;
675 if (!QFile::copy(m_binaryKeys
, keyRingPath
))
677 qWarning("CheckSignature: Failed to copy the key-ring file!");
683 args
<< QStringList() << "--homedir" << ".";
684 args
<< "--keyring" << QFileInfo(keyRingPath
).fileName();
685 args
<< QFileInfo(signature
).fileName();
686 args
<< QFileInfo(file
).fileName();
688 const int exitCode
= execProcess(m_binaryGnuPG
, args
, QFileInfo(file
).absolutePath(), DOWNLOAD_TIMEOUT
);
689 if (exitCode
!= INT_MAX
)
691 log(QString().sprintf("Exited with code %d", exitCode
));
696 remove_file(keyRingPath
);
699 return (exitCode
== 0); /*completed*/
702 bool MUtils::UpdateChecker::execCurl(const QStringList
&args
, const QString
&workingDir
, const int timeout
)
704 const int exitCode
= execProcess(m_binaryCurl
, args
, workingDir
, timeout
+ (timeout
/ 2));
705 if (exitCode
!= INT_MAX
)
710 case 0: log(QLatin1String("DONE: Transfer completed successfully."), ""); break;
711 case 6: log(QLatin1String("ERROR: Remote host could not be resolved!"), ""); break;
712 case 7: log(QLatin1String("ERROR: Connection to remote host could not be established!"), ""); break;
713 case 22: log(QLatin1String("ERROR: Requested URL was not found or returned an error!"), ""); break;
714 case 28: log(QLatin1String("ERROR: Operation timed out !!!"), ""); break;
715 default: log(QString().sprintf("ERROR: Terminated with unknown code %d", exitCode
), ""); break;
719 return (exitCode
== 0); /*completed*/
722 int MUtils::UpdateChecker::execProcess(const QString
&programFile
, const QStringList
&args
, const QString
&workingDir
, const int timeout
)
725 init_process(process
, workingDir
, true, NULL
, m_environment
.data());
728 connect(&process
, SIGNAL(error(QProcess::ProcessError
)), &loop
, SLOT(quit()));
729 connect(&process
, SIGNAL(finished(int, QProcess::ExitStatus
)), &loop
, SLOT(quit()));
730 connect(&process
, SIGNAL(readyRead()), &loop
, SLOT(quit()));
733 timer
.setSingleShot(true);
734 connect(&timer
, SIGNAL(timeout()), &loop
, SLOT(quit()));
736 process
.start(programFile
, args
);
737 if (!process
.waitForStarted())
739 log("PROCESS FAILED TO START !!!", "");
740 qWarning("WARNING: %s process could not be created!", MUTILS_UTF8(QFileInfo(programFile
).fileName()));
741 return INT_MAX
; /*failed to start*/
744 bool bAborted
= false;
745 timer
.start(qMax(timeout
, 1500));
747 while (process
.state() != QProcess::NotRunning
)
750 while (process
.canReadLine())
752 const QString line
= QString::fromLatin1(process
.readLine()).simplified();
753 if (line
.length() > 1)
758 const bool bCancelled
= MUTILS_BOOLIFY(m_cancelled
);
759 if (bAborted
= (bCancelled
|| ((!timer
.isActive()) && (!process
.waitForFinished(125)))))
761 log(bCancelled
? "CANCELLED BY USER !!!" : "PROCESS TIMEOUT !!!", "");
762 qWarning("WARNING: %s process %s!", MUTILS_UTF8(QFileInfo(programFile
).fileName()), bCancelled
? "cancelled" : "timed out");
763 break; /*abort process*/
768 timer
.disconnect(&timer
, SIGNAL(timeout()), &loop
, SLOT(quit()));
773 process
.waitForFinished(-1);
776 while (process
.canReadLine())
778 const QString line
= QString::fromLatin1(process
.readLine()).simplified();
779 if (line
.length() > 1)
785 return bAborted
? INT_MAX
: process
.exitCode();
788 ////////////////////////////////////////////////////////////
790 ////////////////////////////////////////////////////////////
794 ////////////////////////////////////////////////////////////
796 ////////////////////////////////////////////////////////////