1 ///////////////////////////////////////////////////////////////////////////////
2 // MuldeR's Utilities for Qt
3 // Copyright (C) 2004-2017 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>
34 #include <QElapsedTimer>
39 ///////////////////////////////////////////////////////////////////////////////
41 ///////////////////////////////////////////////////////////////////////////////
43 static const char *header_id
= "!Update";
45 static const char *mirror_url_postfix
[] =
52 static const int MIN_CONNSCORE
= 5;
53 static const int QUICK_MIRRORS
= 3;
54 static const int MAX_CONN_TIMEOUT
= 16000;
55 static const int DOWNLOAD_TIMEOUT
= 30000;
57 static const int VERSION_INFO_EXPIRES_MONTHS
= 6;
58 static char *USER_AGENT_STR
= "Mozilla/5.0 (X11; Linux i686; rv:10.0) Gecko/20100101 Firefox/10.0"; /*use something innocuous*/
60 #define CHECK_CANCELLED() do \
62 if(MUTILS_BOOLIFY(m_cancelled)) \
64 m_success.fetchAndStoreOrdered(0); \
65 log("", "Update check has been cancelled by user!"); \
66 setProgress(m_maxProgress); \
67 setStatus(UpdateStatus_CancelledByUser); \
73 ////////////////////////////////////////////////////////////
75 ////////////////////////////////////////////////////////////
77 static int getMaxProgress(void)
80 while (update_mirrors
[counter
])
84 counter
+= MIN_CONNSCORE
+ QUICK_MIRRORS
+ 2;
88 static QStringList
buildRandomList(const char *const values
[])
91 for (int index
= 0; values
[index
]; index
++)
93 const int pos
= MUtils::next_rand_u32() % (index
+ 1);
94 list
.insert(pos
, QString::fromLatin1(values
[index
]));
99 ////////////////////////////////////////////////////////////
101 ////////////////////////////////////////////////////////////
103 MUtils::UpdateCheckerInfo::UpdateCheckerInfo(void)
108 void MUtils::UpdateCheckerInfo::resetInfo(void)
111 m_buildDate
.setDate(1900, 1, 1);
112 m_downloadSite
.clear();
113 m_downloadAddress
.clear();
114 m_downloadFilename
.clear();
115 m_downloadFilecode
.clear();
116 m_downloadChecksum
.clear();
119 bool MUtils::UpdateCheckerInfo::isComplete(void)
121 if(this->m_buildNo
< 1) return false;
122 if(this->m_buildDate
.year() < 2010) return false;
123 if(this->m_downloadSite
.isEmpty()) return false;
124 if(this->m_downloadAddress
.isEmpty()) return false;
125 if(this->m_downloadFilename
.isEmpty()) return false;
126 if(this->m_downloadFilecode
.isEmpty()) return false;
127 if(this->m_downloadChecksum
.isEmpty()) return false;
132 ////////////////////////////////////////////////////////////
133 // Constructor & Destructor
134 ////////////////////////////////////////////////////////////
136 MUtils::UpdateChecker::UpdateChecker(const QString
&binWGet
, const QString
&binMCat
, const QString
&binGnuPG
, const QString
&binKeys
, const QString
&applicationId
, const quint32
&installedBuildNo
, const bool betaUpdates
, const bool testMode
)
138 m_updateInfo(new UpdateCheckerInfo()),
139 m_binaryWGet(binWGet
),
140 m_binaryMCat(binMCat
),
141 m_binaryGnuPG(binGnuPG
),
142 m_binaryKeys(binKeys
),
143 m_applicationId(applicationId
),
144 m_installedBuildNo(installedBuildNo
),
145 m_betaUpdates(betaUpdates
),
146 m_testMode(testMode
),
147 m_maxProgress(getMaxProgress())
149 m_status
= UpdateStatus_NotStartedYet
;
152 if(m_binaryWGet
.isEmpty() || m_binaryGnuPG
.isEmpty() || m_binaryKeys
.isEmpty())
154 MUTILS_THROW("Tools not initialized correctly!");
158 MUtils::UpdateChecker::~UpdateChecker(void)
162 ////////////////////////////////////////////////////////////
164 ////////////////////////////////////////////////////////////
166 void MUtils::UpdateChecker::start(Priority priority
)
168 m_success
.fetchAndStoreOrdered(0);
169 m_cancelled
.fetchAndStoreOrdered(0);
170 QThread::start(priority
);
173 ////////////////////////////////////////////////////////////
174 // Protected functions
175 ////////////////////////////////////////////////////////////
177 void MUtils::UpdateChecker::run(void)
179 qDebug("Update checker thread started!");
180 MUTILS_EXCEPTION_HANDLER(m_testMode
? testMirrorsList() : checkForUpdates());
181 qDebug("Update checker thread completed.");
184 void MUtils::UpdateChecker::checkForUpdates(void)
186 // ----- Initialization ----- //
188 m_updateInfo
->resetInfo();
191 // ----- Test Internet Connection ----- //
193 log("Checking internet connection...", "");
194 setStatus(UpdateStatus_CheckingConnection
);
196 const int networkStatus
= OS::network_status();
197 if(networkStatus
== OS::NETWORK_TYPE_NON
)
199 log("Operating system reports that the computer is currently offline !!!");
200 setProgress(m_maxProgress
);
201 setStatus(UpdateStatus_ErrorNoConnection
);
207 // ----- Test Known Hosts Connectivity ----- //
209 int connectionScore
= 0;
210 QStringList mirrorList
= buildRandomList(known_hosts
);
212 for(int connectionTimout
= 500; connectionTimout
<= MAX_CONN_TIMEOUT
; connectionTimout
*= 2)
214 QElapsedTimer elapsedTimer
;
215 elapsedTimer
.start();
216 const int globalTimout
= 2 * MIN_CONNSCORE
* connectionTimout
;
217 while (!elapsedTimer
.hasExpired(globalTimout
))
219 const QString hostName
= mirrorList
.takeFirst();
220 if (tryContactHost(hostName
, connectionTimout
))
222 connectionScore
+= 1;
223 setProgress(qBound(1, connectionScore
+ 1, MIN_CONNSCORE
+ 1));
224 elapsedTimer
.restart();
225 if (connectionScore
>= MIN_CONNSCORE
)
227 goto endLoop
; /*success*/
232 mirrorList
.append(hostName
); /*re-schedule*/
240 if(connectionScore
< MIN_CONNSCORE
)
242 log("", "Connectivity test has failed: Internet connection appears to be broken!");
243 setProgress(m_maxProgress
);
244 setStatus(UpdateStatus_ErrorConnectionTestFailed
);
248 // ----- Fetch Update Info From Server ----- //
250 log("----", "", "Checking for updates online...");
251 setStatus(UpdateStatus_FetchingUpdates
);
254 mirrorList
= buildRandomList(update_mirrors
);
256 while(!mirrorList
.isEmpty())
258 setProgress(m_progress
+ 1);
259 const QString currentMirror
= mirrorList
.takeFirst();
260 const bool isQuick
= (mirrorCount
++ < QUICK_MIRRORS
);
261 if(tryUpdateMirror(m_updateInfo
.data(), currentMirror
, isQuick
))
263 m_success
.ref(); /*success*/
268 mirrorList
.append(currentMirror
); /*re-schedule*/
274 while (m_progress
< m_maxProgress
)
277 setProgress(m_progress
+ 1);
281 // ----- Generate final result ----- //
283 if(MUTILS_BOOLIFY(m_success
))
285 if(m_updateInfo
->m_buildNo
> m_installedBuildNo
)
287 setStatus(UpdateStatus_CompletedUpdateAvailable
);
289 else if(m_updateInfo
->m_buildNo
== m_installedBuildNo
)
291 setStatus(UpdateStatus_CompletedNoUpdates
);
295 setStatus(UpdateStatus_CompletedNewVersionOlder
);
300 setStatus(UpdateStatus_ErrorFetchUpdateInfo
);
304 void MUtils::UpdateChecker::testMirrorsList(void)
306 // ----- Test update mirrors ----- //
308 QStringList mirrorList
;
309 for(int i
= 0; update_mirrors
[i
]; i
++)
311 mirrorList
<< QString::fromLatin1(update_mirrors
[i
]);
314 qDebug("\n[Mirror Sites]");
315 log("Testing all known mirror sites...", "", "---");
317 UpdateCheckerInfo updateInfo
;
318 while (!mirrorList
.isEmpty())
320 const QString currentMirror
= mirrorList
.takeFirst();
321 bool success
= false;
322 qDebug("Testing: %s", MUTILS_L1STR(currentMirror
));
323 log("", "Testing:", currentMirror
, "");
324 for (quint8 attempt
= 0; attempt
< 3; ++attempt
)
326 updateInfo
.resetInfo();
327 if (tryUpdateMirror(&updateInfo
, currentMirror
, (!attempt
)))
335 qWarning("\nUpdate mirror seems to be unavailable:\n%s\n", MUTILS_L1STR(currentMirror
));
340 // ----- Test known hosts ----- //
342 QStringList knownHostList
;
343 for (int i
= 0; known_hosts
[i
]; i
++)
345 knownHostList
<< QString::fromLatin1(known_hosts
[i
]);
348 qDebug("\n[Known Hosts]");
349 log("Testing all known hosts...", "", "---");
351 QSet
<quint32
> ipAddrSet
;
353 while(!knownHostList
.isEmpty())
355 const QString currentHost
= knownHostList
.takeFirst();
356 qDebug("Testing: %s", MUTILS_L1STR(currentHost
));
357 log(QLatin1String(""), "Testing:", currentHost
, "");
358 if (tryContactHost(currentHost
, DOWNLOAD_TIMEOUT
, &ipAddr
))
360 if (ipAddrSet
.contains(ipAddr
))
362 qWarning("Duplicate IP-address 0x%08X was encountered!", ipAddr
);
366 ipAddrSet
<< ipAddr
; /*not encountered yet*/
371 qWarning("\nConnectivity test FAILED on the following host:\n%s\n", MUTILS_L1STR(currentHost
));
377 ////////////////////////////////////////////////////////////
379 ////////////////////////////////////////////////////////////
381 void MUtils::UpdateChecker::setStatus(const int status
)
383 if(m_status
!= status
)
386 emit
statusChanged(status
);
390 void MUtils::UpdateChecker::setProgress(const int progress
)
392 if(m_progress
!= progress
)
394 m_progress
= progress
;
395 emit
progressChanged(progress
);
399 void MUtils::UpdateChecker::log(const QString
&str1
, const QString
&str2
, const QString
&str3
, const QString
&str4
)
401 if(!str1
.isNull()) emit
messageLogged(str1
);
402 if(!str2
.isNull()) emit
messageLogged(str2
);
403 if(!str3
.isNull()) emit
messageLogged(str3
);
404 if(!str4
.isNull()) emit
messageLogged(str4
);
407 bool MUtils::UpdateChecker::tryUpdateMirror(UpdateCheckerInfo
*updateInfo
, const QString
&url
, const bool &quick
)
409 bool success
= false;
410 log("", "Trying mirror:", url
, "");
414 if (!tryContactHost(QUrl(url
).host(), (MAX_CONN_TIMEOUT
/ 10)))
416 log("", "Mirror is too slow, skipping!");
421 const QString randPart
= next_rand_str();
422 const QString outFileVers
= QString("%1/%2.ver").arg(temp_folder(), randPart
);
423 const QString outFileSign
= QString("%1/%2.sig").arg(temp_folder(), randPart
);
425 if (getUpdateInfo(url
, outFileVers
, outFileSign
))
427 log("", "Download okay, checking signature:");
428 if (checkSignature(outFileVers
, outFileSign
))
430 log("", "Signature okay, parsing info:", "");
431 success
= parseVersionInfo(outFileVers
, updateInfo
);
435 log("", "Bad signature, take care!");
440 log("", "Download has failed!");
443 QFile::remove(outFileVers
);
444 QFile::remove(outFileSign
);
449 bool MUtils::UpdateChecker::getUpdateInfo(const QString
&url
, const QString
&outFileVers
, const QString
&outFileSign
)
451 log("Downloading update info:", "");
452 if(getFile(QString("%1%2").arg(url
, mirror_url_postfix
[m_betaUpdates
? 1 : 0]), outFileVers
))
456 log("", "Downloading signature:", "");
457 if (getFile(QString("%1%2.sig2").arg(url
, mirror_url_postfix
[m_betaUpdates
? 1 : 0]), outFileSign
))
466 bool MUtils::UpdateChecker::parseVersionInfo(const QString
&file
, UpdateCheckerInfo
*updateInfo
)
468 QRegExp
value("^(\\w+)=(.+)$");
469 QRegExp
section("^\\[(.+)\\]$");
471 QDate updateInfoDate
;
472 updateInfo
->resetInfo();
475 if(!data
.open(QIODevice::ReadOnly
))
477 qWarning("Cannot open update info file for reading!");
486 QString line
= QString::fromLatin1(data
.readLine()).trimmed();
487 if(section
.indexIn(line
) >= 0)
489 log(QString("Sec: [%1]").arg(section
.cap(1)));
490 inSec
= (section
.cap(1).compare(m_applicationId
, Qt::CaseInsensitive
) == 0);
491 inHdr
= (section
.cap(1).compare(QString::fromLatin1(header_id
), Qt::CaseInsensitive
) == 0);
494 if(inSec
&& (value
.indexIn(line
) >= 0))
496 log(QString("Val: '%1' ==> '%2").arg(value
.cap(1), value
.cap(2)));
497 if(value
.cap(1).compare("BuildNo", Qt::CaseInsensitive
) == 0)
500 const unsigned int temp
= value
.cap(2).toUInt(&ok
);
501 if(ok
) updateInfo
->m_buildNo
= temp
;
503 else if(value
.cap(1).compare("BuildDate", Qt::CaseInsensitive
) == 0)
505 const QDate temp
= QDate::fromString(value
.cap(2).trimmed(), Qt::ISODate
);
506 if(temp
.isValid()) updateInfo
->m_buildDate
= temp
;
508 else if(value
.cap(1).compare("DownloadSite", Qt::CaseInsensitive
) == 0)
510 updateInfo
->m_downloadSite
= value
.cap(2).trimmed();
512 else if(value
.cap(1).compare("DownloadAddress", Qt::CaseInsensitive
) == 0)
514 updateInfo
->m_downloadAddress
= value
.cap(2).trimmed();
516 else if(value
.cap(1).compare("DownloadFilename", Qt::CaseInsensitive
) == 0)
518 updateInfo
->m_downloadFilename
= value
.cap(2).trimmed();
520 else if(value
.cap(1).compare("DownloadFilecode", Qt::CaseInsensitive
) == 0)
522 updateInfo
->m_downloadFilecode
= value
.cap(2).trimmed();
524 else if(value
.cap(1).compare("DownloadChecksum", Qt::CaseInsensitive
) == 0)
526 updateInfo
->m_downloadChecksum
= value
.cap(2).trimmed();
529 if(inHdr
&& (value
.indexIn(line
) >= 0))
531 log(QString("Val: '%1' ==> '%2").arg(value
.cap(1), value
.cap(2)));
532 if(value
.cap(1).compare("TimestampCreated", Qt::CaseInsensitive
) == 0)
534 QDate temp
= QDate::fromString(value
.cap(2).trimmed(), Qt::ISODate
);
535 if(temp
.isValid()) updateInfoDate
= temp
;
540 if(!updateInfoDate
.isValid())
542 updateInfo
->resetInfo();
543 log("WARNING: Version info timestamp is missing!");
547 const QDate currentDate
= OS::current_date();
548 if(updateInfoDate
.addMonths(VERSION_INFO_EXPIRES_MONTHS
) < currentDate
)
550 updateInfo
->resetInfo();
551 log(QString::fromLatin1("WARNING: This version info has expired at %1!").arg(updateInfoDate
.addMonths(VERSION_INFO_EXPIRES_MONTHS
).toString(Qt::ISODate
)));
554 else if(currentDate
< updateInfoDate
)
556 log("Version info is from the future, take care!");
557 qWarning("Version info is from the future, take care!");
560 if(!updateInfo
->isComplete())
562 log("WARNING: Version info is incomplete!");
569 //----------------------------------------------------------
571 //----------------------------------------------------------
573 bool MUtils::UpdateChecker::getFile(const QString
&url
, const QString
&outFile
, const unsigned int maxRedir
)
575 for (int i
= 0; i
< 2; i
++)
577 if (getFile(url
, (i
> 0), outFile
, maxRedir
))
581 if (MUTILS_BOOLIFY(m_cancelled
))
589 bool MUtils::UpdateChecker::getFile(const QString
&url
, const bool forceIp4
, const QString
&outFile
, const unsigned int maxRedir
)
591 QFileInfo
output(outFile
);
592 output
.setCaching(false);
596 QFile::remove(output
.canonicalFilePath());
604 init_process(process
, output
.absolutePath());
612 args
<< "--no-config" << "--no-cache" << "--no-dns-cache" << "--no-check-certificate" << "--no-hsts";
613 args
<< QString().sprintf("--max-redirect=%u", maxRedir
) << QString().sprintf("--timeout=%u", DOWNLOAD_TIMEOUT
/ 1000);
614 args
<< QString("--referer=%1://%2/").arg(QUrl(url
).scheme(), QUrl(url
).host()) << "-U" << USER_AGENT_STR
;
615 args
<< "-O" << output
.fileName() << url
;
618 connect(&process
, SIGNAL(error(QProcess::ProcessError
)), &loop
, SLOT(quit()));
619 connect(&process
, SIGNAL(finished(int, QProcess::ExitStatus
)), &loop
, SLOT(quit()));
620 connect(&process
, SIGNAL(readyRead()), &loop
, SLOT(quit()));
623 timer
.setSingleShot(true);
624 connect(&timer
, SIGNAL(timeout()), &loop
, SLOT(quit()));
626 const QRegExp
httpResponseOK("200 OK$");
628 process
.start(m_binaryWGet
, args
);
630 if (!process
.waitForStarted())
635 timer
.start(DOWNLOAD_TIMEOUT
);
637 while (process
.state() != QProcess::NotRunning
)
640 const bool bTimeOut
= (!timer
.isActive());
641 while (process
.canReadLine())
643 const QString line
= QString::fromLatin1(process
.readLine()).simplified();
646 if (bTimeOut
|| MUTILS_BOOLIFY(m_cancelled
))
648 qWarning("WGet process timed out <-- killing!");
650 process
.waitForFinished();
651 log(bTimeOut
? "!!! TIMEOUT !!!": "!!! CANCELLED !!!");
657 timer
.disconnect(&timer
, SIGNAL(timeout()), &loop
, SLOT(quit()));
659 log(QString().sprintf("Exited with code %d", process
.exitCode()));
660 return (process
.exitCode() == 0) && output
.exists() && output
.isFile();
663 bool MUtils::UpdateChecker::tryContactHost(const QString
&hostname
, const int &timeoutMsec
, quint32
*const ipAddrOut
)
665 log(QString("Connecting to host: %1").arg(hostname
), "");
668 init_process(process
, temp_folder());
671 args
<< "--retry" << QString::number(3) << hostname
<< QString::number(80);
674 connect(&process
, SIGNAL(error(QProcess::ProcessError
)), &loop
, SLOT(quit()));
675 connect(&process
, SIGNAL(finished(int, QProcess::ExitStatus
)), &loop
, SLOT(quit()));
676 connect(&process
, SIGNAL(readyRead()), &loop
, SLOT(quit()));
679 timer
.setSingleShot(true);
680 connect(&timer
, SIGNAL(timeout()), &loop
, SLOT(quit()));
682 QScopedPointer
<QRegExp
> ipAddr
;
686 ipAddr
.reset(new QRegExp("Connecting\\s+to\\s+(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+):(\\d+)", Qt::CaseInsensitive
));
689 process
.start(m_binaryMCat
, args
);
691 if (!process
.waitForStarted())
696 timer
.start(timeoutMsec
);
698 while (process
.state() != QProcess::NotRunning
)
701 const bool bTimeOut
= (!timer
.isActive());
702 while (process
.canReadLine())
704 const QString line
= QString::fromLatin1(process
.readLine()).simplified();
705 if (!ipAddr
.isNull())
707 if (ipAddr
->indexIn(line
) >= 0)
710 if (MUtils::regexp_parse_uint32((*ipAddr
), values
, 4))
712 *ipAddrOut
|= ((values
[0] & 0xFF) << 0x18);
713 *ipAddrOut
|= ((values
[1] & 0xFF) << 0x10);
714 *ipAddrOut
|= ((values
[2] & 0xFF) << 0x08);
715 *ipAddrOut
|= ((values
[3] & 0xFF) << 0x00);
721 if (bTimeOut
|| MUTILS_BOOLIFY(m_cancelled
))
723 qWarning("MCat process timed out <-- killing!");
725 process
.waitForFinished();
726 log(bTimeOut
? "!!! TIMEOUT !!!" : "!!! CANCELLED !!!");
732 timer
.disconnect(&timer
, SIGNAL(timeout()), &loop
, SLOT(quit()));
734 if (process
.exitCode() != 0)
736 log("Connection has failed!");
739 log(QString().sprintf("Exited with code %d", process
.exitCode()), "");
740 return (process
.exitCode() == 0);
743 bool MUtils::UpdateChecker::checkSignature(const QString
&file
, const QString
&signature
)
745 if (QFileInfo(file
).absolutePath().compare(QFileInfo(signature
).absolutePath(), Qt::CaseInsensitive
) != 0)
747 qWarning("CheckSignature: File and signature should be in same folder!");
751 QString
keyRingPath(m_binaryKeys
);
752 bool removeKeyring
= false;
753 if (QFileInfo(file
).absolutePath().compare(QFileInfo(m_binaryKeys
).absolutePath(), Qt::CaseInsensitive
) != 0)
755 keyRingPath
= make_temp_file(QFileInfo(file
).absolutePath(), "gpg");
756 removeKeyring
= true;
757 if (!QFile::copy(m_binaryKeys
, keyRingPath
))
759 qWarning("CheckSignature: Failed to copy the key-ring file!");
765 init_process(process
, QFileInfo(file
).absolutePath());
768 connect(&process
, SIGNAL(error(QProcess::ProcessError
)), &loop
, SLOT(quit()));
769 connect(&process
, SIGNAL(finished(int, QProcess::ExitStatus
)), &loop
, SLOT(quit()));
770 connect(&process
, SIGNAL(readyRead()), &loop
, SLOT(quit()));
772 process
.start(m_binaryGnuPG
, QStringList() << "--homedir" << "." << "--keyring" << QFileInfo(keyRingPath
).fileName() << QFileInfo(signature
).fileName() << QFileInfo(file
).fileName());
774 if (!process
.waitForStarted())
778 remove_file(keyRingPath
);
783 while (process
.state() == QProcess::Running
)
786 while (process
.canReadLine())
788 log(QString::fromLatin1(process
.readLine()).simplified());
794 remove_file(keyRingPath
);
797 log(QString().sprintf("Exited with code %d", process
.exitCode()));
798 return (process
.exitCode() == 0);
801 ////////////////////////////////////////////////////////////
803 ////////////////////////////////////////////////////////////
807 ////////////////////////////////////////////////////////////
809 ////////////////////////////////////////////////////////////