mirror of https://github.com/qTox/qTox.git
Browse Source
No need to keep it ifdef'd out in code, it can be re-added from git history instead once fixed.reviewable/pr5346/r8
11 changed files with 3 additions and 926 deletions
@ -1,710 +0,0 @@ |
|||||||
/*
|
|
||||||
Copyright © 2014-2018 by The qTox Project Contributors |
|
||||||
|
|
||||||
This file is part of qTox, a Qt-based graphical interface for Tox. |
|
||||||
|
|
||||||
qTox is libre software: you can redistribute it and/or modify |
|
||||||
it under the terms of the GNU General Public License as published by |
|
||||||
the Free Software Foundation, either version 3 of the License, or |
|
||||||
(at your option) any later version. |
|
||||||
|
|
||||||
qTox is distributed in the hope that it will be useful, |
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
||||||
GNU General Public License for more details. |
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License |
|
||||||
along with qTox. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/ |
|
||||||
|
|
||||||
#include "src/net/autoupdate.h" |
|
||||||
#include "src/nexus.h" |
|
||||||
#include "src/persistence/serialize.h" |
|
||||||
#include "src/persistence/settings.h" |
|
||||||
#include "src/widget/gui.h" |
|
||||||
#include "src/widget/widget.h" |
|
||||||
#include <QCoreApplication> |
|
||||||
#include <QDir> |
|
||||||
#include <QFile> |
|
||||||
#include <QMessageBox> |
|
||||||
#include <QMutexLocker> |
|
||||||
#include <QNetworkAccessManager> |
|
||||||
#include <QNetworkReply> |
|
||||||
#include <QProcess> |
|
||||||
#include <QtConcurrent/QtConcurrent> |
|
||||||
#include <iostream> |
|
||||||
|
|
||||||
#ifdef Q_OS_WIN |
|
||||||
// Because of replacing to incorrect order, which leads to building failing,
|
|
||||||
// this region is ignored for clang-format
|
|
||||||
// clang-format off
|
|
||||||
#include <windows.h> |
|
||||||
#include <shellapi.h> |
|
||||||
// clang-format on
|
|
||||||
#endif |
|
||||||
|
|
||||||
/**
|
|
||||||
* @file autoupdate.cpp |
|
||||||
* |
|
||||||
* For now we only support auto updates on Windows and OS X, although extending it is not a |
|
||||||
* technical issue. |
|
||||||
* Linux users are expected to use their package managers or update manually through official |
|
||||||
* channels. |
|
||||||
*/ |
|
||||||
|
|
||||||
#ifdef Q_OS_WIN |
|
||||||
#ifdef Q_OS_WIN64 |
|
||||||
const QString AutoUpdater::platform = "win64"; |
|
||||||
#else |
|
||||||
const QString AutoUpdater::platform = "win32"; |
|
||||||
#endif |
|
||||||
const QString AutoUpdater::updaterBin = "qtox-updater.exe"; |
|
||||||
const QString AutoUpdater::updateServer = "https://qtox-win.pkg.tox.chat"; |
|
||||||
|
|
||||||
unsigned char AutoUpdater::key[crypto_sign_PUBLICKEYBYTES] = {0x20, 0x89, 0x39, 0xaa, 0x9a, 0xe8, |
|
||||||
0xb5, 0x21, 0x0e, 0xac, 0x02, 0xa9, |
|
||||||
0xc4, 0x92, 0xd9, 0xa2, 0x17, 0x83, |
|
||||||
0xbd, 0x78, 0x0a, 0xda, 0x33, 0xcd, |
|
||||||
0xa5, 0xc6, 0x44, 0xc7, 0xfc, 0xed, |
|
||||||
0x00, 0x13}; |
|
||||||
|
|
||||||
#elif defined(Q_OS_OSX) |
|
||||||
const QString AutoUpdater::platform = "osx"; |
|
||||||
const QString AutoUpdater::updaterBin = "/Applications/qtox.app/Contents/MacOS/updater"; |
|
||||||
const QString AutoUpdater::updateServer = "https://dist-build.tox.im"; |
|
||||||
|
|
||||||
unsigned char AutoUpdater::key[crypto_sign_PUBLICKEYBYTES] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
|
||||||
0x00, 0x00}; |
|
||||||
|
|
||||||
#else |
|
||||||
const QString AutoUpdater::platform; |
|
||||||
const QString AutoUpdater::updaterBin; |
|
||||||
const QString AutoUpdater::updateServer; |
|
||||||
unsigned char AutoUpdater::key[crypto_sign_PUBLICKEYBYTES]; |
|
||||||
#endif |
|
||||||
|
|
||||||
/**
|
|
||||||
* @var unsigned char AutoUpdater::UpdateFileMeta::sig[crypto_sign_BYTES] |
|
||||||
* @brief Signature of the file (ed25519) |
|
||||||
* |
|
||||||
* @var QString AutoUpdater::UpdateFileMeta::id |
|
||||||
* @brief Unique id of the file |
|
||||||
* |
|
||||||
* @var QString AutoUpdater::UpdateFileMeta::installpath |
|
||||||
* @brief Local path including the file name. May be relative to qtox-updater or absolute |
|
||||||
* |
|
||||||
* @var uint64_t AutoUpdater::UpdateFileMeta::size |
|
||||||
* @brief Size in bytes of the file |
|
||||||
*/ |
|
||||||
|
|
||||||
/**
|
|
||||||
* @var static const QString AutoUpdater::updateServer |
|
||||||
* @brief Hostname of the qTox update server |
|
||||||
* |
|
||||||
* @var static const QString AutoUpdater::platform |
|
||||||
* @brief Name of platform we're trying to get updates for |
|
||||||
* |
|
||||||
* @var static const QString AutoUpdater::checkURI |
|
||||||
* @brief URI of the file containing the latest version string |
|
||||||
* |
|
||||||
* @var static const QString AutoUpdater::flistURI |
|
||||||
* @brief URI of the file containing info on each file (hash, signature, size, name, ..) |
|
||||||
* |
|
||||||
* @var static const QString AutoUpdater::filesURI |
|
||||||
* @brief URI of the actual files of the latest version |
|
||||||
* |
|
||||||
* @var static const QString AutoUpdater::updaterBin |
|
||||||
* @brief Path to the qtox-updater binary |
|
||||||
* |
|
||||||
* @var static std::atomic_bool AutoUpdater::abortFlag |
|
||||||
* @brief If true, try to abort everything. |
|
||||||
* |
|
||||||
* @var static std::atomic_bool AutoUpdater::isDownloadingUpdate |
|
||||||
* @brief We'll pretend there's no new update available if we're already updating |
|
||||||
* |
|
||||||
* @var static QMutex AutoUpdater::progressVersionMutex |
|
||||||
* @brief No, we can't just make the QString atomic |
|
||||||
*/ |
|
||||||
|
|
||||||
const QString AutoUpdater::checkURI = |
|
||||||
AutoUpdater::updateServer + "/qtox/" + AutoUpdater::platform + "/version"; |
|
||||||
const QString AutoUpdater::flistURI = |
|
||||||
AutoUpdater::updateServer + "/qtox/" + AutoUpdater::platform + "/flist"; |
|
||||||
const QString AutoUpdater::filesURI = |
|
||||||
AutoUpdater::updateServer + "/qtox/" + AutoUpdater::platform + "/files/"; |
|
||||||
std::atomic_bool AutoUpdater::abortFlag{false}; |
|
||||||
std::atomic_bool AutoUpdater::isDownloadingUpdate{false}; |
|
||||||
std::atomic<float> AutoUpdater::progressValue{0}; |
|
||||||
QString AutoUpdater::progressVersion; |
|
||||||
QMutex AutoUpdater::progressVersionMutex; |
|
||||||
|
|
||||||
/**
|
|
||||||
* @class AutoUpdater |
|
||||||
* @brief Handles checking and applying updates for qTox. |
|
||||||
* @note Do *NOT* use auto update unless AUTOUPDATE_ENABLED is defined to 1. |
|
||||||
*/ |
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Checks if an update is available for download. |
|
||||||
* @return True if an update is available for download, false otherwise. |
|
||||||
* |
|
||||||
* Connects to the qTox update server, and check if an update is available for download |
|
||||||
* Will call getUpdateVersion, and as such may block and processEvents. |
|
||||||
*/ |
|
||||||
bool AutoUpdater::isUpdateAvailable() |
|
||||||
{ |
|
||||||
if (isDownloadingUpdate) |
|
||||||
return false; |
|
||||||
|
|
||||||
QString updaterPath = |
|
||||||
updaterBin.startsWith('/') ? updaterBin : qApp->applicationDirPath() + '/' + updaterBin; |
|
||||||
if (!QFile::exists(updaterPath)) |
|
||||||
return false; |
|
||||||
|
|
||||||
QByteArray updateFlist = getUpdateFlist(); |
|
||||||
QList<UpdateFileMeta> diff = genUpdateDiff(parseFlist(updateFlist)); |
|
||||||
return !diff.isEmpty(); |
|
||||||
} |
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Fetch the version info of the last update available from the qTox update server |
|
||||||
* @note Will try to follow qTox's proxy settings, may block and processEvents |
|
||||||
* @return Avaliable version info. |
|
||||||
*/ |
|
||||||
AutoUpdater::VersionInfo AutoUpdater::getUpdateVersion() |
|
||||||
{ |
|
||||||
VersionInfo versionInfo; |
|
||||||
versionInfo.timestamp = 0; |
|
||||||
|
|
||||||
// Updates only for supported platforms
|
|
||||||
if (platform.isEmpty()) |
|
||||||
return versionInfo; |
|
||||||
|
|
||||||
if (abortFlag) |
|
||||||
return versionInfo; |
|
||||||
|
|
||||||
QNetworkAccessManager* manager = new QNetworkAccessManager; |
|
||||||
manager->setProxy(Settings::getInstance().getProxy()); |
|
||||||
QNetworkReply* reply = manager->get(QNetworkRequest(QUrl(checkURI))); |
|
||||||
while (!reply->isFinished()) { |
|
||||||
if (abortFlag) |
|
||||||
return versionInfo; |
|
||||||
qApp->processEvents(); |
|
||||||
} |
|
||||||
|
|
||||||
if (reply->error() != QNetworkReply::NoError) { |
|
||||||
qWarning() << "getUpdateVersion: network error: " + reply->errorString(); |
|
||||||
reply->deleteLater(); |
|
||||||
manager->deleteLater(); |
|
||||||
return versionInfo; |
|
||||||
} |
|
||||||
|
|
||||||
QByteArray data = reply->readAll(); |
|
||||||
reply->deleteLater(); |
|
||||||
manager->deleteLater(); |
|
||||||
if (data.size() < (int)(1 + crypto_sign_BYTES)) |
|
||||||
return versionInfo; |
|
||||||
|
|
||||||
// Check updater protocol version
|
|
||||||
if ((int)data[0] != '3') { |
|
||||||
qWarning() << "getUpdateVersion: Bad version " << (uint8_t)data[0]; |
|
||||||
return versionInfo; |
|
||||||
} |
|
||||||
|
|
||||||
// Check the signature
|
|
||||||
QByteArray sigData = data.mid(1, crypto_sign_BYTES); |
|
||||||
unsigned char* sig = (unsigned char*)sigData.data(); |
|
||||||
QByteArray msgData = data.mid(1 + crypto_sign_BYTES); |
|
||||||
unsigned char* msg = (unsigned char*)msgData.data(); |
|
||||||
|
|
||||||
if (crypto_sign_verify_detached(sig, msg, msgData.size(), key) != 0) { |
|
||||||
qCritical() << "getUpdateVersion: RECEIVED FORGED VERSION FILE FROM " << updateServer; |
|
||||||
return versionInfo; |
|
||||||
} |
|
||||||
|
|
||||||
int sepPos = msgData.indexOf('!'); |
|
||||||
versionInfo.timestamp = QString(msgData.left(sepPos)).toInt(); |
|
||||||
versionInfo.versionString = msgData.mid(sepPos + 1); |
|
||||||
|
|
||||||
qDebug() << "timestamp:" << versionInfo.timestamp << ", str:" << versionInfo.versionString; |
|
||||||
|
|
||||||
return versionInfo; |
|
||||||
} |
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Parses and validates a flist file. |
|
||||||
* @param flistData Install file data. |
|
||||||
* @return An empty list on error. |
|
||||||
*/ |
|
||||||
QList<AutoUpdater::UpdateFileMeta> AutoUpdater::parseFlist(QByteArray flistData) |
|
||||||
{ |
|
||||||
QList<UpdateFileMeta> flist; |
|
||||||
|
|
||||||
if (flistData.isEmpty()) { |
|
||||||
qWarning() << "parseflist: Empty data"; |
|
||||||
return flist; |
|
||||||
} |
|
||||||
|
|
||||||
// Check version
|
|
||||||
if (flistData[0] != '1') { |
|
||||||
qWarning() << "parseflist: Bad version " << (uint8_t)flistData[0]; |
|
||||||
return flist; |
|
||||||
} |
|
||||||
flistData = flistData.mid(1); |
|
||||||
|
|
||||||
// Check signature
|
|
||||||
if (flistData.size() < (int)(crypto_sign_BYTES)) { |
|
||||||
qWarning() << "parseflist: Truncated data"; |
|
||||||
return flist; |
|
||||||
} else { |
|
||||||
QByteArray msgData = flistData.mid(crypto_sign_BYTES); |
|
||||||
unsigned char* msg = (unsigned char*)msgData.data(); |
|
||||||
if (crypto_sign_verify_detached((unsigned char*)flistData.data(), msg, msgData.size(), key) |
|
||||||
!= 0) { |
|
||||||
qCritical() << "parseflist: FORGED FLIST FILE"; |
|
||||||
return flist; |
|
||||||
} |
|
||||||
flistData = flistData.mid(crypto_sign_BYTES); |
|
||||||
} |
|
||||||
|
|
||||||
// Parse. We assume no errors handling needed since the signature is valid.
|
|
||||||
while (!flistData.isEmpty()) { |
|
||||||
UpdateFileMeta newFile; |
|
||||||
|
|
||||||
memcpy(newFile.sig, flistData.data(), crypto_sign_BYTES); |
|
||||||
flistData = flistData.mid(crypto_sign_BYTES); |
|
||||||
|
|
||||||
newFile.id = dataToString(flistData); |
|
||||||
flistData = flistData.mid(newFile.id.size() + getVUint32Size(flistData)); |
|
||||||
|
|
||||||
newFile.installpath = dataToString(flistData); |
|
||||||
flistData = flistData.mid(newFile.installpath.size() + getVUint32Size(flistData)); |
|
||||||
|
|
||||||
newFile.size = dataToUint64(flistData); |
|
||||||
flistData = flistData.mid(8); |
|
||||||
|
|
||||||
flist += newFile; |
|
||||||
} |
|
||||||
|
|
||||||
return flist; |
|
||||||
} |
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Gets the update server's flist. |
|
||||||
* @note Will try to follow qTox's proxy settings, may block and processEvents |
|
||||||
* @return An empty array on error |
|
||||||
*/ |
|
||||||
QByteArray AutoUpdater::getUpdateFlist() |
|
||||||
{ |
|
||||||
QByteArray flist; |
|
||||||
|
|
||||||
QNetworkAccessManager* manager = new QNetworkAccessManager; |
|
||||||
manager->setProxy(Settings::getInstance().getProxy()); |
|
||||||
QNetworkReply* reply = manager->get(QNetworkRequest(QUrl(flistURI))); |
|
||||||
while (!reply->isFinished()) { |
|
||||||
if (abortFlag) |
|
||||||
return flist; |
|
||||||
qApp->processEvents(); |
|
||||||
} |
|
||||||
|
|
||||||
if (reply->error() != QNetworkReply::NoError) { |
|
||||||
qWarning() << "getUpdateFlist: network error: " + reply->errorString(); |
|
||||||
reply->deleteLater(); |
|
||||||
manager->deleteLater(); |
|
||||||
return flist; |
|
||||||
} |
|
||||||
|
|
||||||
flist = reply->readAll(); |
|
||||||
reply->deleteLater(); |
|
||||||
manager->deleteLater(); |
|
||||||
|
|
||||||
return flist; |
|
||||||
} |
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Generates a list of files we need to update. |
|
||||||
* @param updateFlist List of files available to update. |
|
||||||
* @return List of files we need to update. |
|
||||||
*/ |
|
||||||
QList<AutoUpdater::UpdateFileMeta> AutoUpdater::genUpdateDiff(QList<UpdateFileMeta> updateFlist) |
|
||||||
{ |
|
||||||
QList<UpdateFileMeta> diff; |
|
||||||
|
|
||||||
for (UpdateFileMeta file : updateFlist) |
|
||||||
if (!isUpToDate(file)) |
|
||||||
diff += file; |
|
||||||
|
|
||||||
return diff; |
|
||||||
} |
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Checks if we have an up to date version of this file locally installed. |
|
||||||
* @param fileMeta File to check. |
|
||||||
* @return True if file doesn't need updates, false if need. |
|
||||||
*/ |
|
||||||
bool AutoUpdater::isUpToDate(AutoUpdater::UpdateFileMeta fileMeta) |
|
||||||
{ |
|
||||||
QString appDir = qApp->applicationDirPath(); |
|
||||||
QFile file(appDir + QDir::separator() + fileMeta.installpath); |
|
||||||
if (!file.open(QIODevice::ReadOnly)) |
|
||||||
return false; |
|
||||||
|
|
||||||
// If the data we have is corrupted or old, mark it for update
|
|
||||||
QByteArray data = file.readAll(); |
|
||||||
if (crypto_sign_verify_detached(fileMeta.sig, (unsigned char*)data.data(), data.size(), key) != 0) |
|
||||||
return false; |
|
||||||
|
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Tries to fetch the file from the update server. |
|
||||||
* @note Note that a file with an empty but non-null QByteArray is not an error, merely a file of |
|
||||||
* size 0. |
|
||||||
* @note Will try to follow qTox's proxy settings, may block and processEvents. |
|
||||||
* @param fileMeta Meta data fo file to update. |
|
||||||
* @param progressCallback Callback function, which will connected with |
|
||||||
* QNetworkReply::downloadProgress |
|
||||||
* @return A file with a null QByteArray on error. |
|
||||||
*/ |
|
||||||
AutoUpdater::UpdateFile AutoUpdater::getUpdateFile(UpdateFileMeta fileMeta, |
|
||||||
std::function<void(int, int)> progressCallback) |
|
||||||
{ |
|
||||||
UpdateFile file; |
|
||||||
file.metadata = fileMeta; |
|
||||||
|
|
||||||
QNetworkAccessManager* manager = new QNetworkAccessManager; |
|
||||||
manager->setProxy(Settings::getInstance().getProxy()); |
|
||||||
QNetworkReply* reply = manager->get(QNetworkRequest(QUrl(filesURI + fileMeta.id))); |
|
||||||
QObject::connect(reply, &QNetworkReply::downloadProgress, progressCallback); |
|
||||||
while (!reply->isFinished()) { |
|
||||||
if (abortFlag) |
|
||||||
return file; |
|
||||||
qApp->processEvents(); |
|
||||||
} |
|
||||||
|
|
||||||
if (reply->error() != QNetworkReply::NoError) { |
|
||||||
qWarning() << "getUpdateFile: network error: " + reply->errorString(); |
|
||||||
reply->deleteLater(); |
|
||||||
manager->deleteLater(); |
|
||||||
return file; |
|
||||||
} |
|
||||||
|
|
||||||
file.data = reply->readAll(); |
|
||||||
reply->deleteLater(); |
|
||||||
manager->deleteLater(); |
|
||||||
|
|
||||||
return file; |
|
||||||
} |
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Will try to download an update, if successful qTox will apply it after a restart |
|
||||||
* @note Will try to follow qTox's proxy settings, may block and processEvents |
|
||||||
* @result True if successful and qTox will apply it after a restart |
|
||||||
*/ |
|
||||||
bool AutoUpdater::downloadUpdate() |
|
||||||
{ |
|
||||||
// Updates only for supported platforms
|
|
||||||
if (platform.isEmpty()) |
|
||||||
return false; |
|
||||||
|
|
||||||
bool expectFalse = false; |
|
||||||
if (!isDownloadingUpdate.compare_exchange_strong(expectFalse, true)) |
|
||||||
return false; |
|
||||||
|
|
||||||
// Get a list of files to update
|
|
||||||
QByteArray newFlistData = getUpdateFlist(); |
|
||||||
QList<UpdateFileMeta> newFlist = parseFlist(newFlistData); |
|
||||||
QList<UpdateFileMeta> diff = genUpdateDiff(newFlist); |
|
||||||
|
|
||||||
// Progress
|
|
||||||
progressValue = 0; |
|
||||||
|
|
||||||
if (abortFlag) { |
|
||||||
isDownloadingUpdate = false; |
|
||||||
return false; |
|
||||||
} |
|
||||||
|
|
||||||
qDebug() << "Need to update" << diff.size() << "files"; |
|
||||||
|
|
||||||
// Create an empty directory to download updates into
|
|
||||||
QString updateDirStr = Settings::getInstance().getSettingsDirPath() + "/update/"; |
|
||||||
QDir updateDir(updateDirStr); |
|
||||||
if (!updateDir.exists()) |
|
||||||
QDir().mkdir(updateDirStr); |
|
||||||
updateDir = QDir(updateDirStr); |
|
||||||
if (!updateDir.exists()) { |
|
||||||
qWarning() << "downloadUpdate: Can't create update directory, aborting..."; |
|
||||||
isDownloadingUpdate = false; |
|
||||||
return false; |
|
||||||
} |
|
||||||
|
|
||||||
// Write the new flist for the updater
|
|
||||||
QFile newFlistFile(updateDirStr + "flist"); |
|
||||||
if (!newFlistFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) { |
|
||||||
qWarning() << "downloadUpdate: Can't save new flist file, aborting..."; |
|
||||||
isDownloadingUpdate = false; |
|
||||||
return false; |
|
||||||
} |
|
||||||
newFlistFile.write(newFlistData); |
|
||||||
newFlistFile.close(); |
|
||||||
|
|
||||||
progressValue = 1; |
|
||||||
|
|
||||||
// Download and write each new file
|
|
||||||
for (UpdateFileMeta fileMeta : diff) { |
|
||||||
float initialProgress = progressValue, step = 99. / diff.size(); |
|
||||||
auto stepProgressCallback = [&](int current, int total) { |
|
||||||
progressValue = initialProgress + step * (float)current / total; |
|
||||||
}; |
|
||||||
|
|
||||||
if (abortFlag) { |
|
||||||
isDownloadingUpdate = false; |
|
||||||
return false; |
|
||||||
} |
|
||||||
|
|
||||||
// Skip files we already have
|
|
||||||
QFile fileFile(updateDirStr + fileMeta.installpath); |
|
||||||
if (fileFile.open(QIODevice::ReadOnly) && fileFile.size() == (qint64)fileMeta.size) { |
|
||||||
qDebug() << "Skipping already downloaded file '" + fileMeta.installpath + "'"; |
|
||||||
progressValue = initialProgress + step; |
|
||||||
continue; |
|
||||||
} |
|
||||||
fileFile.close(); |
|
||||||
|
|
||||||
qDebug() << "Downloading '" + fileMeta.installpath + "' ..."; |
|
||||||
|
|
||||||
// Create subdirs if necessary
|
|
||||||
QString fileDirStr{QFileInfo(updateDirStr + fileMeta.installpath).absolutePath()}; |
|
||||||
if (!QDir(fileDirStr).exists()) |
|
||||||
QDir().mkpath(fileDirStr); |
|
||||||
|
|
||||||
// Download
|
|
||||||
UpdateFile file = getUpdateFile(fileMeta, stepProgressCallback); |
|
||||||
if (abortFlag) |
|
||||||
goto fail; |
|
||||||
if (file.data.isNull()) { |
|
||||||
qCritical() << "downloadUpdate: Error downloading a file, aborting..."; |
|
||||||
goto fail; |
|
||||||
} |
|
||||||
|
|
||||||
// Check signature
|
|
||||||
if (crypto_sign_verify_detached(file.metadata.sig, (unsigned char*)file.data.data(), |
|
||||||
file.data.size(), key) |
|
||||||
!= 0) { |
|
||||||
qCritical() << "downloadUpdate: RECEIVED FORGED FILE, aborting..."; |
|
||||||
goto fail; |
|
||||||
} |
|
||||||
|
|
||||||
// Save
|
|
||||||
if (!fileFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) { |
|
||||||
qCritical() << "downloadUpdate: Can't save new update file, aborting..."; |
|
||||||
goto fail; |
|
||||||
} |
|
||||||
fileFile.write(file.data); |
|
||||||
fileFile.close(); |
|
||||||
|
|
||||||
progressValue = initialProgress + step; |
|
||||||
} |
|
||||||
|
|
||||||
qDebug() << "downloadUpdate: The update is ready, it'll be installed on the next restart"; |
|
||||||
|
|
||||||
isDownloadingUpdate = false; |
|
||||||
progressValue = 100; |
|
||||||
return true; |
|
||||||
|
|
||||||
fail: |
|
||||||
isDownloadingUpdate = false; |
|
||||||
progressValue = 0; |
|
||||||
setProgressVersion(""); |
|
||||||
return false; |
|
||||||
} |
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Checks if an update is downloaded and ready to be installed. |
|
||||||
* @note If result is true, call installLocalUpdate, |
|
||||||
* @return True if an update is downloaded, false if partially downloaded. |
|
||||||
* |
|
||||||
* If an update was partially downloaded, the function will resume asynchronously and return false. |
|
||||||
*/ |
|
||||||
bool AutoUpdater::isLocalUpdateReady() |
|
||||||
{ |
|
||||||
// Updates only for supported platforms
|
|
||||||
if (platform.isEmpty()) |
|
||||||
return false; |
|
||||||
|
|
||||||
if (isDownloadingUpdate) |
|
||||||
return false; |
|
||||||
|
|
||||||
// Check that there's an update dir in the first place, valid or not
|
|
||||||
QString updateDirStr = Settings::getInstance().getSettingsDirPath() + "/update/"; |
|
||||||
QDir updateDir(updateDirStr); |
|
||||||
if (!updateDir.exists()) |
|
||||||
return false; |
|
||||||
|
|
||||||
// Check that we have a flist and generate a diff
|
|
||||||
QFile updateFlistFile(updateDirStr + "flist"); |
|
||||||
if (!updateFlistFile.open(QIODevice::ReadOnly)) |
|
||||||
return false; |
|
||||||
QByteArray updateFlistData = updateFlistFile.readAll(); |
|
||||||
updateFlistFile.close(); |
|
||||||
|
|
||||||
QList<UpdateFileMeta> updateFlist = parseFlist(updateFlistData); |
|
||||||
QList<UpdateFileMeta> diff = genUpdateDiff(updateFlist); |
|
||||||
|
|
||||||
// Check that we have every file
|
|
||||||
for (UpdateFileMeta fileMeta : diff) { |
|
||||||
if (!QFile::exists(updateDirStr + fileMeta.installpath)) |
|
||||||
return false; |
|
||||||
|
|
||||||
QFile f(updateDirStr + fileMeta.installpath); |
|
||||||
if (f.size() != (int64_t)fileMeta.size) |
|
||||||
return false; |
|
||||||
} |
|
||||||
|
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Launches the qTox updater to try to install the local update and exits immediately. |
|
||||||
* |
|
||||||
* @note Will not check that the update actually exists, use isLocalUpdateReady first for that. |
|
||||||
* The qTox updater will restart us after the update is done. |
|
||||||
* If we fail to start the qTox updater, we will delete the update and exit. |
|
||||||
*/ |
|
||||||
void AutoUpdater::installLocalUpdate() |
|
||||||
{ |
|
||||||
qDebug() << "About to start the qTox updater to install a local update"; |
|
||||||
|
|
||||||
// Prepare to delete the update if we fail so we don't fail again.
|
|
||||||
auto failExit = []() { |
|
||||||
qCritical() << "Failed to start the qTox updater, removing the update and exiting"; |
|
||||||
QString updateDirStr = Settings::getInstance().getSettingsDirPath() + "/update/"; |
|
||||||
QDir(updateDirStr).removeRecursively(); |
|
||||||
exit(-1); |
|
||||||
}; |
|
||||||
|
|
||||||
// Updates only for supported platforms.
|
|
||||||
if (platform.isEmpty()) |
|
||||||
failExit(); |
|
||||||
|
|
||||||
// Workaround QTBUG-7645
|
|
||||||
// QProcess fails silently when elevation is required instead of showing a UAC prompt on Win7/Vista
|
|
||||||
#ifdef Q_OS_WIN |
|
||||||
const std::wstring modulePath = qApp->applicationDirPath().replace('/', '\\').toStdWString(); |
|
||||||
const std::wstring updaterBinPath = updaterBin.toStdWString(); |
|
||||||
HINSTANCE result = ::ShellExecuteW(0, L"open", updaterBinPath.c_str(), 0, |
|
||||||
modulePath.c_str(), SW_SHOWNORMAL); |
|
||||||
if (result == (HINSTANCE)SE_ERR_ACCESSDENIED) { |
|
||||||
// Requesting elevation
|
|
||||||
result = ::ShellExecuteW(0, L"runas", updaterBinPath.c_str(), 0, |
|
||||||
modulePath.c_str(), SW_SHOWNORMAL); |
|
||||||
} |
|
||||||
if (result <= (HINSTANCE)32) |
|
||||||
failExit(); |
|
||||||
#else |
|
||||||
if (!QProcess::startDetached(updaterBin)) |
|
||||||
failExit(); |
|
||||||
#endif |
|
||||||
|
|
||||||
exit(0); |
|
||||||
} |
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Checks update an show dialog asking to download it. |
|
||||||
* @note Runs asynchronously in its own thread, and will return immediatly |
|
||||||
* |
|
||||||
* Will call isUpdateAvailable, and as such may processEvents. |
|
||||||
* Connects to the qTox update server, if an update is found |
|
||||||
* shows a dialog to the user asking to download it. |
|
||||||
*/ |
|
||||||
void AutoUpdater::checkUpdatesAsyncInteractive() |
|
||||||
{ |
|
||||||
if (isDownloadingUpdate) |
|
||||||
return; |
|
||||||
|
|
||||||
QtConcurrent::run(&AutoUpdater::checkUpdatesAsyncInteractiveWorker); |
|
||||||
} |
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Does the actual work for checkUpdatesAsyncInteractive |
|
||||||
* |
|
||||||
* Blocking, but otherwise has the same properties than checkUpdatesAsyncInteractive |
|
||||||
*/ |
|
||||||
void AutoUpdater::checkUpdatesAsyncInteractiveWorker() |
|
||||||
{ |
|
||||||
if (!isUpdateAvailable()) |
|
||||||
return; |
|
||||||
|
|
||||||
// If there's already an update dir, resume updating, otherwise ask the user
|
|
||||||
QString updateDirStr = Settings::getInstance().getSettingsDirPath() + "/update/"; |
|
||||||
QDir updateDir(updateDirStr); |
|
||||||
|
|
||||||
|
|
||||||
if (updateDir.exists() && QFile(updateDirStr + "flist").exists()) { |
|
||||||
setProgressVersion(getUpdateVersion().versionString); |
|
||||||
downloadUpdate(); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
VersionInfo newVersion = getUpdateVersion(); |
|
||||||
QString contentText = QObject::tr("An update is available, do you want to download it now?\n" |
|
||||||
"It will be installed when qTox restarts."); |
|
||||||
if (!newVersion.versionString.isEmpty()) |
|
||||||
contentText += |
|
||||||
"\n\n" |
|
||||||
+ QObject::tr("Version %1, %2") |
|
||||||
.arg(newVersion.versionString, |
|
||||||
QDateTime::fromMSecsSinceEpoch(newVersion.timestamp * 1000).toString()); |
|
||||||
|
|
||||||
|
|
||||||
if (abortFlag) |
|
||||||
return; |
|
||||||
|
|
||||||
if (GUI::askQuestion(QObject::tr("Update", "The title of a message box"), contentText, true, false)) { |
|
||||||
setProgressVersion(newVersion.versionString); |
|
||||||
GUI::showUpdateDownloadProgress(); |
|
||||||
downloadUpdate(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Thread safe setter |
|
||||||
* @param version Version to set. |
|
||||||
*/ |
|
||||||
void AutoUpdater::setProgressVersion(QString version) |
|
||||||
{ |
|
||||||
QMutexLocker lock(&progressVersionMutex); |
|
||||||
progressVersion = version; |
|
||||||
} |
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Abort update process. |
|
||||||
* |
|
||||||
* @note Aborting will make some functions try to return early. |
|
||||||
* Call before qTox exits to avoid the updater running in the background. |
|
||||||
*/ |
|
||||||
void AutoUpdater::abortUpdates() |
|
||||||
{ |
|
||||||
abortFlag = true; |
|
||||||
isDownloadingUpdate = false; |
|
||||||
} |
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Functions giving info on the progress of update downloads. |
|
||||||
* @return Version as string. |
|
||||||
*/ |
|
||||||
QString AutoUpdater::getProgressVersion() |
|
||||||
{ |
|
||||||
QMutexLocker lock(&progressVersionMutex); |
|
||||||
return progressVersion; |
|
||||||
} |
|
||||||
|
|
||||||
int AutoUpdater::getProgressValue() |
|
||||||
{ |
|
||||||
return progressValue; |
|
||||||
} |
|
@ -1,99 +0,0 @@ |
|||||||
/*
|
|
||||||
Copyright © 2014-2018 by The qTox Project Contributors |
|
||||||
|
|
||||||
This file is part of qTox, a Qt-based graphical interface for Tox. |
|
||||||
|
|
||||||
qTox is libre software: you can redistribute it and/or modify |
|
||||||
it under the terms of the GNU General Public License as published by |
|
||||||
the Free Software Foundation, either version 3 of the License, or |
|
||||||
(at your option) any later version. |
|
||||||
|
|
||||||
qTox is distributed in the hope that it will be useful, |
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
||||||
GNU General Public License for more details. |
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License |
|
||||||
along with qTox. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/ |
|
||||||
|
|
||||||
|
|
||||||
#ifndef AUTOUPDATE_H |
|
||||||
#define AUTOUPDATE_H |
|
||||||
|
|
||||||
#include <QList> |
|
||||||
#include <QMutex> |
|
||||||
#include <QString> |
|
||||||
#include <atomic> |
|
||||||
#include <functional> |
|
||||||
#include <sodium.h> |
|
||||||
|
|
||||||
class AutoUpdater |
|
||||||
{ |
|
||||||
public: |
|
||||||
struct UpdateFileMeta |
|
||||||
{ |
|
||||||
unsigned char sig[crypto_sign_BYTES]; |
|
||||||
QString id; |
|
||||||
QString installpath; |
|
||||||
uint64_t size; |
|
||||||
|
|
||||||
bool operator==(const UpdateFileMeta& other) |
|
||||||
{ |
|
||||||
return (size == other.size && id == other.id && installpath == other.installpath |
|
||||||
&& memcmp(sig, other.sig, crypto_sign_BYTES) == 0); |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
struct UpdateFile |
|
||||||
{ |
|
||||||
UpdateFileMeta metadata; |
|
||||||
QByteArray data; |
|
||||||
}; |
|
||||||
|
|
||||||
struct VersionInfo |
|
||||||
{ |
|
||||||
uint64_t timestamp; |
|
||||||
QString versionString; |
|
||||||
}; |
|
||||||
|
|
||||||
public: |
|
||||||
static void checkUpdatesAsyncInteractive(); |
|
||||||
static bool isUpdateAvailable(); |
|
||||||
static VersionInfo getUpdateVersion(); |
|
||||||
static bool downloadUpdate(); |
|
||||||
static bool isLocalUpdateReady(); |
|
||||||
[[noreturn]] static void installLocalUpdate(); |
|
||||||
static void abortUpdates(); |
|
||||||
static QString getProgressVersion(); |
|
||||||
static int getProgressValue(); |
|
||||||
|
|
||||||
protected: |
|
||||||
static QList<UpdateFileMeta> parseFlist(QByteArray flistData); |
|
||||||
static QByteArray getUpdateFlist(); |
|
||||||
static QList<UpdateFileMeta> genUpdateDiff(QList<UpdateFileMeta> updateFlist); |
|
||||||
static bool isUpToDate(UpdateFileMeta file); |
|
||||||
static UpdateFile getUpdateFile(UpdateFileMeta fileMeta, |
|
||||||
std::function<void(int, int)> progressCallback); |
|
||||||
static void checkUpdatesAsyncInteractiveWorker(); |
|
||||||
static void setProgressVersion(QString version); |
|
||||||
|
|
||||||
private: |
|
||||||
AutoUpdater() = delete; |
|
||||||
|
|
||||||
private: |
|
||||||
static const QString updateServer; |
|
||||||
static const QString platform; |
|
||||||
static const QString checkURI; |
|
||||||
static const QString flistURI; |
|
||||||
static const QString filesURI; |
|
||||||
static const QString updaterBin; |
|
||||||
static unsigned char key[]; |
|
||||||
static std::atomic_bool abortFlag; |
|
||||||
static std::atomic_bool isDownloadingUpdate; |
|
||||||
static std::atomic<float> progressValue; |
|
||||||
static QString progressVersion; |
|
||||||
static QMutex progressVersionMutex; |
|
||||||
}; |
|
||||||
|
|
||||||
#endif // AUTOUPDATE_H
|
|
Loading…
Reference in new issue