mirror of
https://github.com/qTox/qTox.git
synced 2024-03-22 14:00:36 +08:00
56e9f6f1be
Check that each file was downloaded and that the size matches. We don't check the signatures inside qTox so as not to freeze the GUI during startup. The updater will check the signatures anyway We now try to restart update downloads if we detect that it was interrupted
468 lines
14 KiB
C++
468 lines
14 KiB
C++
/*
|
|
Copyright (C) 2014 by Project Tox <https://tox.im>
|
|
|
|
This file is part of qTox, a Qt-based graphical interface for Tox.
|
|
|
|
This program 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.
|
|
This program 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 COPYING file for more details.
|
|
*/
|
|
|
|
|
|
#include "src/autoupdate.h"
|
|
#include "src/misc/serialize.h"
|
|
#include "src/misc/settings.h"
|
|
#include "src/widget/widget.h"
|
|
#include <QNetworkAccessManager>
|
|
#include <QNetworkReply>
|
|
#include <QCoreApplication>
|
|
#include <QFile>
|
|
#include <QDir>
|
|
#include <QProcess>
|
|
#include <QtConcurrent/QtConcurrent>
|
|
#include <QMessageBox>
|
|
|
|
#ifdef Q_OS_WIN
|
|
#include <windows.h>
|
|
#include <shellapi.h>
|
|
#endif
|
|
|
|
#ifdef Q_OS_WIN
|
|
const QString AutoUpdater::platform = "win32";
|
|
const QString AutoUpdater::updaterBin = "qtox-updater.exe";
|
|
const QString AutoUpdater::updateServer = "https://s3.amazonaws.com/qtox-updater";
|
|
|
|
unsigned char AutoUpdater::key[crypto_sign_PUBLICKEYBYTES] =
|
|
{
|
|
0xa5, 0x80, 0xf3, 0xb7, 0xd0, 0x10, 0xc0, 0xf9, 0xd6, 0xcf, 0x48, 0x15, 0x99, 0x70, 0x92, 0x49,
|
|
0xf6, 0xe8, 0xe5, 0xe2, 0x6c, 0x73, 0x8c, 0x48, 0x25, 0xed, 0x01, 0x72, 0xf7, 0x6c, 0x17, 0x28
|
|
};
|
|
|
|
#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] =
|
|
{
|
|
0x12, 0x86, 0x25, 0x05, 0xb8, 0x9b, 0x39, 0x6f, 0xf1, 0xb1, 0xc4, 0x4d, 0x6f, 0x39, 0x35, 0x4d,
|
|
0xea, 0xdf, 0x6c, 0x97, 0x98, 0x7d, 0x6f, 0x1c, 0x29, 0xf5, 0xb2, 0x3a, 0x5b, 0x78, 0xc1, 0x34
|
|
};
|
|
|
|
#else
|
|
const QString AutoUpdater::platform;
|
|
const QString AutoUpdater::updaterBin;
|
|
const QString AutoUpdater::updateServer;
|
|
unsigned char AutoUpdater::key[crypto_sign_PUBLICKEYBYTES];
|
|
#endif
|
|
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/";
|
|
bool AutoUpdater::abortFlag{false};
|
|
|
|
bool AutoUpdater::isUpdateAvailable()
|
|
{
|
|
VersionInfo newVersion = getUpdateVersion();
|
|
if (newVersion.timestamp <= TIMESTAMP
|
|
|| newVersion.versionString.isEmpty() || newVersion.versionString == GIT_VERSION)
|
|
return false;
|
|
else
|
|
return true;
|
|
}
|
|
|
|
AutoUpdater::VersionInfo AutoUpdater::getUpdateVersion()
|
|
{
|
|
VersionInfo versionInfo;
|
|
versionInfo.timestamp = 0;
|
|
|
|
// Updates only for supported platforms
|
|
if (platform.isEmpty())
|
|
return versionInfo;
|
|
|
|
QNetworkAccessManager *manager = new QNetworkAccessManager;
|
|
QNetworkReply* reply = manager->get(QNetworkRequest(QUrl(checkURI)));
|
|
while (!reply->isFinished())
|
|
qApp->processEvents();
|
|
|
|
if (reply->error() != QNetworkReply::NoError)
|
|
{
|
|
qWarning() << "AutoUpdater: 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] != '2')
|
|
{
|
|
qWarning() << "AutoUpdater: 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() << "AutoUpdater: 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;
|
|
}
|
|
|
|
QList<AutoUpdater::UpdateFileMeta> AutoUpdater::parseFlist(QByteArray flistData)
|
|
{
|
|
QList<UpdateFileMeta> flist;
|
|
|
|
if (flistData.isEmpty())
|
|
{
|
|
qWarning() << "AutoUpdater::parseflist: Empty data";
|
|
return flist;
|
|
}
|
|
|
|
// Check version
|
|
if (flistData[0] != '1')
|
|
{
|
|
qWarning() << "AutoUpdater: parseflist: Bad version "<<(uint8_t)flistData[0];
|
|
return flist;
|
|
}
|
|
flistData = flistData.mid(1);
|
|
|
|
// Check signature
|
|
if (flistData.size() < (int)(crypto_sign_BYTES))
|
|
{
|
|
qWarning() << "AutoUpdater::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() << "AutoUpdater: 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;
|
|
}
|
|
|
|
QByteArray AutoUpdater::getUpdateFlist()
|
|
{
|
|
QByteArray flist;
|
|
|
|
QNetworkAccessManager *manager = new QNetworkAccessManager;
|
|
QNetworkReply* reply = manager->get(QNetworkRequest(QUrl(flistURI)));
|
|
while (!reply->isFinished())
|
|
qApp->processEvents();
|
|
|
|
if (reply->error() != QNetworkReply::NoError)
|
|
{
|
|
qWarning() << "AutoUpdater: getUpdateFlist: network error: "<<reply->errorString();
|
|
reply->deleteLater();
|
|
manager->deleteLater();
|
|
return flist;
|
|
}
|
|
|
|
flist = reply->readAll();
|
|
reply->deleteLater();
|
|
manager->deleteLater();
|
|
|
|
return flist;
|
|
}
|
|
|
|
QByteArray AutoUpdater::getLocalFlist()
|
|
{
|
|
QByteArray flist;
|
|
|
|
QFile flistFile("flist");
|
|
if (!flistFile.open(QIODevice::ReadOnly))
|
|
{
|
|
qWarning() << "AutoUpdater::getLocalFlist: Can't open local flist";
|
|
return flist;
|
|
}
|
|
|
|
flist = flistFile.readAll();
|
|
flistFile.close();
|
|
|
|
return flist;
|
|
}
|
|
|
|
QList<AutoUpdater::UpdateFileMeta> AutoUpdater::genUpdateDiff(QList<UpdateFileMeta> updateFlist)
|
|
{
|
|
QList<UpdateFileMeta> diff;
|
|
QList<UpdateFileMeta> localFlist = parseFlist(getLocalFlist());
|
|
|
|
for (UpdateFileMeta file : updateFlist)
|
|
if (!localFlist.contains(file))
|
|
diff += file;
|
|
|
|
return diff;
|
|
}
|
|
|
|
AutoUpdater::UpdateFile AutoUpdater::getUpdateFile(UpdateFileMeta fileMeta)
|
|
{
|
|
UpdateFile file;
|
|
file.metadata = fileMeta;
|
|
|
|
QNetworkAccessManager *manager = new QNetworkAccessManager;
|
|
QNetworkReply* reply = manager->get(QNetworkRequest(QUrl(filesURI+fileMeta.id)));
|
|
while (!reply->isFinished())
|
|
{
|
|
if (abortFlag)
|
|
return file;
|
|
qApp->processEvents();
|
|
}
|
|
|
|
if (reply->error() != QNetworkReply::NoError)
|
|
{
|
|
qWarning() << "AutoUpdater: getUpdateFile: network error: "<<reply->errorString();
|
|
reply->deleteLater();
|
|
manager->deleteLater();
|
|
return file;
|
|
}
|
|
|
|
file.data = reply->readAll();
|
|
reply->deleteLater();
|
|
manager->deleteLater();
|
|
|
|
return file;
|
|
}
|
|
|
|
|
|
bool AutoUpdater::downloadUpdate()
|
|
{
|
|
// Updates only for supported platforms
|
|
if (platform.isEmpty())
|
|
return false;
|
|
|
|
// Get a list of files to update
|
|
QByteArray newFlistData = getUpdateFlist();
|
|
QList<UpdateFileMeta> newFlist = parseFlist(newFlistData);
|
|
QList<UpdateFileMeta> diff = genUpdateDiff(newFlist);
|
|
|
|
if (abortFlag)
|
|
return false;
|
|
|
|
qDebug() << "AutoUpdater: 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())
|
|
updateDir.removeRecursively();
|
|
QDir().mkdir(updateDirStr);
|
|
updateDir = QDir(updateDirStr);
|
|
if (!updateDir.exists())
|
|
{
|
|
qWarning() << "AutoUpdater::downloadUpdate: Can't create update directory, aborting...";
|
|
return false;
|
|
}
|
|
|
|
// Write the new flist for the updater
|
|
QFile newFlistFile(updateDirStr+"flist");
|
|
if (!newFlistFile.open(QIODevice::WriteOnly | QIODevice::Truncate))
|
|
{
|
|
qWarning() << "AutoUpdater::downloadUpdate: Can't save new flist file, aborting...";
|
|
return false;
|
|
}
|
|
newFlistFile.write(newFlistData);
|
|
newFlistFile.close();
|
|
|
|
// Download and write each new file
|
|
for (UpdateFileMeta fileMeta : diff)
|
|
{
|
|
if (abortFlag)
|
|
return false;
|
|
|
|
qDebug() << "AutoUpdater: 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);
|
|
if (abortFlag)
|
|
return false;
|
|
if (file.data.isNull())
|
|
{
|
|
qWarning() << "AutoUpdater::downloadUpdate: Error downloading a file, aborting...";
|
|
return false;
|
|
}
|
|
|
|
// Check signature
|
|
if (crypto_sign_verify_detached(file.metadata.sig, (unsigned char*)file.data.data(),
|
|
file.data.size(), key) != 0)
|
|
{
|
|
qCritical() << "AutoUpdater: downloadUpdate: RECEIVED FORGED FILE, aborting...";
|
|
return false;
|
|
}
|
|
|
|
// Save
|
|
QFile fileFile(updateDirStr+fileMeta.installpath);
|
|
if (!fileFile.open(QIODevice::WriteOnly | QIODevice::Truncate))
|
|
{
|
|
qWarning() << "AutoUpdater::downloadUpdate: Can't save new update file, aborting...";
|
|
return false;
|
|
}
|
|
fileFile.write(file.data);
|
|
fileFile.close();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool AutoUpdater::isLocalUpdateReady()
|
|
{
|
|
// Updates only for supported platforms
|
|
if (platform.isEmpty())
|
|
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);
|
|
|
|
// If the update wasn't downloaded correctly, redownload it
|
|
// We don't check signatures to not block qTox too long, the updater will do it anyway
|
|
for (UpdateFileMeta fileMeta : diff)
|
|
{
|
|
if (!QFile::exists(updateDirStr+fileMeta.installpath))
|
|
{
|
|
QtConcurrent::run(&AutoUpdater::downloadUpdate);
|
|
return false;
|
|
}
|
|
|
|
QFile f(updateDirStr+fileMeta.installpath);
|
|
if (f.size() != (int64_t)fileMeta.size)
|
|
{
|
|
QtConcurrent::run(&AutoUpdater::downloadUpdate);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void AutoUpdater::installLocalUpdate()
|
|
{
|
|
qDebug() << "AutoUpdater: About to start the qTox updater to install a local update";
|
|
|
|
// Delete the update if we fail so we don't fail again.
|
|
|
|
// Updates only for supported platforms.
|
|
if (platform.isEmpty())
|
|
{
|
|
qCritical() << "AutoUpdater: Failed to start the qTox updater, removing the update and exiting";
|
|
QString updateDirStr = Settings::getInstance().getSettingsDirPath() + "/update/";
|
|
QDir(updateDirStr).removeRecursively();
|
|
exit(-1);
|
|
}
|
|
|
|
// Workaround QTBUG-7645
|
|
// QProcess fails silently when elevation is required instead of showing a UAC prompt on Win7/Vista
|
|
#ifdef Q_OS_WIN
|
|
HINSTANCE result = ::ShellExecuteA(0, "open", updaterBin.toUtf8().constData(), 0, 0, SW_SHOWNORMAL);
|
|
if (result == (HINSTANCE)SE_ERR_ACCESSDENIED)
|
|
{
|
|
// Requesting elevation
|
|
result = ::ShellExecuteA(0, "runas", updaterBin.toUtf8().constData(), 0, 0, SW_SHOWNORMAL);
|
|
}
|
|
if (result <= (HINSTANCE)32)
|
|
{
|
|
goto fail;
|
|
}
|
|
#else
|
|
if (!QProcess::startDetached(updaterBin))
|
|
goto fail;
|
|
#endif
|
|
|
|
exit(0);
|
|
|
|
// Centralized error handling
|
|
fail:
|
|
qCritical() << "AutoUpdater: Failed to start the qTox updater, removing the update and exiting";
|
|
QString updateDirStr = Settings::getInstance().getSettingsDirPath() + "/update/";
|
|
QDir(updateDirStr).removeRecursively();
|
|
exit(-1);
|
|
}
|
|
|
|
void AutoUpdater::checkUpdatesAsyncInteractive()
|
|
{
|
|
QtConcurrent::run(&AutoUpdater::checkUpdatesAsyncInteractiveWorker);
|
|
}
|
|
|
|
void AutoUpdater::checkUpdatesAsyncInteractiveWorker()
|
|
{
|
|
if (!isUpdateAvailable())
|
|
return;
|
|
|
|
if (Widget::getInstance()->askMsgboxQuestion(QObject::tr("Update", "The title of a message box"),
|
|
QObject::tr("An update is available, do you want to download it now?\nIt will be installed when qTox restarts.")))
|
|
{
|
|
downloadUpdate();
|
|
}
|
|
}
|
|
|
|
void AutoUpdater::abortUpdates()
|
|
{
|
|
abortFlag = true;
|
|
}
|