mirror of
https://github.com/Zaarrg/stremio-community-v5.git
synced 2026-05-11 12:40:37 +00:00
- **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.
351 lines
12 KiB
C++
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;
|
|
}
|