stremio-community-v5/src/updater/autoupdater.cpp
Zarg 56dd1f4bd6 Auto Updater!
- **Auto updating!** Fixed auto updater to work with this fork.
- Added Updater Notification Screen
- Fixed Chrome Session not being saved, forcing login on each start.
2024-12-20 12:17:11 +01:00

351 lines
12 KiB
C++

#include "autoupdater.h"
#ifdef Q_OS_MACOS
#include <sys/types.h>
#include <sys/sysctl.h>
#include <QDebug>
#endif
AutoUpdater::AutoUpdater(): manager(new QNetworkAccessManager(this)) {
init_public_key();
}
// HANDLE FATAL ERRORS
void AutoUpdater::emitFatalError(QString msg, QVariant err = QVariant()) {
this->abort();
emit error(msg, err);
}
// IS INSTALLED?
bool AutoUpdater::isInstalled() {
QString dirPath = QDir::toNativeSeparators(QCoreApplication::applicationDirPath());
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
// Windows
if (env.contains("LOCALAPPDATA") && dirPath.startsWith(env.value("LOCALAPPDATA"))) return true;
if (env.contains("ProgramFiles") && dirPath.startsWith(env.value("ProgramFiles"))) return true;
if (env.contains("ProgramFiles(x86)") && dirPath.startsWith(env.value("ProgramFiles(x86)"))) return true;
// macOS
if (dirPath.contains("/Applications") && dirPath.contains(".app")) return true;
// Rosetta2
#ifdef Q_OS_MACOS
int ret = 0;
size_t size = sizeof(ret);
if (sysctlbyname("sysctl.proc_translated", &ret, &size, NULL, 0) >= 0 && ret) {
QDir pathDir("/Applications/Stremio.app");
qDebug() << "AUTOUPDATER: Installed on Rosetta!";
return pathDir.exists();
}
#endif
// Linux - appImage
if (dirPath.startsWith("/tmp/.mount_")) return true;
// Other UNIX
// Disabled, because we cannot update those cases
//if (dirPath.contains("/usr/bin") || dir.contains("/usr/local") || dir.contains("/opt")) return true;
return false;
}
// WRAPPERS for public slots to make sure we execute on our thread
void AutoUpdater::checkForUpdates(QString endpoint, QString userAgent) {
if (inProgress) return;
inProgress = true;
QMetaObject::invokeMethod(this, "checkForUpdatesPerform", Qt::QueuedConnection, Q_ARG(QString, endpoint), Q_ARG(QString, userAgent));
}
void AutoUpdater::updateFromVersionDesc(QUrl versionDesc, QByteArray base64Sig) {
if (inProgress) return;
inProgress = true;
QMetaObject::invokeMethod(this, "updateFromVersionDescPerform", Qt::QueuedConnection, Q_ARG(QUrl, versionDesc),
Q_ARG(QByteArray, base64Sig));
}
void AutoUpdater::abort() {
QMetaObject::invokeMethod(this, "abortPerform", Qt::QueuedConnection);
}
// SETTINGS
void AutoUpdater::setForceFullUpdate(bool force) {
forceFullUpdate = force;
}
// UTILS
bool AutoUpdater::moveFileToAppDir(QString from) {
QDir dir;
QFileInfo oldFile = QFileInfo(from);
QString dest = QCoreApplication::applicationDirPath() + QDir::separator() + oldFile.fileName();
if (! QFile::exists(from)) return false;
if (QFile::exists(dest)) {
if (! QFile::remove(dest)) return false;
}
return dir.rename(from, dest);
}
int AutoUpdater::executeCmd(QString cmd, QStringList args, bool noWait = false) {
QProcess proc;
proc.setProcessChannelMode(QProcess::ForwardedChannels);
if (noWait) {
proc.startDetached(cmd, args);
return -1;
}
proc.start(cmd, args);
// We mostly need quick commands executed, and waiting for them in that func removes a huge layer of complexity
if (! proc.waitForFinished(5 * 60 * 1000)) return -1;
return proc.exitCode();
}
// CHECK FOR UPDATES
void AutoUpdater::checkForUpdatesPerform(QString endpoint, QString userAgent)
{
QUrl url = QUrl(endpoint);
qDebug() << "AUTOUPDATER: Checking for updates...";
auto request = QNetworkRequest(QUrl(url));
request.setRawHeader("User-Agent", userAgent.toUtf8());
currentCheck = manager->get(request);
QObject::connect(currentCheck, &QNetworkReply::finished, this, &AutoUpdater::checkForUpdatesFinished);
}
void AutoUpdater::checkForUpdatesFinished()
{
if (currentCheck == NULL) {
emitFatalError("internal error - currentCheck NULL on checkForUpdatesFinished");
return;
}
QNetworkReply* reply = currentCheck;
reply->deleteLater();
currentCheck = NULL;
if (reply->error() == QNetworkReply::NoError) {
QJsonParseError *error = NULL;
QJsonDocument jsonResponse = QJsonDocument::fromJson(reply->readAll(), error);
emit checkFinished(jsonResponse.toVariant());
if (jsonResponse.isObject()) {
QJsonObject obj = jsonResponse.object();
if (obj.value("upToDate").toBool()) {
// NO NEW VERSION, DO NOTHING
inProgress = false;
} else {
updateFromVersionDescPerform(
QUrl(obj.value("versionDesc").toString()),
QByteArray::fromBase64(obj.value("signature").toString().toUtf8())
);
}
} else if (error) {
emitFatalError("JSON parse error on checkForUpdates "+error->errorString());
} else {
emitFatalError("Unable to understand response from checkForUpdates");
}
delete error;
} else if (reply->error() != QNetworkReply::OperationCanceledError) {
emitFatalError("Network error on checkForUpdates "+reply->url().toString(), reply->error());
}
}
// GET & VERIFY (SIGNATURE) VERSION DESC
void AutoUpdater::updateFromVersionDescPerform(QUrl versionDesc, QByteArray base64Sig) {
currentCheck = manager->get(QNetworkRequest(versionDesc));
currentCheck->setProperty("signature", base64Sig);
QObject::connect(currentCheck, &QNetworkReply::finished, this, &AutoUpdater::updateFromVersionDescFinished);
}
void AutoUpdater::updateFromVersionDescFinished() {
if (currentCheck == NULL) {
emitFatalError("internal error - currentCheck NULL on updateFromVersionDescFinished");
return;
}
QNetworkReply* reply = currentCheck;
reply->deleteLater();
currentCheck = NULL;
if (reply->error() == QNetworkReply::NoError) {
QByteArray dataReply = reply->readAll();
QByteArray sig = reply->property("signature").toByteArray();
if (verify_sig(
(const byte*)dataReply.data(), dataReply.size(),
(const byte*)sig.data(), sig.length()
) != 0) {
emitFatalError("Unable to verify update signature");
} else {
QJsonParseError *error = NULL;
QJsonDocument jsonResponse = QJsonDocument::fromJson(dataReply, error);
if (jsonResponse.isObject()) {
prepareUpdate(jsonResponse);
} else if (error) {
emitFatalError("JSON parse error on updateFromVersionDesc "+error->errorString());
} else {
emitFatalError("Unable to understand response from updateFromVersionDesc");
}
delete error;
}
} else if (reply->error() != QNetworkReply::OperationCanceledError) {
emitFatalError("Network error on updateFromVersionDesc "+reply->url().toString(), reply->error());
}
}
// DETERMINE WHAT TO DOWNLOAD FROM versionDesc
void AutoUpdater::prepareUpdate(QJsonDocument versionDescDoc) {
currentVersionDesc = versionDescDoc;
QJsonObject versionDesc = versionDescDoc.object();
QJsonObject files = versionDesc.value("files").toObject();
QByteArray serverHash = getFileChecksum(QCoreApplication::applicationDirPath() + QDir::separator() + SERVER_FNAME);
QString serverHashHex = QString::fromLatin1(serverHash.toHex());
QVector<QString> toDownload;
if (forceFullUpdate
|| versionDesc.value("shellVersion").toString() != QCoreApplication::applicationVersion()
) {
toDownload = FULL_UPDATE_FILES;
} else if (files.value(PARTIAL_UPDATE_FILES).toObject().value("checksum").toString() != serverHashHex) {
toDownload = PARTIAL_UPDATE_FILES;
} else {
qDebug() << "Everything is up to date";
return;
}
if (! toDownload.length()) {
emitFatalError("internal error - no files to download. Unsupported OS?");
return;
}
foreach (const QString &prop, toDownload) {
QJsonObject file = files.value(prop).toObject();
if (! (file.contains("url") && file.contains("checksum"))) continue;
enqueueDownload(
QUrl(file.value("url").toString()),
QByteArray::fromHex(file.value("checksum").toString().toUtf8())
);
}
startNextDownload();
}
// DOWNLOAD & VERIFY (CHECKSUM)
QByteArray AutoUpdater::getFileChecksum(QString path) {
QCryptographicHash crypto(QCryptographicHash::Sha256);
QFile file(path);
file.open(QFile::ReadOnly);
while (!file.atEnd()) { crypto.addData(file.read(FILE_READ_CHUNK)); }
return crypto.result();
}
void AutoUpdater::enqueueDownload(QUrl from, QByteArray checksum) {
downloadQueue.enqueue(fDownload(from, checksum));
}
void AutoUpdater::startNextDownload() {
if (downloadQueue.isEmpty()) {
inProgress = false;
emit prepared(preparedFiles, QVariant(currentVersionDesc.object()));
return;
}
fDownload next = downloadQueue.dequeue();
QUrl url = next.first;
QByteArray checksum = next.second;
// WARNING: TODO: do we want to make a separate dir inside tempPath? ; we should ensure downloadFile always overrides
QString dest = QDir::tempPath() + QDir::separator() + url.fileName();
// Check if the download is already downloaded - could happen if we try to do a full upgrade when we've
// already prepared one
// Sketchy case: if the file does not exist, getFileChecksum would return the default sha256 hash; -
// this would actually prevent a case where the version descriptor is generated from empty files from breaking
// the system - because this check would return true, and then the file wouldn't exist at all, emitting an error
// (this shouldn't be able to happen, but still...)
if (checksum == getFileChecksum(dest)) {
preparedFiles.push_back(dest);
startNextDownload();
return;
}
// Start the download
output.setFileName(dest);
if (!output.open(QIODevice::WriteOnly)) {
emitFatalError("error opening file "+dest+" for download: "+output.errorString());
return;
}
currentDownload = manager->get(QNetworkRequest(url));
currentDownload->setProperty("checksum", checksum);
QObject::connect(currentDownload, &QNetworkReply::readyRead, this, &AutoUpdater::downloadReadyRead);
QObject::connect(currentDownload, &QNetworkReply::finished, this, &AutoUpdater::downloadFinished);
}
void AutoUpdater::downloadReadyRead()
{
output.write(currentDownload->readAll());
}
void AutoUpdater::downloadFinished()
{
output.close();
if (currentDownload == NULL) {
emitFatalError("internal error - currentDownload NULL on downloadFinished");
return;
}
QNetworkReply* reply = currentDownload;
reply->deleteLater();
currentDownload = NULL;
if (reply->error() == QNetworkReply::NoError) {
QString dest = output.fileName();
QByteArray checksum = reply->property("checksum").toByteArray();
if (checksum == getFileChecksum(dest)) {
preparedFiles.push_back(dest);
startNextDownload();
} else {
emitFatalError("Unable to verify checksum for file "+dest);
}
} else if (reply->error() != QNetworkReply::OperationCanceledError) {
emitFatalError("Network error on downloadFinished "+reply->url().toString(), reply->error());
}
}
// ABORT
void AutoUpdater::abortPerform() {
// EXPLANATION: those will be aborted, and then in the 'finished' handler, they will be deleted via ->deleteLater()
// since the event handlers are executed in the event loop, one might think calling .checkForVer() right after
// .abort() will re-set currentCheck before the 'finished' handler is executed
// This is not a problem, because all public methods call the internal ones with invokeMethod and queuedConnection
if (currentCheck) currentCheck->abort();
if (currentDownload) currentDownload->abort();
currentVersionDesc = QJsonDocument();
downloadQueue = QQueue<fDownload>();
preparedFiles = QVariantList();
output.close();
inProgress = false;
}