MainWindow: Help -> Check for updates

This commit is contained in:
tobtoht 2023-02-01 15:40:12 +01:00
parent a748eaf494
commit bc63a9d4f1
No known key found for this signature in database
GPG key ID: E45B10DD027D2472
8 changed files with 331 additions and 156 deletions

View file

@ -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<Updater>(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;

View file

@ -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<Updater> m_updater = nullptr;
};
#endif // FEATHER_MAINWINDOW_H

View file

@ -591,6 +591,7 @@
<string>Help</string>
</property>
<addaction name="actionAbout"/>
<addaction name="actionCheckForUpdates"/>
<addaction name="actionOfficialWebsite"/>
<addaction name="separator"/>
<addaction name="actionDocumentation"/>
@ -942,6 +943,11 @@
<string>Lock wallet</string>
</property>
</action>
<action name="actionCheckForUpdates">
<property name="text">
<string>Check for updates</string>
</property>
</action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<customwidgets>

View file

@ -6,6 +6,7 @@
#include <QFileDialog>
#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> 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);

View file

@ -7,6 +7,8 @@
#include <QDialog>
#include <QNetworkReply>
#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> 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::UpdateDialog> ui;
QSharedPointer<Updater> m_updater;
QString m_version;
QString m_downloadUrl;
QString m_hash;
QString m_signer;
QString m_platformTag;
QString m_updatePath;

View file

@ -6,12 +6,12 @@
<rect>
<x>0</x>
<y>0</y>
<width>569</width>
<height>148</height>
<width>540</width>
<height>144</height>
</rect>
</property>
<property name="windowTitle">
<string>Update Available</string>
<string>Updater</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
@ -26,59 +26,86 @@
<property name="text">
<string>Do you want to download and verify the new version?</string>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>0</number>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="btn_cancel">
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btn_download">
<property name="text">
<string>Download</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btn_installUpdate">
<property name="text">
<string>Install Update</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btn_restart">
<property name="text">
<string>Restart Feather</string>
</property>
</widget>
</item>
</layout>
<widget class="QFrame" name="frame">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="btn_cancel">
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btn_download">
<property name="text">
<string>Download</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btn_installUpdate">
<property name="text">
<string>Install Update</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btn_restart">
<property name="text">
<string>Restart Feather</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>

View file

@ -6,13 +6,154 @@
#include <common/util.h>
#include <openpgp/hash.h>
#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,

View file

@ -8,10 +8,20 @@
#include <openpgp/openpgp.h>
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<const uint8_t> data, const openpgp::signature_rsa &signature) const;
void onSignedHashesReceived(QNetworkReply *reply, const QString &platformTag, const QString &version);
QString getPlatformTag();
private:
std::vector<openpgp::public_key_block> m_maintainers;