From bc63a9d4f122fd8507ce2fb864f3c469ccb260c2 Mon Sep 17 00:00:00 2001 From: tobtoht Date: Wed, 1 Feb 2023 15:40:12 +0100 Subject: [PATCH] MainWindow: Help -> Check for updates --- src/MainWindow.cpp | 89 ++++------------------ src/MainWindow.h | 9 ++- src/MainWindow.ui | 6 ++ src/dialog/UpdateDialog.cpp | 63 +++++++++++----- src/dialog/UpdateDialog.h | 13 ++-- src/dialog/UpdateDialog.ui | 131 ++++++++++++++++++++------------- src/utils/Updater.cpp | 143 +++++++++++++++++++++++++++++++++++- src/utils/Updater.h | 33 ++++++++- 8 files changed, 331 insertions(+), 156 deletions(-) diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 64e6306..a513b05 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -54,6 +54,8 @@ MainWindow::MainWindow(WindowManager *windowManager, Wallet *wallet, QWidget *pa m_splashDialog = new SplashDialog(this); m_accountSwitcherDialog = new AccountSwitcherDialog(m_ctx, this); + m_updater = QSharedPointer(new Updater(this)); + this->restoreGeo(); this->initStatusBar(); @@ -67,7 +69,7 @@ MainWindow::MainWindow(WindowManager *windowManager, Wallet *wallet, QWidget *pa connect(websocketNotifier(), &WebsocketNotifier::BountyReceived, ui->bountiesWidget->model(), &BountiesModel::updateBounties); connect(websocketNotifier(), &WebsocketNotifier::RedditReceived, ui->redditWidget->model(), &RedditModel::updatePosts); connect(websocketNotifier(), &WebsocketNotifier::RevuoReceived, ui->revuoWidget, &RevuoWidget::updateItems); - connect(websocketNotifier(), &WebsocketNotifier::UpdatesReceived, this, &MainWindow::onUpdatesAvailable); + connect(websocketNotifier(), &WebsocketNotifier::UpdatesReceived, m_updater.data(), &Updater::wsUpdatesReceived); #ifdef HAS_XMRIG connect(websocketNotifier(), &WebsocketNotifier::XMRigDownloadsReceived, m_xmrig, &XMRigWidget::onDownloads); #endif @@ -80,6 +82,8 @@ MainWindow::MainWindow(WindowManager *windowManager, Wallet *wallet, QWidget *pa connect(torManager(), &TorManager::connectionStateChanged, this, &MainWindow::onTorConnectionStateChanged); this->onTorConnectionStateChanged(torManager()->torConnected); + connect(m_updater.data(), &Updater::updateAvailable, this, &MainWindow::showUpdateNotification); + ColorScheme::updateFromWidget(this); QTimer::singleShot(1, [this]{this->updateWidgetIcons();}); @@ -350,6 +354,11 @@ void MainWindow::initMenu() { // [Help] connect(ui->actionAbout, &QAction::triggered, this, &MainWindow::menuAboutClicked); +#if defined(CHECK_UPDATES) + connect(ui->actionCheckForUpdates, &QAction::triggered, this, &MainWindow::showUpdateDialog); +#else + ui->actionCheckForUpdates->setVisible(false); +#endif connect(ui->actionOfficialWebsite, &QAction::triggered, [this](){Utils::externalLinkWarning(this, "https://featherwallet.org");}); connect(ui->actionDonate_to_Feather, &QAction::triggered, this, &MainWindow::donateButtonClicked); connect(ui->actionDocumentation, &QAction::triggered, this, &MainWindow::onShowDocumentation); @@ -1341,9 +1350,8 @@ void MainWindow::onTorConnectionStateChanged(bool connected) { m_statusBtnTor->setIcon(icons()->icon("tor_logo_disabled.png")); } -void MainWindow::onCheckUpdatesComplete(const QString &version, const QString &binaryFilename, - const QString &hash, const QString &signer) { - QString versionDisplay{version}; +void MainWindow::showUpdateNotification() { + QString versionDisplay{m_updater->version}; versionDisplay.replace("beta", "Beta"); QString updateText = QString("Update to Feather %1 is available").arg(versionDisplay); m_statusUpdateAvailable->setText(updateText); @@ -1351,82 +1359,15 @@ void MainWindow::onCheckUpdatesComplete(const QString &version, const QString &b m_statusUpdateAvailable->show(); m_statusUpdateAvailable->disconnect(); - connect(m_statusUpdateAvailable, &StatusBarButton::clicked, [this, version, binaryFilename, hash, signer] { - this->onShowUpdateCheck(version, binaryFilename, hash, signer); - }); + connect(m_statusUpdateAvailable, &StatusBarButton::clicked, this, &MainWindow::showUpdateDialog); } -void MainWindow::onShowUpdateCheck(const QString &version, const QString &binaryFilename, - const QString &hash, const QString &signer) { - QString platformTag = this->getPlatformTag(); - QString downloadUrl = QString("https://featherwallet.org/files/releases/%1/%2").arg(platformTag, binaryFilename); - - UpdateDialog updateDialog{this, version, downloadUrl, hash, signer, platformTag}; +void MainWindow::showUpdateDialog() { + UpdateDialog updateDialog{this, m_updater}; connect(&updateDialog, &UpdateDialog::restartWallet, m_windowManager, &WindowManager::restartApplication); updateDialog.exec(); } -void MainWindow::onUpdatesAvailable(const QJsonObject &updates) { - QString featherVersionStr{FEATHER_VERSION}; - - auto featherVersion = SemanticVersion::fromString(featherVersionStr); - - QString platformTag = getPlatformTag(); - if (platformTag.isEmpty()) { - qWarning() << "Unsupported platform, unable to fetch update"; - return; - } - - QJsonObject platformData = updates["platform"].toObject()[platformTag].toObject(); - if (platformData.isEmpty()) { - qWarning() << "Unable to find current platform in updates data"; - return; - } - - QString newVersion = platformData["version"].toString(); - if (SemanticVersion::fromString(newVersion) <= featherVersion) { - return; - } - - // Hooray! New update available - - QString hashesUrl = QString("%1/files/releases/hashes-%2-plain.txt").arg(constants::websiteUrl, newVersion); - - UtilsNetworking network{getNetworkTor()}; - QNetworkReply *reply = network.get(hashesUrl); - - connect(reply, &QNetworkReply::finished, this, std::bind(&MainWindow::onSignedHashesReceived, this, reply, platformTag, newVersion)); -} - -void MainWindow::onSignedHashesReceived(QNetworkReply *reply, const QString &platformTag, const QString &version) { - if (reply->error() != QNetworkReply::NoError) { - qWarning() << "Unable to fetch signed hashes: " << reply->errorString(); - return; - } - - QByteArray armoredSignedHashes = reply->readAll(); - reply->deleteLater(); - - const QString binaryFilename = QString("feather-%1-%2.zip").arg(version, platformTag); - QString signer; - QByteArray signedHash = AsyncTask::runAndWaitForFuture([armoredSignedHashes, binaryFilename, &signer]{ - try { - return Updater().verifyParseSignedHashes(armoredSignedHashes, binaryFilename, signer); - } - catch (const std::exception &e) { - qWarning() << "Failed to fetch and verify signed hash: " << e.what(); - return QByteArray{}; - } - }); - if (signedHash.isEmpty()) { - return; - } - - QString hash = signedHash.toHex(); - qInfo() << "Update found: " << binaryFilename << hash << "signed by:" << signer; - this->onCheckUpdatesComplete(version, binaryFilename, hash, signer); -} - void MainWindow::onInitiateTransaction() { m_statusDots = 0; m_constructingTransaction = true; diff --git a/src/MainWindow.h b/src/MainWindow.h index 7b1c7dc..34a380e 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -31,6 +31,7 @@ #include "utils/networking.h" #include "utils/config.h" #include "utils/EventFilter.h" +#include "utils/Updater.h" #include "widgets/CCSWidget.h" #include "widgets/RedditWidget.h" #include "widgets/TickerWidget.h" @@ -136,9 +137,7 @@ private slots: void loadSignedTxFromText(); void onTorConnectionStateChanged(bool connected); - void onCheckUpdatesComplete(const QString &version, const QString &binaryFilename, const QString &hash, const QString &signer); - void onShowUpdateCheck(const QString &version, const QString &binaryFilename, const QString &hash, const QString &signer); - void onSignedHashesReceived(QNetworkReply *reply, const QString &platformTag, const QString &version); + void showUpdateDialog(); void onInitiateTransaction(); void onEndTransaction(); void onKeysCorrupted(); @@ -181,10 +180,10 @@ private slots: void onDeviceButtonPressed(); void onWalletPassphraseNeeded(bool on_device); void menuHwDeviceClicked(); - void onUpdatesAvailable(const QJsonObject &updates); void toggleSearchbar(bool enabled); void tryStoreWallet(); void onWebsocketStatusChanged(bool enabled); + void showUpdateNotification(); private: friend WindowManager; @@ -287,6 +286,8 @@ private: EventFilter *m_eventFilter = nullptr; qint64 m_userLastActive = QDateTime::currentSecsSinceEpoch(); + + QSharedPointer m_updater = nullptr; }; #endif // FEATHER_MAINWINDOW_H diff --git a/src/MainWindow.ui b/src/MainWindow.ui index fa9799d..0f913ea 100644 --- a/src/MainWindow.ui +++ b/src/MainWindow.ui @@ -591,6 +591,7 @@ Help + @@ -942,6 +943,11 @@ Lock wallet + + + Check for updates + + diff --git a/src/dialog/UpdateDialog.cpp b/src/dialog/UpdateDialog.cpp index 1af5841..3fd9ecb 100644 --- a/src/dialog/UpdateDialog.cpp +++ b/src/dialog/UpdateDialog.cpp @@ -6,6 +6,7 @@ #include +#include "constants.h" #include "utils/AsyncTask.h" #include "utils/networking.h" #include "utils/NetworkManager.h" @@ -14,24 +15,23 @@ #include "zip.h" -UpdateDialog::UpdateDialog(QWidget *parent, QString version, QString downloadUrl, QString hash, QString signer, QString platformTag) - : QDialog(parent) - , ui(new Ui::UpdateDialog) - , m_version(std::move(version)) - , m_downloadUrl(std::move(downloadUrl)) - , m_hash(std::move(hash)) - , m_signer(std::move(signer)) - , m_platformTag(std::move(platformTag)) +UpdateDialog::UpdateDialog(QWidget *parent, QSharedPointer updater) + : QDialog(parent) + , ui(new Ui::UpdateDialog) + , m_updater(std::move(updater)) { ui->setupUi(this); - ui->btn_installUpdate->hide(); - ui->btn_restart->hide(); - ui->progressBar->hide(); - auto bigFont = Utils::relativeFont(4); ui->label_header->setFont(bigFont); - ui->label_header->setText(QString("New Feather version %1 is available").arg(m_version)); + ui->frame->hide(); + + bool updateAvailable = (m_updater->state == Updater::State::UPDATE_AVAILABLE); + if (updateAvailable) { + this->updateAvailable(); + } else { + this->checkForUpdates(); + } connect(ui->btn_cancel, &QPushButton::clicked, [this]{ if (m_reply) { @@ -43,9 +43,36 @@ UpdateDialog::UpdateDialog(QWidget *parent, QString version, QString downloadUrl connect(ui->btn_installUpdate, &QPushButton::clicked, this, &UpdateDialog::onInstallUpdate); connect(ui->btn_restart, &QPushButton::clicked, this, &UpdateDialog::onRestartClicked); + connect(m_updater.data(), &Updater::updateAvailable, this, &UpdateDialog::updateAvailable); + connect(m_updater.data(), &Updater::noUpdateAvailable, this, &UpdateDialog::noUpdateAvailable); + connect(m_updater.data(), &Updater::updateCheckFailed, this, &UpdateDialog::onUpdateCheckFailed); + this->adjustSize(); } +void UpdateDialog::checkForUpdates() { + ui->label_header->setText("Checking for updates..."); + ui->label_body->setText("..."); + m_updater->checkForUpdates(); +} + +void UpdateDialog::noUpdateAvailable() { + this->setStatus("Feather is up-to-date.", true); +} + +void UpdateDialog::updateAvailable() { + ui->frame->show(); + ui->btn_installUpdate->hide(); + ui->btn_restart->hide(); + ui->progressBar->hide(); + ui->label_header->setText(QString("New Feather version %1 is available").arg(m_updater->version)); + ui->label_body->setText("Do you want to download and verify the new version?"); +} + +void UpdateDialog::onUpdateCheckFailed(const QString &errorMsg) { + this->setStatus(QString("Failed to check for updates: %1").arg(errorMsg), false); +} + void UpdateDialog::onDownloadClicked() { ui->label_body->setText("Downloading update.."); ui->btn_download->hide(); @@ -53,7 +80,7 @@ void UpdateDialog::onDownloadClicked() { UtilsNetworking network{getNetworkTor()}; - m_reply = network.get(m_downloadUrl); + m_reply = network.get(m_updater->downloadUrl); connect(m_reply, &QNetworkReply::downloadProgress, this, &UpdateDialog::onDownloadProgress); connect(m_reply, &QNetworkReply::finished, this, &UpdateDialog::onDownloadFinished); } @@ -83,7 +110,7 @@ void UpdateDialog::onDownloadFinished() { return Updater().getHash(&responseStr[0], responseStr.size()); }); - const QByteArray signedHash = QByteArray::fromHex(m_hash.toUtf8()); + const QByteArray signedHash = QByteArray::fromHex(m_updater->hash.toUtf8()); if (signedHash != calculatedHash) { this->onDownloadError("Error: Hash sum mismatch."); @@ -180,7 +207,7 @@ void UpdateDialog::onInstallUpdate() { QDir applicationDir(Utils::applicationPath()); QString filePath = applicationDir.filePath(name); - if (m_platformTag == "win-installer") { + if (m_updater->platformTag == "win-installer") { filePath = QString("%1/%2").arg(QStandardPaths::writableLocation(QStandardPaths::DownloadLocation), name); } @@ -205,7 +232,7 @@ void UpdateDialog::onInstallUpdate() { return; } - if (m_platformTag == "win-installer") { + if (m_updater->platformTag == "win-installer") { this->setStatus("Installer written. Click 'restart' to close Feather and start the installer."); } else { this->setStatus("Installation successful. Do you want to restart Feather now?"); @@ -219,7 +246,7 @@ void UpdateDialog::installUpdateMac() { if (appPath.endsWith("Contents/MacOS")) { appDir.cd("../../.."); } - QString appName = QString("feather-%1").arg(m_version); + QString appName = QString("feather-%1").arg(m_updater->version); QString zipName = QString("%1.zip").arg(appName); QString fPath = appDir.filePath(zipName); diff --git a/src/dialog/UpdateDialog.h b/src/dialog/UpdateDialog.h index a6641ac..53cb07b 100644 --- a/src/dialog/UpdateDialog.h +++ b/src/dialog/UpdateDialog.h @@ -7,6 +7,8 @@ #include #include +#include "utils/Updater.h" + namespace Ui { class UpdateDialog; } @@ -16,7 +18,7 @@ class UpdateDialog : public QDialog Q_OBJECT public: - explicit UpdateDialog(QWidget *parent, QString version, QString downloadUrl, QString hash, QString signer, QString platformTag); + explicit UpdateDialog(QWidget *parent, QSharedPointer updater); ~UpdateDialog() override; private slots: @@ -27,21 +29,22 @@ private slots: void onInstallUpdate(); void onInstallError(const QString &errMsg); void onRestartClicked(); + void onUpdateCheckFailed(const QString &onUpdateCheckFailed); signals: void restartWallet(const QString &binaryFilename); private: + void checkForUpdates(); + void noUpdateAvailable(); + void updateAvailable(); void setStatus(const QString &msg, bool success = false); void installUpdateMac(); QScopedPointer ui; + QSharedPointer m_updater; - QString m_version; QString m_downloadUrl; - QString m_hash; - QString m_signer; - QString m_platformTag; QString m_updatePath; diff --git a/src/dialog/UpdateDialog.ui b/src/dialog/UpdateDialog.ui index fba1689..3c12c1d 100644 --- a/src/dialog/UpdateDialog.ui +++ b/src/dialog/UpdateDialog.ui @@ -6,12 +6,12 @@ 0 0 - 569 - 148 + 540 + 144 - Update Available + Updater @@ -26,59 +26,86 @@ Do you want to download and verify the new version? - - - - - - 0 + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Cancel - - - - - - - Download - - - - - - - Install Update - - - - - - - Restart Feather - - - - + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Cancel + + + + + + + Download + + + + + + + Install Update + + + + + + + Restart Feather + + + + + + + diff --git a/src/utils/Updater.cpp b/src/utils/Updater.cpp index da2e9be..4e12c0d 100644 --- a/src/utils/Updater.cpp +++ b/src/utils/Updater.cpp @@ -6,13 +6,154 @@ #include #include +#include "config-feather.h" +#include "constants.h" #include "Utils.h" +#include "utils/AsyncTask.h" +#include "utils/networking.h" +#include "utils/NetworkManager.h" +#include "utils/SemanticVersion.h" -Updater::Updater() { +Updater::Updater(QObject *parent) : + QObject(parent) +{ std::string featherWallet = Utils::fileOpen(":/assets/gpg_keys/featherwallet.asc").toStdString(); m_maintainers.emplace_back(featherWallet); } +void Updater::checkForUpdates() { + UtilsNetworking network{getNetworkTor()}; + QNetworkReply *reply = network.getJson("https://featherwallet.org/updates.json"); + + connect(reply, &QNetworkReply::finished, this, std::bind(&Updater::onUpdateCheckResponse, this, reply)); +} + +void Updater::onUpdateCheckResponse(QNetworkReply *reply) { + const QString err = reply->errorString(); + + QByteArray data = reply->readAll(); + reply->deleteLater(); + + QJsonObject updates; + if (!data.isEmpty() && Utils::validateJSON(data)) { + auto doc = QJsonDocument::fromJson(data); + updates = doc.object(); + } + else { + qWarning() << err; + emit updateCheckFailed(err); + return; + } + + this->wsUpdatesReceived(updates); +} + +void Updater::wsUpdatesReceived(const QJsonObject &updates) { + QString featherVersionStr{FEATHER_VERSION}; + + auto featherVersion = SemanticVersion::fromString(featherVersionStr); + + QString platformTag = getPlatformTag(); + if (platformTag.isEmpty()) { + QString err{"Unsupported platform, unable to fetch update"}; + emit updateCheckFailed(err); + qWarning() << err; + return; + } + + QJsonObject platformData = updates["platform"].toObject()[platformTag].toObject(); + if (platformData.isEmpty()) { + QString err{"Unable to find current platform in updates data"}; + emit updateCheckFailed(err); + qWarning() << err; + return; + } + + QString newVersion = platformData["version"].toString(); + if (SemanticVersion::fromString(newVersion) <= featherVersion) { + emit noUpdateAvailable(); + return; + } + + // Hooray! New update available + + QString hashesUrl = QString("%1/files/releases/hashes-%2-plain.txt").arg(constants::websiteUrl, newVersion); + + UtilsNetworking network{getNetworkTor()}; + QNetworkReply *reply = network.get(hashesUrl); + + connect(reply, &QNetworkReply::finished, this, std::bind(&Updater::onSignedHashesReceived, this, reply, platformTag, newVersion)); +} + +void Updater::onSignedHashesReceived(QNetworkReply *reply, const QString &platformTag, const QString &version) { + if (reply->error() != QNetworkReply::NoError) { + QString err{QString("Unable to fetch signed hashed: %1").arg(reply->errorString())}; + emit updateCheckFailed(err); + qWarning() << err; + return; + } + + QByteArray armoredSignedHashes = reply->readAll(); + reply->deleteLater(); + + const QString binaryFilename = QString("feather-%1-%2.zip").arg(version, platformTag); + QByteArray signedHash{}; + QString signer; + try { + signedHash = this->verifyParseSignedHashes(armoredSignedHashes, binaryFilename, signer); + } + catch (const std::exception &e) { + QString err{QString("Failed to fetch and verify signed hash: %1").arg(e.what())}; + emit updateCheckFailed(err); + qWarning() << err; + return; + } + + QString hash = signedHash.toHex(); + qInfo() << "Update found: " << binaryFilename << hash << "signed by:" << signer; + + this->state = Updater::State::UPDATE_AVAILABLE; + this->version = version; + this->binaryFilename = binaryFilename; + this->downloadUrl = QString("https://featherwallet.org/files/releases/%1/%2").arg(platformTag, binaryFilename); + this->hash = hash; + this->signer = signer; + this->platformTag = platformTag; + + emit updateAvailable(); +} + +QString Updater::getPlatformTag() { +#ifdef Q_OS_MACOS + return "mac"; +#endif +#ifdef Q_OS_WIN + #ifdef PLATFORM_INSTALLER + return "win-installer"; +#endif + return "win"; +#endif +#ifdef Q_OS_LINUX + QString tag = ""; + + QString arch = QSysInfo::buildCpuArchitecture(); + if (arch == "arm64") { + tag += "linux-arm64"; + } else if (arch == "arm") { + tag += "linux-arm"; + } else { + tag += "linux"; + } + + if (!qEnvironmentVariableIsEmpty("APPIMAGE")) { + tag += "-appimage"; + } + + return tag; +#endif + return ""; +} + QByteArray Updater::verifyParseSignedHashes( const QByteArray &armoredSignedHashes, const QString &binaryFilename, diff --git a/src/utils/Updater.h b/src/utils/Updater.h index 8a42b88..d280520 100644 --- a/src/utils/Updater.h +++ b/src/utils/Updater.h @@ -8,10 +8,20 @@ #include -class Updater +class Updater : public QObject { +Q_OBJECT + public: - explicit Updater(); + enum State { + NO_UPDATE = 0, + UPDATE_AVAILABLE = 1 + }; + +public: + explicit Updater(QObject *parent = nullptr); + + void checkForUpdates(); QByteArray verifyParseSignedHashes(const QByteArray &armoredSignedHashes, const QString &binaryFilename, QString &signers) const; @@ -19,8 +29,27 @@ public: QString verifySignature(const QByteArray &armoredSignedMessage, QString &signer) const; QByteArray parseShasumOutput(const QString &message, const QString &filename) const; + State state = State::NO_UPDATE; + QString version; + QString binaryFilename; + QString downloadUrl; + QString hash; + QString signer; + QString platformTag; + +signals: + void updateCheckFailed(const QString &error); + void noUpdateAvailable(); + void updateAvailable(); + +public slots: + void onUpdateCheckResponse(QNetworkReply *reply); + void wsUpdatesReceived(const QJsonObject &updates); + private: QString verifySignature(const epee::span data, const openpgp::signature_rsa &signature) const; + void onSignedHashesReceived(QNetworkReply *reply, const QString &platformTag, const QString &version); + QString getPlatformTag(); private: std::vector m_maintainers;