fix: 优化客户端错误提示和校验诊断信息
This commit is contained in:
@@ -228,7 +228,7 @@ bool runElevatedSelfCommand(const QStringList& arguments, const QString& targetP
|
||||
const QMessageBox::StandardButton choice = QMessageBox::question(
|
||||
nullptr,
|
||||
QCoreApplication::translate("ConfigHelper", "Administrator Permission Required"),
|
||||
QCoreApplication::translate("ConfigHelper", "The current installation directory requires administrator permission to save configuration.\n\nTarget file: %1\nReason: %2\n\nClick OK, then choose Yes in the Windows permission confirmation dialog.")
|
||||
QCoreApplication::translate("ConfigHelper", "The current operation needs administrator permission to modify a protected file.\n\nTarget file: %1\nReason: %2\n\nClick OK, then choose Yes in the Windows permission confirmation dialog.")
|
||||
.arg(QDir::toNativeSeparators(targetPath), originalError),
|
||||
QMessageBox::Ok | QMessageBox::Cancel,
|
||||
QMessageBox::Ok);
|
||||
|
||||
@@ -257,12 +257,34 @@ bool DeviceIdentityHelper::ensureIssued(
|
||||
timer.stop();
|
||||
|
||||
const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
const QString networkError = reply->errorString();
|
||||
const QByteArray raw = reply->readAll();
|
||||
reply->deleteLater();
|
||||
if (status != 200) {
|
||||
m_error = QCoreApplication::translate("DeviceIdentityHelper", "Device identity request failed (HTTP %1): %2")
|
||||
.arg(status)
|
||||
.arg(QString::fromUtf8(raw));
|
||||
QJsonParseError responseError;
|
||||
const QJsonDocument errorDoc = QJsonDocument::fromJson(raw, &responseError);
|
||||
QString serverMessage;
|
||||
if (responseError.error == QJsonParseError::NoError && errorDoc.isObject()) {
|
||||
const QJsonValue detail = errorDoc.object().value(QStringLiteral("detail"));
|
||||
serverMessage = detail.isObject()
|
||||
? detail.toObject().value(QStringLiteral("msg")).toString()
|
||||
: detail.toString();
|
||||
}
|
||||
if (serverMessage.isEmpty())
|
||||
serverMessage = QString::fromUtf8(raw).trimmed();
|
||||
|
||||
if (status == 0) {
|
||||
m_error = QCoreApplication::translate(
|
||||
"DeviceIdentityHelper",
|
||||
"Cannot contact the update server to issue device identity.\nServer: %1\nApp: %2\nChannel: %3\nNetwork error: %4\nTimeout: %5 ms")
|
||||
.arg(trimmedBaseUrl, appId, channel, networkError, QString::number(timeoutMs));
|
||||
} else {
|
||||
m_error = QCoreApplication::translate(
|
||||
"DeviceIdentityHelper",
|
||||
"Device identity request was rejected by the update server.\nServer: %1\nHTTP status: %2\nApp: %3\nChannel: %4\nServer message: %5")
|
||||
.arg(trimmedBaseUrl, QString::number(status), appId, channel,
|
||||
serverMessage.isEmpty() ? QCoreApplication::translate("DeviceIdentityHelper", "<empty response>") : serverMessage);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
+100
-43
@@ -5,8 +5,9 @@
|
||||
#include <QDirIterator>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonArray>
|
||||
#include <QCoreApplication>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QSet>
|
||||
#ifdef HAVE_OPENSSL
|
||||
@@ -58,19 +59,32 @@ QString IntegrityHelper::sha256(const QString& filePath) const
|
||||
|
||||
bool IntegrityHelper::verifySignature(const QByteArray& payload, const QString& signatureBase64)
|
||||
{
|
||||
#ifndef HAVE_OPENSSL
|
||||
Q_UNUSED(payload); Q_UNUSED(signatureBase64); m_error = "OpenSSL unavailable"; return false;
|
||||
#else
|
||||
#ifndef HAVE_OPENSSL
|
||||
Q_UNUSED(payload); Q_UNUSED(signatureBase64);
|
||||
m_error = QCoreApplication::translate("IntegrityHelper",
|
||||
"Cannot verify signed manifest because OpenSSL support is unavailable. Stage: installed version verification.");
|
||||
return false;
|
||||
#else
|
||||
QString keyPath = QDir(m_installDir).filePath("config/manifest_public_key.pem");
|
||||
if (!QFile::exists(keyPath))
|
||||
keyPath = QFileInfo(ConfigHelper::instance().configPath()).dir().filePath("manifest_public_key.pem");
|
||||
QFile keyFile(keyPath);
|
||||
if (!keyFile.open(QIODevice::ReadOnly)) { m_error = "manifest public key missing"; return false; }
|
||||
if (!QFile::exists(keyPath))
|
||||
keyPath = QFileInfo(ConfigHelper::instance().configPath()).dir().filePath("manifest_public_key.pem");
|
||||
QFile keyFile(keyPath);
|
||||
if (!keyFile.open(QIODevice::ReadOnly)) {
|
||||
m_error = QCoreApplication::translate("IntegrityHelper",
|
||||
"Cannot open manifest public key. Stage: installed version verification. Public key path: %1.")
|
||||
.arg(keyPath);
|
||||
return false;
|
||||
}
|
||||
const QByteArray keyData = keyFile.readAll();
|
||||
BIO* bio = BIO_new_mem_buf(keyData.constData(), keyData.size());
|
||||
EVP_PKEY* key = bio ? PEM_read_bio_PUBKEY(bio, nullptr, nullptr, nullptr) : nullptr;
|
||||
if (bio) BIO_free(bio);
|
||||
if (!key) { m_error = "manifest public key invalid"; return false; }
|
||||
if (!key) {
|
||||
m_error = QCoreApplication::translate("IntegrityHelper",
|
||||
"Manifest public key is invalid. Stage: installed version verification. Public key path: %1.")
|
||||
.arg(keyPath);
|
||||
return false;
|
||||
}
|
||||
EVP_MD_CTX* ctx = EVP_MD_CTX_new();
|
||||
const QByteArray signature = QByteArray::fromBase64(signatureBase64.toUtf8());
|
||||
const bool ok = ctx && EVP_DigestVerifyInit(ctx, nullptr, EVP_sha256(), nullptr, key) == 1
|
||||
@@ -78,10 +92,13 @@ bool IntegrityHelper::verifySignature(const QByteArray& payload, const QString&
|
||||
&& EVP_DigestVerifyFinal(ctx, reinterpret_cast<const unsigned char*>(signature.constData()), signature.size()) == 1;
|
||||
if (ctx) EVP_MD_CTX_free(ctx);
|
||||
EVP_PKEY_free(key);
|
||||
if (!ok) m_error = "manifest RSA signature invalid";
|
||||
return ok;
|
||||
#endif
|
||||
}
|
||||
if (!ok) {
|
||||
m_error = QCoreApplication::translate("IntegrityHelper",
|
||||
"Manifest RSA signature is invalid. Stage: installed version verification. This usually means the cached manifest was changed, the client public key does not match the server private key, or the wrong version cache is being used.");
|
||||
}
|
||||
return ok;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool IntegrityHelper::verifyInstalledVersion(const QString& appId, const QString& channel,
|
||||
const QString& version)
|
||||
@@ -96,42 +113,79 @@ bool IntegrityHelper::verifyInstalledVersion(const QString& appId, const QString
|
||||
if (!QFile::exists(cachePath))
|
||||
cachePath = legacyCachePath;
|
||||
QFile cache(cachePath);
|
||||
if (!cache.open(QIODevice::ReadOnly)) { m_error = "signed manifest cache missing for " + version; return false; }
|
||||
if (!cache.open(QIODevice::ReadOnly)) {
|
||||
m_error = QCoreApplication::translate("IntegrityHelper",
|
||||
"Local signed manifest cache is missing. Stage: installed version verification. Version: %1. Expected cache file: %2. This cache is created after the same version is published or installed successfully.")
|
||||
.arg(version, cachePath);
|
||||
return false;
|
||||
}
|
||||
QJsonParseError wrapperError;
|
||||
const QJsonDocument wrapperDoc = QJsonDocument::fromJson(cache.readAll(), &wrapperError);
|
||||
if (wrapperError.error != QJsonParseError::NoError || !wrapperDoc.isObject()) {
|
||||
m_error = "manifest cache JSON invalid"; return false;
|
||||
}
|
||||
const QJsonDocument wrapperDoc = QJsonDocument::fromJson(cache.readAll(), &wrapperError);
|
||||
if (wrapperError.error != QJsonParseError::NoError || !wrapperDoc.isObject()) {
|
||||
m_error = QCoreApplication::translate("IntegrityHelper",
|
||||
"Local signed manifest cache is not valid JSON. Stage: installed version verification. Version: %1. File: %2. JSON error: %3.")
|
||||
.arg(version, cachePath, wrapperError.errorString());
|
||||
return false;
|
||||
}
|
||||
const QJsonObject wrapper = wrapperDoc.object();
|
||||
const QByteArray manifestText = wrapper.value("manifest_text").toString().toUtf8();
|
||||
const QString signature = wrapper.value("manifest").toObject().value("signature").toString();
|
||||
if (manifestText.isEmpty() || signature.isEmpty() || !verifySignature(manifestText, signature)) return false;
|
||||
if (manifestText.isEmpty() || signature.isEmpty()) {
|
||||
m_error = QCoreApplication::translate("IntegrityHelper",
|
||||
"Local signed manifest cache is incomplete. Stage: installed version verification. Version: %1. File: %2.")
|
||||
.arg(version, cachePath);
|
||||
return false;
|
||||
}
|
||||
if (!verifySignature(manifestText, signature)) return false;
|
||||
|
||||
QJsonParseError manifestError;
|
||||
const QJsonDocument manifestDoc = QJsonDocument::fromJson(manifestText, &manifestError);
|
||||
if (manifestError.error != QJsonParseError::NoError || !manifestDoc.isObject()) {
|
||||
m_error = "signed manifest payload invalid"; return false;
|
||||
}
|
||||
const QJsonDocument manifestDoc = QJsonDocument::fromJson(manifestText, &manifestError);
|
||||
if (manifestError.error != QJsonParseError::NoError || !manifestDoc.isObject()) {
|
||||
m_error = QCoreApplication::translate("IntegrityHelper",
|
||||
"Signed manifest payload is not valid JSON. Stage: installed version verification. Version: %1. File: %2. JSON error: %3.")
|
||||
.arg(version, cachePath, manifestError.errorString());
|
||||
return false;
|
||||
}
|
||||
const QJsonObject manifest = manifestDoc.object();
|
||||
if (manifest.value("app_id").toString() != appId
|
||||
|| manifest.value("channel").toString() != channel
|
||||
|| manifest.value("version").toString() != version) {
|
||||
m_error = "manifest identity does not match local application"; return false;
|
||||
}
|
||||
|| manifest.value("channel").toString() != channel
|
||||
|| manifest.value("version").toString() != version) {
|
||||
m_error = QCoreApplication::translate("IntegrityHelper",
|
||||
"Local signed manifest identity does not match this application. Stage: installed version verification. Expected app/channel/version: %1 / %2 / %3. Manifest app/channel/version: %4 / %5 / %6.")
|
||||
.arg(appId, channel, version,
|
||||
manifest.value("app_id").toString(),
|
||||
manifest.value("channel").toString(),
|
||||
manifest.value("version").toString());
|
||||
return false;
|
||||
}
|
||||
|
||||
QSet<QString> declaredExecutables;
|
||||
for (const QJsonValue& value : manifest.value("files").toArray()) {
|
||||
const QJsonObject item = value.toObject();
|
||||
const QString path = QDir::fromNativeSeparators(item.value("path").toString());
|
||||
if (!safeRelativePath(path)) { m_error = "unsafe manifest path: " + path; return false; }
|
||||
if (runtimeProtectedPath(path)) continue;
|
||||
const QString fullPath = QDir(m_installDir).filePath(path);
|
||||
if (!QFile::exists(fullPath)) { m_error = "required file missing: " + path; return false; }
|
||||
const QString expected = item.value("sha256").toString();
|
||||
const QString actual = sha256(fullPath);
|
||||
if (actual.isEmpty() || actual.compare(expected, Qt::CaseInsensitive) != 0) {
|
||||
m_error = "file hash mismatch: " + path; return false;
|
||||
}
|
||||
const QJsonObject item = value.toObject();
|
||||
const QString path = QDir::fromNativeSeparators(item.value("path").toString());
|
||||
if (!safeRelativePath(path)) {
|
||||
m_error = QCoreApplication::translate("IntegrityHelper",
|
||||
"Signed manifest contains an unsafe file path. Stage: installed version verification. Version: %1. Path: %2.")
|
||||
.arg(version, path);
|
||||
return false;
|
||||
}
|
||||
if (runtimeProtectedPath(path)) continue;
|
||||
const QString fullPath = QDir(m_installDir).filePath(path);
|
||||
if (!QFile::exists(fullPath)) {
|
||||
m_error = QCoreApplication::translate("IntegrityHelper",
|
||||
"A required installed file is missing. Stage: installed version verification. Version: %1. Manifest path: %2. Checked path: %3. The local installation no longer matches the published version.")
|
||||
.arg(version, path, fullPath);
|
||||
return false;
|
||||
}
|
||||
const QString expected = item.value("sha256").toString();
|
||||
const QString actual = sha256(fullPath);
|
||||
if (actual.isEmpty() || actual.compare(expected, Qt::CaseInsensitive) != 0) {
|
||||
m_error = QCoreApplication::translate("IntegrityHelper",
|
||||
"Installed file SHA-256 does not match the local signed manifest. Stage: installed version verification. Version: %1. Manifest path: %2. Local path: %3.\nExpected SHA-256: %4\nActual SHA-256: %5\nThis means the installed file is different from the version that was published or installed. If this is a developer test machine, check whether the local Release directory was recompiled or overwritten after publishing.")
|
||||
.arg(version, path, fullPath, expected,
|
||||
actual.isEmpty() ? QCoreApplication::translate("IntegrityHelper", "<cannot read file>") : actual);
|
||||
return false;
|
||||
}
|
||||
const QString suffix = QFileInfo(path).suffix().toCaseFolded();
|
||||
if (suffix == "exe" || suffix == "dll") declaredExecutables.insert(path.toCaseFolded());
|
||||
}
|
||||
@@ -150,10 +204,13 @@ bool IntegrityHelper::verifyInstalledVersion(const QString& appId, const QString
|
||||
|| folded.startsWith(runtimePrefix + "/update_temp/"));
|
||||
if (folded.startsWith("update/") || folded.startsWith("update_temp/")
|
||||
|| runtimeWorkDir || runtimeProtectedPath(relative)) continue;
|
||||
const QString suffix = QFileInfo(relative).suffix().toCaseFolded();
|
||||
if ((suffix == "exe" || suffix == "dll") && !declaredExecutables.contains(folded)) {
|
||||
m_error = "undeclared executable or plugin: " + relative; return false;
|
||||
}
|
||||
const QString suffix = QFileInfo(relative).suffix().toCaseFolded();
|
||||
if ((suffix == "exe" || suffix == "dll") && !declaredExecutables.contains(folded)) {
|
||||
m_error = QCoreApplication::translate("IntegrityHelper",
|
||||
"An executable or DLL exists locally but is not declared in the signed manifest. Stage: installed version verification. Version: %1. Extra file: %2. Remove unexpected executable/plugin files or publish a new version that declares them.")
|
||||
.arg(version, relative);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
+33
-18
@@ -1,5 +1,6 @@
|
||||
#include "LocalStateHelper.h"
|
||||
#include "ConfigHelper.h"
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QJsonDocument>
|
||||
@@ -29,30 +30,39 @@ bool LocalStateHelper::loadState(const QString& relativePath)
|
||||
{"last_success_version", QString()}
|
||||
};
|
||||
m_loaded = true;
|
||||
if (!saveState())
|
||||
{
|
||||
m_error = QString("Cannot write new state file: %1").arg(m_filePath);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
if (!saveState())
|
||||
{
|
||||
m_error = QCoreApplication::translate(
|
||||
"LocalStateHelper",
|
||||
"Cannot create local state file: %1. Error: %2.")
|
||||
.arg(m_filePath, m_error);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
{
|
||||
m_error = QString("Cannot open state file: %1").arg(m_filePath);
|
||||
return false;
|
||||
}
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
{
|
||||
m_error = QCoreApplication::translate(
|
||||
"LocalStateHelper",
|
||||
"Cannot open local state file: %1. Error: %2.")
|
||||
.arg(m_filePath, file.errorString());
|
||||
return false;
|
||||
}
|
||||
|
||||
QByteArray raw = file.readAll();
|
||||
file.close();
|
||||
|
||||
QJsonParseError parseError;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(raw, &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !doc.isObject())
|
||||
{
|
||||
m_error = QString("Invalid state JSON: %1").arg(parseError.errorString());
|
||||
return false;
|
||||
}
|
||||
if (parseError.error != QJsonParseError::NoError || !doc.isObject())
|
||||
{
|
||||
m_error = QCoreApplication::translate(
|
||||
"LocalStateHelper",
|
||||
"Local state file is not valid JSON. File: %1. JSON error: %2.")
|
||||
.arg(m_filePath, parseError.errorString());
|
||||
return false;
|
||||
}
|
||||
|
||||
m_state = doc.object();
|
||||
m_loaded = true;
|
||||
@@ -63,7 +73,9 @@ bool LocalStateHelper::saveState() const
|
||||
{
|
||||
if (!m_loaded)
|
||||
{
|
||||
m_error = "State is not loaded";
|
||||
m_error = QCoreApplication::translate(
|
||||
"LocalStateHelper",
|
||||
"Local state has not been loaded.");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -71,7 +83,10 @@ bool LocalStateHelper::saveState() const
|
||||
QString writeError;
|
||||
if (!ConfigHelper::writeFileWithElevationIfNeeded(m_filePath, doc.toJson(QJsonDocument::Indented), &writeError))
|
||||
{
|
||||
m_error = QString("Cannot save state file: %1").arg(writeError);
|
||||
m_error = QCoreApplication::translate(
|
||||
"LocalStateHelper",
|
||||
"Cannot save local state file: %1. Error: %2.")
|
||||
.arg(m_filePath, writeError);
|
||||
return false;
|
||||
}
|
||||
m_error.clear();
|
||||
|
||||
+79
-26
@@ -1,6 +1,7 @@
|
||||
#include "PolicyHelper.h"
|
||||
#include "ConfigHelper.h"
|
||||
#include <QApplication>
|
||||
#include <QCoreApplication>
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
@@ -29,15 +30,26 @@ static QString resolvePolicyPath(const QString& baseDir, const QString& relative
|
||||
|
||||
bool PolicyHelper::loadPolicy(const QString& relativePath)
|
||||
{
|
||||
QFile file(resolvePolicyPath(m_baseDir, relativePath, false));
|
||||
if (!file.open(QIODevice::ReadOnly)) { m_error = "Cannot open policy file"; return false; }
|
||||
QJsonParseError error;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error);
|
||||
if (error.error != QJsonParseError::NoError || !doc.isObject()) {
|
||||
m_error = "Invalid policy JSON: " + error.errorString(); return false;
|
||||
}
|
||||
return loadPolicyObject(doc.object());
|
||||
}
|
||||
const QString path = resolvePolicyPath(m_baseDir, relativePath, false);
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
m_error = QCoreApplication::translate(
|
||||
"PolicyHelper",
|
||||
"Cannot open signed version policy file: %1. Error: %2.")
|
||||
.arg(path, file.errorString());
|
||||
return false;
|
||||
}
|
||||
QJsonParseError error;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error);
|
||||
if (error.error != QJsonParseError::NoError || !doc.isObject()) {
|
||||
m_error = QCoreApplication::translate(
|
||||
"PolicyHelper",
|
||||
"Signed version policy is not valid JSON. File: %1. JSON error: %2.")
|
||||
.arg(path, error.errorString());
|
||||
return false;
|
||||
}
|
||||
return loadPolicyObject(doc.object());
|
||||
}
|
||||
|
||||
bool PolicyHelper::loadPolicyObject(const QJsonObject& policy, const QString& signedText)
|
||||
{
|
||||
@@ -51,7 +63,10 @@ bool PolicyHelper::savePolicy(const QString& relativePath) const
|
||||
QString writeError;
|
||||
const QString path = resolvePolicyPath(m_baseDir, relativePath, true);
|
||||
if (!ConfigHelper::writeFileWithElevationIfNeeded(path, bytes, &writeError)) {
|
||||
m_error = "Cannot save policy file: " + writeError;
|
||||
m_error = QCoreApplication::translate(
|
||||
"PolicyHelper",
|
||||
"Cannot save signed version policy file: %1. Error: %2.")
|
||||
.arg(path, writeError);
|
||||
return false;
|
||||
}
|
||||
m_error.clear();
|
||||
@@ -84,35 +99,73 @@ QByteArray PolicyHelper::canonicalPolicyBytes(const QJsonObject& policy) const
|
||||
|
||||
bool PolicyHelper::verifySignature(const QByteArray& payload, const QString& signatureBase64) const
|
||||
{
|
||||
#ifndef HAVE_OPENSSL
|
||||
Q_UNUSED(payload); Q_UNUSED(signatureBase64); m_error = "OpenSSL unavailable"; return false;
|
||||
#else
|
||||
QString keyPath = m_baseDir + "/config/manifest_public_key.pem";
|
||||
QFile keyFile(keyPath);
|
||||
if (!keyFile.open(QIODevice::ReadOnly)) { m_error = "Cannot open policy public key"; return false; }
|
||||
#ifndef HAVE_OPENSSL
|
||||
Q_UNUSED(payload);
|
||||
Q_UNUSED(signatureBase64);
|
||||
m_error = QCoreApplication::translate(
|
||||
"PolicyHelper",
|
||||
"OpenSSL is unavailable, so the signed version policy cannot be verified.");
|
||||
return false;
|
||||
#else
|
||||
QString keyPath = m_baseDir + "/config/manifest_public_key.pem";
|
||||
QFile keyFile(keyPath);
|
||||
if (!keyFile.open(QIODevice::ReadOnly)) {
|
||||
m_error = QCoreApplication::translate(
|
||||
"PolicyHelper",
|
||||
"Cannot open version policy public key: %1. Error: %2.")
|
||||
.arg(keyPath, keyFile.errorString());
|
||||
return false;
|
||||
}
|
||||
const QByteArray keyData = keyFile.readAll();
|
||||
BIO* bio = BIO_new_mem_buf(keyData.constData(), keyData.size());
|
||||
EVP_PKEY* key = bio ? PEM_read_bio_PUBKEY(bio, nullptr, nullptr, nullptr) : nullptr;
|
||||
if (bio) BIO_free(bio);
|
||||
if (!key) { m_error = "Invalid policy public key"; return false; }
|
||||
if (!key) {
|
||||
m_error = QCoreApplication::translate(
|
||||
"PolicyHelper",
|
||||
"Version policy public key is invalid: %1.")
|
||||
.arg(keyPath);
|
||||
return false;
|
||||
}
|
||||
EVP_MD_CTX* ctx = EVP_MD_CTX_new();
|
||||
const QByteArray signature = QByteArray::fromBase64(signatureBase64.toUtf8());
|
||||
bool ok = ctx && EVP_DigestVerifyInit(ctx, nullptr, EVP_sha256(), nullptr, key) == 1
|
||||
&& EVP_DigestVerifyUpdate(ctx, payload.constData(), payload.size()) == 1
|
||||
&& EVP_DigestVerifyFinal(ctx, reinterpret_cast<const unsigned char*>(signature.constData()), signature.size()) == 1;
|
||||
if (ctx) EVP_MD_CTX_free(ctx); EVP_PKEY_free(key);
|
||||
if (!ok) m_error = "Invalid RSA policy signature";
|
||||
return ok;
|
||||
#endif
|
||||
}
|
||||
if (!ok) {
|
||||
m_error = QCoreApplication::translate(
|
||||
"PolicyHelper",
|
||||
"Version policy RSA signature is invalid. The policy file may have been changed, or the public key does not match the server private key.");
|
||||
}
|
||||
return ok;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool PolicyHelper::isValid() const
|
||||
{
|
||||
if (!m_loaded) return false;
|
||||
const QStringList required{"app_id","channel","current_version","policy_seq","allow_run",
|
||||
"force_update","allow_rollback","offline_allowed","valid_until","signature_alg","key_id","signature"};
|
||||
for (const QString& key : required) if (!m_policy.contains(key)) { m_error = "Missing policy field: " + key; return false; }
|
||||
if (m_policy.value("signature_alg").toString() != "RSA-2048-SHA256") return false;
|
||||
if (!m_loaded) {
|
||||
m_error = QCoreApplication::translate("PolicyHelper", "Version policy has not been loaded.");
|
||||
return false;
|
||||
}
|
||||
const QStringList required{"app_id","channel","current_version","policy_seq","allow_run",
|
||||
"force_update","allow_rollback","offline_allowed","valid_until","signature_alg","key_id","signature"};
|
||||
for (const QString& key : required) {
|
||||
if (!m_policy.contains(key)) {
|
||||
m_error = QCoreApplication::translate(
|
||||
"PolicyHelper",
|
||||
"Version policy is missing required field: %1.")
|
||||
.arg(key);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (m_policy.value("signature_alg").toString() != "RSA-2048-SHA256") {
|
||||
m_error = QCoreApplication::translate(
|
||||
"PolicyHelper",
|
||||
"Version policy signature algorithm is unsupported: %1.")
|
||||
.arg(m_policy.value("signature_alg").toString());
|
||||
return false;
|
||||
}
|
||||
const QByteArray payload = m_signedText.isEmpty() ? canonicalPolicyBytes(m_policy) : m_signedText.toUtf8();
|
||||
return verifySignature(payload, m_policy.value("signature").toString());
|
||||
}
|
||||
|
||||
+106
-36
@@ -5,10 +5,11 @@
|
||||
#include <QFile>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QMessageAuthenticationCode>
|
||||
#include <QSaveFile>
|
||||
#include <QStandardPaths>
|
||||
#include <QUuid>
|
||||
#include <QMessageAuthenticationCode>
|
||||
#include <QSaveFile>
|
||||
#include <QStandardPaths>
|
||||
#include <QStringList>
|
||||
#include <QUuid>
|
||||
|
||||
static QByteArray ticketMac(const QJsonObject& payload, const QString& secret)
|
||||
{
|
||||
@@ -22,10 +23,20 @@ bool TicketHelper::createTicket(const QString& appId, const QString& deviceId,
|
||||
{
|
||||
// Launcher 启动业务主程序前生成一次性 ticket。
|
||||
// ticket 只保存在临时目录、有效期 60 秒,并用 launch_token 做 HMAC,防止用户绕过 Launcher 直接启动主程序。
|
||||
if (appId.isEmpty() || version.isEmpty() || secret.isEmpty()) {
|
||||
if (errorMessage) *errorMessage = "ticket identity or secret is empty";
|
||||
return false;
|
||||
}
|
||||
if (appId.isEmpty() || deviceId.isEmpty() || version.isEmpty() || secret.isEmpty()) {
|
||||
if (errorMessage) {
|
||||
QStringList missing;
|
||||
if (appId.isEmpty()) missing.append(QStringLiteral("app_id"));
|
||||
if (deviceId.isEmpty()) missing.append(QStringLiteral("device_id"));
|
||||
if (version.isEmpty()) missing.append(QStringLiteral("current_version"));
|
||||
if (secret.isEmpty()) missing.append(QStringLiteral("launch_token"));
|
||||
*errorMessage = QCoreApplication::translate(
|
||||
"TicketHelper",
|
||||
"Cannot create launch ticket because required fields are empty: %1. Check app_config.json, registry-imported configuration and device authorization.")
|
||||
.arg(missing.join(QStringLiteral(", ")));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const QDateTime now = QDateTime::currentDateTimeUtc();
|
||||
QJsonObject payload{
|
||||
{"app_id", appId}, {"device_id", deviceId}, {"version", version},
|
||||
@@ -36,14 +47,27 @@ bool TicketHelper::createTicket(const QString& appId, const QString& deviceId,
|
||||
};
|
||||
QJsonObject wrapper{{"payload", payload}, {"signature", QString::fromLatin1(ticketMac(payload, secret))}};
|
||||
const QString dirPath = QDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation)).filePath("marsco_tickets");
|
||||
if (!QDir().mkpath(dirPath)) { if (errorMessage) *errorMessage = "cannot create ticket directory"; return false; }
|
||||
if (!QDir().mkpath(dirPath)) {
|
||||
if (errorMessage) {
|
||||
*errorMessage = QCoreApplication::translate(
|
||||
"TicketHelper",
|
||||
"Cannot create launch ticket directory: %1.")
|
||||
.arg(dirPath);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const QString path = QDir(dirPath).filePath("ticket_" + QUuid::createUuid().toString(QUuid::WithoutBraces) + ".json");
|
||||
QSaveFile file(path);
|
||||
const QByteArray bytes = QJsonDocument(wrapper).toJson(QJsonDocument::Compact);
|
||||
if (!file.open(QIODevice::WriteOnly) || file.write(bytes) != bytes.size() || !file.commit()) {
|
||||
if (errorMessage) *errorMessage = "cannot save ticket";
|
||||
return false;
|
||||
}
|
||||
QSaveFile file(path);
|
||||
const QByteArray bytes = QJsonDocument(wrapper).toJson(QJsonDocument::Compact);
|
||||
if (!file.open(QIODevice::WriteOnly) || file.write(bytes) != bytes.size() || !file.commit()) {
|
||||
if (errorMessage) {
|
||||
*errorMessage = QCoreApplication::translate(
|
||||
"TicketHelper",
|
||||
"Cannot save launch ticket file: %1. Error: %2.")
|
||||
.arg(path, file.errorString());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
QFile::setPermissions(path, QFileDevice::ReadOwner | QFileDevice::WriteOwner);
|
||||
if (ticketPath) *ticketPath = path;
|
||||
return true;
|
||||
@@ -56,24 +80,40 @@ bool TicketHelper::consumeAndVerify(const QString& ticketPath, const QString& ex
|
||||
// 主程序启动后第一时间把 ticket 改名成 .consuming,再读取并删除。
|
||||
// 这样同一张 ticket 即使校验失败也不能被重复使用,避免重放启动。
|
||||
const QString consumingPath = ticketPath + ".consuming."
|
||||
+ QString::number(QCoreApplication::applicationPid());
|
||||
if (!QFile::rename(ticketPath, consumingPath)) {
|
||||
if (errorMessage) *errorMessage = "ticket missing or already consumed";
|
||||
return false;
|
||||
}
|
||||
QFile file(consumingPath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
QFile::remove(consumingPath);
|
||||
if (errorMessage) *errorMessage = "cannot read claimed ticket";
|
||||
return false;
|
||||
}
|
||||
+ QString::number(QCoreApplication::applicationPid());
|
||||
if (!QFile::rename(ticketPath, consumingPath)) {
|
||||
if (errorMessage) {
|
||||
*errorMessage = QCoreApplication::translate(
|
||||
"TicketHelper",
|
||||
"Launch ticket is missing or has already been consumed. Ticket file: %1. Please start the application from Launcher.")
|
||||
.arg(ticketPath);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
QFile file(consumingPath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
QFile::remove(consumingPath);
|
||||
if (errorMessage) {
|
||||
*errorMessage = QCoreApplication::translate(
|
||||
"TicketHelper",
|
||||
"Cannot read claimed launch ticket: %1. Error: %2.")
|
||||
.arg(consumingPath, file.errorString());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const QByteArray raw = file.readAll(); file.close();
|
||||
QFile::remove(consumingPath); // Consume once; it must not be replayed regardless of success or failure.
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(raw, &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
|
||||
if (errorMessage) *errorMessage = "invalid ticket JSON"; return false;
|
||||
}
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(raw, &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
|
||||
if (errorMessage) {
|
||||
*errorMessage = QCoreApplication::translate(
|
||||
"TicketHelper",
|
||||
"Launch ticket is not valid JSON. JSON error: %1.")
|
||||
.arg(parseError.errorString());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const QJsonObject wrapper = doc.object();
|
||||
const QJsonObject payload = wrapper.value("payload").toObject();
|
||||
const QByteArray actual = wrapper.value("signature").toString().toLatin1();
|
||||
@@ -84,27 +124,57 @@ bool TicketHelper::consumeAndVerify(const QString& ticketPath, const QString& ex
|
||||
const bool timeOk = issued.isValid() && expires.isValid() && issued <= now.addSecs(5)
|
||||
&& expires >= now && issued.secsTo(expires) <= 65;
|
||||
if (actual.isEmpty() || actual != expected) {
|
||||
if (errorMessage) *errorMessage = "ticket signature invalid; check launch_token";
|
||||
if (errorMessage) {
|
||||
*errorMessage = QCoreApplication::translate(
|
||||
"TicketHelper",
|
||||
"Launch ticket signature is invalid. The launch_token used by Launcher and the main application is inconsistent, or the ticket content was changed.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (payload.value("app_id").toString() != expectedAppId) {
|
||||
if (errorMessage) *errorMessage = "ticket app_id mismatch";
|
||||
if (errorMessage) {
|
||||
*errorMessage = QCoreApplication::translate(
|
||||
"TicketHelper",
|
||||
"Launch ticket app_id does not match. Ticket app_id: %1. Expected app_id: %2.")
|
||||
.arg(payload.value("app_id").toString(), expectedAppId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (payload.value("device_id").toString() != expectedDeviceId) {
|
||||
if (errorMessage) *errorMessage = "ticket device_id mismatch";
|
||||
if (errorMessage) {
|
||||
*errorMessage = QCoreApplication::translate(
|
||||
"TicketHelper",
|
||||
"Launch ticket device_id does not match. Ticket device_id: %1. Expected device_id: %2. Reauthorize the device from Launcher if the configuration was regenerated.")
|
||||
.arg(payload.value("device_id").toString(), expectedDeviceId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (payload.value("version").toString() != expectedVersion) {
|
||||
if (errorMessage) *errorMessage = "ticket version mismatch";
|
||||
if (errorMessage) {
|
||||
*errorMessage = QCoreApplication::translate(
|
||||
"TicketHelper",
|
||||
"Launch ticket version does not match. Ticket version: %1. Expected version: %2.")
|
||||
.arg(payload.value("version").toString(), expectedVersion);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (!timeOk) {
|
||||
if (errorMessage) *errorMessage = "ticket time invalid or expired";
|
||||
if (errorMessage) {
|
||||
*errorMessage = QCoreApplication::translate(
|
||||
"TicketHelper",
|
||||
"Launch ticket time is invalid or expired. Issued at: %1. Expires at: %2. Current UTC time: %3.")
|
||||
.arg(payload.value("issued_at").toString(),
|
||||
payload.value("expires_at").toString(),
|
||||
now.toString(Qt::ISODate));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (payload.value("nonce").toString().isEmpty()) {
|
||||
if (errorMessage) *errorMessage = "ticket nonce missing";
|
||||
if (errorMessage) {
|
||||
*errorMessage = QCoreApplication::translate(
|
||||
"TicketHelper",
|
||||
"Launch ticket nonce is missing. The ticket is incomplete.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
+52
-32
@@ -104,11 +104,13 @@ void UpdateLogic::getDownloadUrl(const QString& appId, const QString& channel, c
|
||||
bool UpdateLogic::cacheManifest(const QString& appId, const QString& channel, const QString& version,
|
||||
int versionId, const QString& cacheDir)
|
||||
{
|
||||
m_error.clear();
|
||||
if (appId.isEmpty() || channel.isEmpty() || version.isEmpty() || versionId <= 0) {
|
||||
m_error = "manifest identity is incomplete";
|
||||
return false;
|
||||
}
|
||||
m_error.clear();
|
||||
if (appId.isEmpty() || channel.isEmpty() || version.isEmpty() || versionId <= 0) {
|
||||
m_error = QCoreApplication::translate("UpdateLogic",
|
||||
"Current version manifest identity is incomplete. Stage: cache current version manifest. App: %1, channel: %2, version: %3, version id: %4.")
|
||||
.arg(appId, channel, version, QString::number(versionId));
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonObject response;
|
||||
int statusCode = 0;
|
||||
@@ -122,40 +124,58 @@ bool UpdateLogic::cacheManifest(const QString& appId, const QString& channel, co
|
||||
statusCode = code;
|
||||
response = resp;
|
||||
});
|
||||
|
||||
if (statusCode != 200) {
|
||||
const QJsonValue detail = response.value(QStringLiteral("detail"));
|
||||
const QString message = detail.isObject()
|
||||
? detail.toObject().value(QStringLiteral("msg")).toString()
|
||||
: detail.toString();
|
||||
m_error = QCoreApplication::translate("UpdateLogic",
|
||||
"Cannot download signed manifest for the current local version. Stage: cache current version manifest. HTTP status: %1. App: %2, channel: %3, version: %4, version id: %5.%6")
|
||||
.arg(QString::number(statusCode), appId, channel, version, QString::number(versionId),
|
||||
message.isEmpty() ? QString() : QCoreApplication::translate("UpdateLogic", "\nServer message: %1").arg(message));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (statusCode != 200) {
|
||||
m_error = QString("manifest request failed (HTTP %1)").arg(statusCode);
|
||||
return false;
|
||||
}
|
||||
|
||||
const QJsonObject manifest = response.value("manifest").toObject();
|
||||
const QString manifestText = response.value("manifest_text").toString();
|
||||
if (manifest.isEmpty() || manifestText.isEmpty()) {
|
||||
m_error = "manifest response is incomplete";
|
||||
return false;
|
||||
}
|
||||
const QJsonObject manifest = response.value("manifest").toObject();
|
||||
const QString manifestText = response.value("manifest_text").toString();
|
||||
if (manifest.isEmpty() || manifestText.isEmpty()) {
|
||||
m_error = QCoreApplication::translate("UpdateLogic",
|
||||
"The signed manifest response for the current local version is incomplete. Stage: cache current version manifest. App: %1, channel: %2, version: %3, version id: %4.")
|
||||
.arg(appId, channel, version, QString::number(versionId));
|
||||
return false;
|
||||
}
|
||||
if (manifest.value("app_id").toString() != appId
|
||||
|| manifest.value("channel").toString() != channel
|
||||
|| manifest.value("version").toString() != version) {
|
||||
m_error = "manifest identity does not match current version";
|
||||
return false;
|
||||
}
|
||||
|| manifest.value("channel").toString() != channel
|
||||
|| manifest.value("version").toString() != version) {
|
||||
m_error = QCoreApplication::translate("UpdateLogic",
|
||||
"The signed manifest identity does not match the current local version. Stage: cache current version manifest. Expected app/channel/version: %1 / %2 / %3. Manifest app/channel/version: %4 / %5 / %6.")
|
||||
.arg(appId, channel, version,
|
||||
manifest.value("app_id").toString(),
|
||||
manifest.value("channel").toString(),
|
||||
manifest.value("version").toString());
|
||||
return false;
|
||||
}
|
||||
|
||||
QDir dir(cacheDir);
|
||||
if (!dir.exists() && !dir.mkpath(".")) {
|
||||
m_error = "cannot create manifest cache directory";
|
||||
return false;
|
||||
}
|
||||
QDir dir(cacheDir);
|
||||
if (!dir.exists() && !dir.mkpath(".")) {
|
||||
m_error = QCoreApplication::translate("UpdateLogic",
|
||||
"Cannot create manifest cache directory. Stage: cache current version manifest. Directory: %1.")
|
||||
.arg(cacheDir);
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonObject wrapper;
|
||||
wrapper["manifest"] = manifest;
|
||||
wrapper["manifest_text"] = manifestText;
|
||||
QSaveFile file(dir.filePath("manifest_" + version + ".json"));
|
||||
const QByteArray bytes = QJsonDocument(wrapper).toJson(QJsonDocument::Indented);
|
||||
if (!file.open(QIODevice::WriteOnly) || file.write(bytes) != bytes.size() || !file.commit()) {
|
||||
m_error = "cannot save signed manifest cache";
|
||||
return false;
|
||||
}
|
||||
QSaveFile file(dir.filePath("manifest_" + version + ".json"));
|
||||
const QByteArray bytes = QJsonDocument(wrapper).toJson(QJsonDocument::Indented);
|
||||
if (!file.open(QIODevice::WriteOnly) || file.write(bytes) != bytes.size() || !file.commit()) {
|
||||
m_error = QCoreApplication::translate("UpdateLogic",
|
||||
"Cannot save signed manifest cache. Stage: cache current version manifest. File: %1. Error: %2.")
|
||||
.arg(file.fileName(), file.errorString());
|
||||
return false;
|
||||
}
|
||||
qDebug() << "Current version manifest cached to" << file.fileName();
|
||||
return true;
|
||||
}
|
||||
|
||||
+82
-26
@@ -176,9 +176,29 @@ int main(int argc, char* argv[])
|
||||
QCoreApplication::translate("Launcher", "Select Offline Update Package"),
|
||||
QString(),
|
||||
QCoreApplication::translate("Launcher", "Marsco offline update package (*.upd)"));
|
||||
return package.isEmpty() ? false : QProcess::startDetached(updaterPath, QStringList{QString("--offline-package=%1").arg(package)});
|
||||
if (package.isEmpty())
|
||||
return false;
|
||||
if (!QFileInfo::exists(updaterPath)) {
|
||||
QMessageBox::critical(nullptr,
|
||||
QCoreApplication::translate("Launcher", "Updater Startup Failed"),
|
||||
QCoreApplication::translate(
|
||||
"Launcher",
|
||||
"Cannot import the offline update package because the updater executable does not exist.\nUpdater: %1\nOffline package: %2")
|
||||
.arg(updaterPath, package));
|
||||
return false;
|
||||
}
|
||||
if (!QProcess::startDetached(updaterPath, QStringList{QString("--offline-package=%1").arg(package)})) {
|
||||
QMessageBox::critical(nullptr,
|
||||
QCoreApplication::translate("Launcher", "Updater Startup Failed"),
|
||||
QCoreApplication::translate(
|
||||
"Launcher",
|
||||
"Cannot start the updater for offline package import.\nUpdater: %1\nOffline package: %2\nCheck file permissions and dependent DLLs/shared libraries.")
|
||||
.arg(updaterPath, package));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const QString deviceId = config.getValue("Update", "device_id");
|
||||
const QString deviceId = config.getValue("Update", "device_id");
|
||||
if (QCoreApplication::arguments().contains("--import-offline")) {
|
||||
progress.close();
|
||||
if (!importOfflinePackage())
|
||||
@@ -188,21 +208,40 @@ int main(int argc, char* argv[])
|
||||
return 0;
|
||||
}
|
||||
|
||||
QString mainStartupError;
|
||||
const auto startMainApp = [&]() {
|
||||
// 只允许 Launcher 启动业务主程序:先生成一次性 ticket,再通过 --ticket-file 传给主程序。
|
||||
// 业务主程序必须消费并校验 ticket,避免用户直接双击 SimCAE/MainApp 绕过更新和授权检查。
|
||||
mainStartupError.clear();
|
||||
if (!QFileInfo::exists(mainAppPath)) {
|
||||
mainStartupError = QCoreApplication::translate(
|
||||
"Launcher",
|
||||
"Cannot start the main application because the executable file does not exist.\nExecutable: %1\nCheck main_executable and install_root in the generated client configuration.")
|
||||
.arg(mainAppPath);
|
||||
return false;
|
||||
}
|
||||
QString ticketPath;
|
||||
QString ticketError;
|
||||
if (!TicketHelper::createTicket(logic.getAppId(), deviceId, currentVersion,
|
||||
launchToken, &ticketPath, &ticketError)) {
|
||||
qDebug() << "Cannot create launch ticket:" << ticketError;
|
||||
return false;
|
||||
}
|
||||
const bool started = QProcess::startDetached(mainAppPath,
|
||||
QStringList{QString("--ticket-file=%1").arg(ticketPath)});
|
||||
if (!started) QFile::remove(ticketPath);
|
||||
return started;
|
||||
};
|
||||
QString ticketError;
|
||||
if (!TicketHelper::createTicket(logic.getAppId(), deviceId, currentVersion,
|
||||
launchToken, &ticketPath, &ticketError)) {
|
||||
qDebug() << "Cannot create launch ticket:" << ticketError;
|
||||
mainStartupError = QCoreApplication::translate(
|
||||
"Launcher",
|
||||
"Cannot start the main application because the one-time launch ticket could not be created.\nExecutable: %1\nDetails: %2")
|
||||
.arg(mainAppPath, ticketError);
|
||||
return false;
|
||||
}
|
||||
const bool started = QProcess::startDetached(mainAppPath,
|
||||
QStringList{QString("--ticket-file=%1").arg(ticketPath)});
|
||||
if (!started) {
|
||||
QFile::remove(ticketPath);
|
||||
mainStartupError = QCoreApplication::translate(
|
||||
"Launcher",
|
||||
"Cannot start the main application process.\nExecutable: %1\nTicket file: %2\nCheck file permissions, dependent DLLs/shared libraries, and whether the executable can run independently.")
|
||||
.arg(mainAppPath, ticketPath);
|
||||
}
|
||||
return started;
|
||||
};
|
||||
|
||||
progress.setLabelText(QCoreApplication::translate("Launcher", "Verifying local runtime policy..."));
|
||||
QApplication::processEvents();
|
||||
@@ -294,23 +333,38 @@ int main(int argc, char* argv[])
|
||||
accepted = QMessageBox::question(nullptr, dialogTitle, prompt,
|
||||
QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes;
|
||||
}
|
||||
if (!accepted) {
|
||||
if (!accepted) {
|
||||
if (!startMainApp()) {
|
||||
QMessageBox::critical(nullptr,
|
||||
QCoreApplication::translate("Launcher", "Startup Failed"),
|
||||
QCoreApplication::translate("Launcher", "Cannot start the main application: %1").arg(mainAppPath));
|
||||
mainStartupError.isEmpty()
|
||||
? QCoreApplication::translate("Launcher", "Cannot start the main application: %1").arg(mainAppPath)
|
||||
: mainStartupError);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
const QStringList updaterArgs{appId, channel, latestVer, QString::number(targetVersionId)};
|
||||
return 0;
|
||||
}
|
||||
const QStringList updaterArgs{appId, channel, latestVer, QString::number(targetVersionId)};
|
||||
if (!QFileInfo::exists(updaterPath))
|
||||
{
|
||||
QMessageBox::critical(nullptr,
|
||||
QCoreApplication::translate("Launcher", "Updater Startup Failed"),
|
||||
QCoreApplication::translate(
|
||||
"Launcher",
|
||||
"Cannot start the updater because the executable file does not exist.\nExecutable: %1\nCheck updater_executable and install_root in the generated client configuration.")
|
||||
.arg(updaterPath));
|
||||
return -1;
|
||||
}
|
||||
if (!QProcess::startDetached(updaterPath, updaterArgs))
|
||||
{
|
||||
QMessageBox::critical(nullptr,
|
||||
QCoreApplication::translate("Launcher", "Updater Startup Failed"),
|
||||
QCoreApplication::translate("Launcher", "Cannot start the updater: %1").arg(updaterPath));
|
||||
QCoreApplication::translate(
|
||||
"Launcher",
|
||||
"Cannot start the updater process.\nExecutable: %1\nArguments: %2\nCheck file permissions, dependent DLLs/shared libraries, and whether the updater can run independently.")
|
||||
.arg(updaterPath, updaterArgs.join(QStringLiteral(" "))));
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -357,14 +411,16 @@ int main(int argc, char* argv[])
|
||||
? QCoreApplication::translate("Launcher", "The application is up to date. Starting...")
|
||||
: QCoreApplication::translate("Launcher", "Offline mode is active. Starting..."));
|
||||
QApplication::processEvents();
|
||||
if (!startMainApp())
|
||||
{
|
||||
progress.close();
|
||||
if (!startMainApp())
|
||||
{
|
||||
progress.close();
|
||||
QMessageBox::critical(nullptr,
|
||||
QCoreApplication::translate("Launcher", "Startup Failed"),
|
||||
QCoreApplication::translate("Launcher", "Cannot start the main application: %1").arg(mainAppPath));
|
||||
return -1;
|
||||
}
|
||||
mainStartupError.isEmpty()
|
||||
? QCoreApplication::translate("Launcher", "Cannot start the main application: %1").arg(mainAppPath)
|
||||
: mainStartupError);
|
||||
return -1;
|
||||
}
|
||||
progress.close();
|
||||
return 0;
|
||||
}
|
||||
|
||||
+8
-6
@@ -97,12 +97,14 @@ int main(int argc, char* argv[])
|
||||
if (!integrity.verifyInstalledVersion(
|
||||
config.getValue("App", "app_id"), config.getValue("App", "channel"),
|
||||
config.getValue("App", "current_version")))
|
||||
{
|
||||
QMessageBox::critical(nullptr, "Integrity Check Failed",
|
||||
QString("Application files failed signed Manifest verification:\n%1")
|
||||
.arg(integrity.errorString()));
|
||||
return -1;
|
||||
}
|
||||
{
|
||||
QMessageBox::critical(nullptr, "Integrity Check Failed",
|
||||
QString("Startup blocked because the installed files failed local signed Manifest verification.\n"
|
||||
"This is not a download check; it means the current installation directory does not match the signed Manifest cache for this version.\n\n"
|
||||
"Details:\n%1")
|
||||
.arg(integrity.errorString()));
|
||||
return -1;
|
||||
}
|
||||
|
||||
MainWindow w;
|
||||
w.show();
|
||||
|
||||
+367
-209
@@ -25,16 +25,32 @@
|
||||
#include <openssl/err.h>
|
||||
#endif
|
||||
|
||||
UpdaterLogic::UpdaterLogic(QObject* parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
m_serverAddr = ConfigHelper::instance().getValue("Server", "api_base_url");
|
||||
}
|
||||
UpdaterLogic::UpdaterLogic(QObject* parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
m_serverAddr = ConfigHelper::instance().getValue("Server", "api_base_url");
|
||||
}
|
||||
|
||||
namespace {
|
||||
QString serverDetailMessage(const QJsonObject& response)
|
||||
{
|
||||
const QJsonValue detail = response.value(QStringLiteral("detail"));
|
||||
if (detail.isObject()) {
|
||||
const QJsonObject obj = detail.toObject();
|
||||
const QString msg = obj.value(QStringLiteral("msg")).toString();
|
||||
if (!msg.isEmpty()) return msg;
|
||||
const QString error = obj.value(QStringLiteral("error")).toString();
|
||||
if (!error.isEmpty()) return error;
|
||||
}
|
||||
return detail.toString();
|
||||
}
|
||||
}
|
||||
|
||||
void UpdaterLogic::getManifest(const QString& appId, const QString& channel, const QString& targetVer, int versionId)
|
||||
{
|
||||
// Manifest 由服务端按版本动态生成,描述目标版本包含哪些文件以及每个文件的 SHA256。
|
||||
// Updater 先拿到 Manifest,再请求下载 URL,最后按 Manifest 校验本地文件。
|
||||
m_error.clear();
|
||||
QString url = m_serverAddr + "/api/v1/update/manifest";
|
||||
QJsonObject body;
|
||||
body["app_id"] = appId;
|
||||
@@ -42,7 +58,7 @@ void UpdaterLogic::getManifest(const QString& appId, const QString& channel, con
|
||||
body["version"] = targetVer;
|
||||
body["version_id"] = versionId;
|
||||
|
||||
m_http.postRequest(url, body, [this](int code, const QJsonObject& resp)
|
||||
m_http.postRequest(url, body, [this, appId, channel, targetVer, versionId](int code, const QJsonObject& resp)
|
||||
{
|
||||
qDebug() << "Manifest API returned code:" << code;
|
||||
m_manifest = QJsonObject();
|
||||
@@ -68,61 +84,82 @@ void UpdaterLogic::getManifest(const QString& appId, const QString& channel, con
|
||||
m_fileItems.append(fi);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "Manifest response missing fields";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "Failed to get manifest";
|
||||
}
|
||||
emit fetchUrlFinished();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "Manifest response missing fields";
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Target version manifest response is incomplete. Stage: download target manifest. App: %1, channel: %2, version: %3, version id: %4.")
|
||||
.arg(appId, channel, targetVer, QString::number(versionId));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "Failed to get manifest";
|
||||
const QString detail = serverDetailMessage(resp);
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cannot download target version manifest. Stage: download target manifest. HTTP status: %1. App: %2, channel: %3, version: %4, version id: %5.%6")
|
||||
.arg(QString::number(code), appId, channel, targetVer, QString::number(versionId),
|
||||
detail.isEmpty() ? QString() : QCoreApplication::translate("UpdaterLogic", "\nServer message: %1").arg(detail));
|
||||
}
|
||||
emit fetchUrlFinished();
|
||||
});
|
||||
}
|
||||
|
||||
bool UpdaterLogic::verifySignature(const QByteArray& payload, const QString& signatureBase64, const QString& publicKeyPath) const
|
||||
{
|
||||
#ifndef HAVE_OPENSSL
|
||||
Q_UNUSED(payload);
|
||||
Q_UNUSED(signatureBase64);
|
||||
Q_UNUSED(publicKeyPath);
|
||||
qDebug() << "OpenSSL not available, cannot verify manifest signature";
|
||||
return false;
|
||||
#else
|
||||
QFile keyFile(publicKeyPath);
|
||||
if (!keyFile.open(QIODevice::ReadOnly))
|
||||
{
|
||||
qDebug() << "Cannot open public key file:" << publicKeyPath;
|
||||
return false;
|
||||
}
|
||||
#ifndef HAVE_OPENSSL
|
||||
Q_UNUSED(payload);
|
||||
Q_UNUSED(signatureBase64);
|
||||
Q_UNUSED(publicKeyPath);
|
||||
qDebug() << "OpenSSL not available, cannot verify manifest signature";
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cannot verify manifest signature because OpenSSL support is unavailable. Stage: manifest signature verification.");
|
||||
return false;
|
||||
#else
|
||||
QFile keyFile(publicKeyPath);
|
||||
if (!keyFile.open(QIODevice::ReadOnly))
|
||||
{
|
||||
qDebug() << "Cannot open public key file:" << publicKeyPath;
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cannot open manifest public key. Stage: manifest signature verification. Public key path: %1.")
|
||||
.arg(publicKeyPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
QByteArray keyData = keyFile.readAll();
|
||||
keyFile.close();
|
||||
|
||||
BIO* bio = BIO_new_mem_buf(keyData.constData(), keyData.size());
|
||||
if (!bio)
|
||||
{
|
||||
qDebug() << "BIO_new_mem_buf failed";
|
||||
return false;
|
||||
}
|
||||
if (!bio)
|
||||
{
|
||||
qDebug() << "BIO_new_mem_buf failed";
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cannot parse manifest public key buffer. Stage: manifest signature verification. Public key path: %1.")
|
||||
.arg(publicKeyPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
EVP_PKEY* pkey = PEM_read_bio_PUBKEY(bio, NULL, NULL, NULL);
|
||||
BIO_free(bio);
|
||||
if (!pkey)
|
||||
{
|
||||
qDebug() << "PEM_read_bio_PUBKEY failed";
|
||||
return false;
|
||||
}
|
||||
if (!pkey)
|
||||
{
|
||||
qDebug() << "PEM_read_bio_PUBKEY failed";
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Manifest public key is invalid. Stage: manifest signature verification. Public key path: %1.")
|
||||
.arg(publicKeyPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
QByteArray signature = QByteArray::fromBase64(signatureBase64.toUtf8());
|
||||
EVP_MD_CTX* mdctx = EVP_MD_CTX_new();
|
||||
if (!mdctx)
|
||||
{
|
||||
EVP_PKEY_free(pkey);
|
||||
qDebug() << "EVP_MD_CTX_new failed";
|
||||
return false;
|
||||
}
|
||||
if (!mdctx)
|
||||
{
|
||||
EVP_PKEY_free(pkey);
|
||||
qDebug() << "EVP_MD_CTX_new failed";
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cannot create OpenSSL verification context. Stage: manifest signature verification.");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
if (EVP_DigestVerifyInit(mdctx, NULL, EVP_sha256(), NULL, pkey) == 1)
|
||||
@@ -139,29 +176,35 @@ bool UpdaterLogic::verifySignature(const QByteArray& payload, const QString& sig
|
||||
EVP_MD_CTX_free(mdctx);
|
||||
EVP_PKEY_free(pkey);
|
||||
|
||||
if (!ok)
|
||||
{
|
||||
qDebug() << "Manifest signature verification failed";
|
||||
}
|
||||
return ok;
|
||||
#endif
|
||||
}
|
||||
if (!ok)
|
||||
{
|
||||
qDebug() << "Manifest signature verification failed";
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Manifest RSA signature is invalid. Stage: manifest signature verification. This usually means the manifest was not signed by the matching server private key, the client public key is wrong, or the manifest content was changed.");
|
||||
}
|
||||
return ok;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool UpdaterLogic::verifyManifestSignature(const QString& publicKeyPath) const
|
||||
{
|
||||
// 验签用的是客户端随 SDK 分发的公钥。
|
||||
// 只要服务端私钥没有泄漏,客户端就能发现被篡改的 Manifest。
|
||||
if (m_manifest.isEmpty() || m_manifestText.isEmpty())
|
||||
{
|
||||
qDebug() << "No manifest available to verify";
|
||||
return false;
|
||||
}
|
||||
{
|
||||
qDebug() << "No manifest available to verify";
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"No manifest is available for signature verification. Stage: manifest signature verification. The target manifest may not have been downloaded successfully.");
|
||||
return false;
|
||||
}
|
||||
QString signature = m_manifest.value("signature").toString();
|
||||
if (signature.isEmpty())
|
||||
{
|
||||
qDebug() << "Manifest signature empty";
|
||||
return false;
|
||||
}
|
||||
if (signature.isEmpty())
|
||||
{
|
||||
qDebug() << "Manifest signature empty";
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"The manifest does not contain a signature. Stage: manifest signature verification.");
|
||||
return false;
|
||||
}
|
||||
|
||||
QString path = publicKeyPath;
|
||||
if (!QFile::exists(path))
|
||||
@@ -175,18 +218,29 @@ bool UpdaterLogic::saveManifestCache(const QString& cacheDir) const
|
||||
{
|
||||
// 启动后的完整性检查依赖本地 Manifest 缓存。
|
||||
// 升级成功后缓存签名清单,下一次离线启动也能校验当前版本文件。
|
||||
if (m_manifest.isEmpty())
|
||||
return false;
|
||||
|
||||
QDir dir(cacheDir);
|
||||
if (!dir.exists() && !dir.mkpath("."))
|
||||
return false;
|
||||
if (m_manifest.isEmpty()) {
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cannot save manifest cache because the target manifest is empty. Stage: save target manifest cache.");
|
||||
return false;
|
||||
}
|
||||
|
||||
QDir dir(cacheDir);
|
||||
if (!dir.exists() && !dir.mkpath(".")) {
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cannot create manifest cache directory. Stage: save target manifest cache. Directory: %1.")
|
||||
.arg(cacheDir);
|
||||
return false;
|
||||
}
|
||||
|
||||
QString version = m_manifest.value("version").toString();
|
||||
QString filePath = cacheDir + "/manifest_" + version + ".json";
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate))
|
||||
return false;
|
||||
QString filePath = cacheDir + "/manifest_" + version + ".json";
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cannot write manifest cache file. Stage: save target manifest cache. File: %1. Error: %2.")
|
||||
.arg(filePath, file.errorString());
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonObject wrapper;
|
||||
wrapper["manifest"] = m_manifest;
|
||||
@@ -194,33 +248,48 @@ bool UpdaterLogic::saveManifestCache(const QString& cacheDir) const
|
||||
|
||||
QJsonDocument doc(wrapper);
|
||||
file.write(doc.toJson(QJsonDocument::Indented));
|
||||
file.close();
|
||||
qDebug() << "Manifest cached to" << filePath;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UpdaterLogic::loadManifestCache(const QString& cacheDir, const QString& version)
|
||||
{
|
||||
QString filePath = cacheDir + "/manifest_" + version + ".json";
|
||||
QFile file(filePath);
|
||||
if (!file.exists() || !file.open(QIODevice::ReadOnly))
|
||||
return false;
|
||||
file.close();
|
||||
qDebug() << "Manifest cached to" << filePath;
|
||||
m_error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UpdaterLogic::loadManifestCache(const QString& cacheDir, const QString& version)
|
||||
{
|
||||
m_error.clear();
|
||||
QString filePath = cacheDir + "/manifest_" + version + ".json";
|
||||
QFile file(filePath);
|
||||
if (!file.exists() || !file.open(QIODevice::ReadOnly)) {
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cannot read cached signed manifest. Stage: read local manifest cache. Version: %1. File: %2. This cache is created after a version is installed successfully.")
|
||||
.arg(version, filePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
QByteArray raw = file.readAll();
|
||||
file.close();
|
||||
QJsonDocument doc = QJsonDocument::fromJson(raw);
|
||||
if (!doc.isObject())
|
||||
return false;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(raw);
|
||||
if (!doc.isObject()) {
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cached signed manifest is not valid JSON. Stage: read local manifest cache. Version: %1. File: %2.")
|
||||
.arg(version, filePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonObject wrapper = doc.object();
|
||||
if (!wrapper.contains("manifest") || !wrapper.contains("manifest_text")) {
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cached signed manifest is incomplete. Stage: read local manifest cache. Version: %1. File: %2.")
|
||||
.arg(version, filePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonObject wrapper = doc.object();
|
||||
if (!wrapper.contains("manifest") || !wrapper.contains("manifest_text"))
|
||||
return false;
|
||||
|
||||
m_manifest = wrapper["manifest"].toObject();
|
||||
m_manifestText = wrapper["manifest_text"].toString();
|
||||
qDebug() << "Loaded cached manifest" << version;
|
||||
return true;
|
||||
}
|
||||
m_manifest = wrapper["manifest"].toObject();
|
||||
m_manifestText = wrapper["manifest_text"].toString();
|
||||
qDebug() << "Loaded cached manifest" << version;
|
||||
m_error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
QByteArray UpdaterLogic::canonicalManifestBytes(const QJsonObject& manifest) const
|
||||
{
|
||||
@@ -311,43 +380,63 @@ QStringList UpdaterLogic::obsoleteFilesComparedTo(const QJsonObject& oldManifest
|
||||
return obsolete;
|
||||
}
|
||||
|
||||
bool UpdaterLogic::validateLocalFiles(const QString& stagingDir, const QString& installedDir) const
|
||||
{
|
||||
if (m_manifest.isEmpty())
|
||||
return false;
|
||||
|
||||
const QJsonArray files = m_manifest.value("files").toArray();
|
||||
for (const auto& item : files)
|
||||
bool UpdaterLogic::validateLocalFiles(const QString& stagingDir, const QString& installedDir) const
|
||||
{
|
||||
m_error.clear();
|
||||
const QString stage = installedDir.isEmpty()
|
||||
? QCoreApplication::translate("UpdaterLogic", "installed version verification")
|
||||
: QCoreApplication::translate("UpdaterLogic", "downloaded/staged file verification");
|
||||
const QString version = m_manifest.value(QStringLiteral("version")).toString();
|
||||
if (m_manifest.isEmpty()) {
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"No manifest is available. Stage: %1. The updater cannot know which files and hashes should be verified.")
|
||||
.arg(stage);
|
||||
return false;
|
||||
}
|
||||
|
||||
const QJsonArray files = m_manifest.value("files").toArray();
|
||||
for (const auto& item : files)
|
||||
{
|
||||
const QJsonObject fileObject = item.toObject();
|
||||
const QString path = QDir::fromNativeSeparators(fileObject.value("path").toString());
|
||||
const QString expectedSha = fileObject.value("sha256").toString();
|
||||
if (!isSafeRelativePath(path))
|
||||
return false;
|
||||
if (isRuntimeProtectedPath(path)) {
|
||||
qDebug() << "Runtime-protected manifest entry ignored:" << path;
|
||||
continue;
|
||||
const QString path = QDir::fromNativeSeparators(fileObject.value("path").toString());
|
||||
const QString expectedSha = fileObject.value("sha256").toString();
|
||||
if (!isSafeRelativePath(path)) {
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Manifest contains an unsafe file path. Stage: %1. Version: %2. Path: %3.")
|
||||
.arg(stage, version, path);
|
||||
return false;
|
||||
}
|
||||
if (isRuntimeProtectedPath(path)) {
|
||||
qDebug() << "Runtime-protected manifest entry ignored:" << path;
|
||||
continue;
|
||||
}
|
||||
|
||||
QString fullPath = QDir(stagingDir).filePath(path);
|
||||
if (!QFile::exists(fullPath) && !installedDir.isEmpty())
|
||||
fullPath = QDir(installedDir).filePath(path);
|
||||
|
||||
if (!QFile::exists(fullPath))
|
||||
{
|
||||
qDebug() << "Manifest file missing from staging and installation:" << path;
|
||||
return false;
|
||||
}
|
||||
const QString actualSha = calcLocalFileSha256(fullPath);
|
||||
if (actualSha.compare(expectedSha, Qt::CaseInsensitive) != 0)
|
||||
{
|
||||
qDebug() << "File hash mismatch:" << path << actualSha << expectedSha;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
qDebug() << "Full manifest validation passed using staging plus installed files";
|
||||
return true;
|
||||
}
|
||||
if (!QFile::exists(fullPath))
|
||||
{
|
||||
qDebug() << "Manifest file missing from staging and installation:" << path;
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"A required file is missing. Stage: %1. Version: %2. Manifest path: %3. Checked path: %4. If this is a downloaded update, the file was not downloaded or staged correctly; if this is startup verification, the installed file may have been deleted.")
|
||||
.arg(stage, version, path, fullPath);
|
||||
return false;
|
||||
}
|
||||
const QString actualSha = calcLocalFileSha256(fullPath);
|
||||
if (actualSha.compare(expectedSha, Qt::CaseInsensitive) != 0)
|
||||
{
|
||||
qDebug() << "File hash mismatch:" << path << actualSha << expectedSha;
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"File SHA-256 does not match the signed manifest. Stage: %1. Version: %2. Manifest path: %3. Local path: %4.\nExpected SHA-256: %5\nActual SHA-256: %6\nIf this happens during download, clear the update cache and retry. If this happens during startup or after installation, the local file differs from the published version.")
|
||||
.arg(stage, version, path, fullPath, expectedSha, actualSha.isEmpty() ? QCoreApplication::translate("UpdaterLogic", "<cannot read file>") : actualSha);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
qDebug() << "Full manifest validation passed using staging plus installed files";
|
||||
m_error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UpdaterLogic::loadOfflinePackage(const QString& packagePath, const QString& stagingDir)
|
||||
{
|
||||
@@ -395,9 +484,10 @@ bool UpdaterLogic::loadOfflinePackage(const QString& packagePath, const QString&
|
||||
return true;
|
||||
}
|
||||
|
||||
void UpdaterLogic::getDownloadUrl(const QString& appId, const QString& channel, const QString& targetVer, int versionId)
|
||||
{
|
||||
QString url = m_serverAddr + "/api/v1/update/download-url";
|
||||
void UpdaterLogic::getDownloadUrl(const QString& appId, const QString& channel, const QString& targetVer, int versionId)
|
||||
{
|
||||
m_error.clear();
|
||||
QString url = m_serverAddr + "/api/v1/update/download-url";
|
||||
QJsonObject body;
|
||||
body["app_id"] = appId;
|
||||
body["channel"] = channel;
|
||||
@@ -407,7 +497,7 @@ void UpdaterLogic::getDownloadUrl(const QString& appId, const QString& channel,
|
||||
QJsonArray emptyFiles;
|
||||
body["files"] = emptyFiles;
|
||||
|
||||
m_http.postRequest(url, body, [this](int code, const QJsonObject& resp)
|
||||
m_http.postRequest(url, body, [this, appId, channel, targetVer, versionId](int code, const QJsonObject& resp)
|
||||
{
|
||||
qDebug() << "Download URL API returned code:" << code;
|
||||
m_fileItems.clear();
|
||||
@@ -427,13 +517,18 @@ void UpdaterLogic::getDownloadUrl(const QString& appId, const QString& channel,
|
||||
qDebug() << "File info:" << fi.path << fi.url << fi.sha256;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "Failed to get download URL";
|
||||
}
|
||||
emit fetchUrlFinished();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "Failed to get download URL";
|
||||
const QString detail = serverDetailMessage(resp);
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cannot get secure download URLs. Stage: request download URLs. HTTP status: %1. App: %2, channel: %3, version: %4, version id: %5.%6")
|
||||
.arg(QString::number(code), appId, channel, targetVer, QString::number(versionId),
|
||||
detail.isEmpty() ? QString() : QCoreApplication::translate("UpdaterLogic", "\nServer message: %1").arg(detail));
|
||||
}
|
||||
emit fetchUrlFinished();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
QString UpdaterLogic::calcLocalFileSha256(const QString& filePath) const
|
||||
@@ -454,13 +549,21 @@ QString UpdaterLogic::calcLocalFileSha256(const QString& filePath) const
|
||||
bool UpdaterLogic::downloadSingleFile(const QString& url, const QString& savePath,
|
||||
const QString& expectSha256, qint64 expectedSize,
|
||||
const QString& resumePartPath)
|
||||
{
|
||||
const QString partPath = resumePartPath.isEmpty() ? savePath + ".part" : resumePartPath;
|
||||
if (!QDir().mkpath(QFileInfo(partPath).path())) return false;
|
||||
if (QFile::exists(savePath)
|
||||
&& calcLocalFileSha256(savePath).compare(expectSha256, Qt::CaseInsensitive) == 0)
|
||||
return true;
|
||||
QFile::remove(savePath);
|
||||
{
|
||||
const QString partPath = resumePartPath.isEmpty() ? savePath + ".part" : resumePartPath;
|
||||
const QString displayPath = m_currentDownloadPath.isEmpty() ? savePath : m_currentDownloadPath;
|
||||
if (!QDir().mkpath(QFileInfo(partPath).path())) {
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cannot create partial download directory. Stage: download file. File: %1. Partial file: %2.")
|
||||
.arg(displayPath, partPath);
|
||||
return false;
|
||||
}
|
||||
if (QFile::exists(savePath)
|
||||
&& calcLocalFileSha256(savePath).compare(expectSha256, Qt::CaseInsensitive) == 0) {
|
||||
m_error.clear();
|
||||
return true;
|
||||
}
|
||||
QFile::remove(savePath);
|
||||
|
||||
for (int attempt = 0; attempt < 4; ++attempt)
|
||||
{
|
||||
@@ -472,23 +575,37 @@ bool UpdaterLogic::downloadSingleFile(const QString& url, const QString& savePat
|
||||
}
|
||||
if (expectedSize >= 0 && existingSize == expectedSize && existingSize > 0)
|
||||
{
|
||||
if (calcLocalFileSha256(partPath).compare(expectSha256, Qt::CaseInsensitive) == 0)
|
||||
{
|
||||
QFile::remove(savePath);
|
||||
return QFile::rename(partPath, savePath);
|
||||
}
|
||||
QFile::remove(partPath);
|
||||
existingSize = 0;
|
||||
}
|
||||
if (calcLocalFileSha256(partPath).compare(expectSha256, Qt::CaseInsensitive) == 0)
|
||||
{
|
||||
QFile::remove(savePath);
|
||||
if (QFile::rename(partPath, savePath)) {
|
||||
m_error.clear();
|
||||
return true;
|
||||
}
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Downloaded file passed SHA-256 verification, but cannot move it into the staging directory. Stage: download file. File: %1. From: %2. To: %3.")
|
||||
.arg(displayPath, partPath, savePath);
|
||||
return false;
|
||||
}
|
||||
const QString actualSha = calcLocalFileSha256(partPath);
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cached partial file has the expected size but wrong SHA-256. Stage: resume download. File: %1.\nExpected SHA-256: %2\nActual SHA-256: %3\nThe partial cache will be deleted and downloaded again.")
|
||||
.arg(displayPath, expectSha256, actualSha);
|
||||
QFile::remove(partPath);
|
||||
existingSize = 0;
|
||||
}
|
||||
|
||||
QFile partFile(partPath);
|
||||
const QIODevice::OpenMode mode = QIODevice::WriteOnly
|
||||
| (existingSize > 0 ? QIODevice::Append : QIODevice::Truncate);
|
||||
if (!partFile.open(mode))
|
||||
{
|
||||
qDebug() << "Cannot open partial download:" << partPath;
|
||||
return false;
|
||||
}
|
||||
if (!partFile.open(mode))
|
||||
{
|
||||
qDebug() << "Cannot open partial download:" << partPath;
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cannot open partial download file. Stage: download file. File: %1. Partial file: %2. Error: %3.")
|
||||
.arg(displayPath, partPath, partFile.errorString());
|
||||
return false;
|
||||
}
|
||||
|
||||
QNetworkAccessManager manager;
|
||||
manager.setProxy(QNetworkProxy::NoProxy);
|
||||
@@ -533,27 +650,45 @@ bool UpdaterLogic::downloadSingleFile(const QString& url, const QString& savePat
|
||||
else if (networkOk && writeOk && (httpStatus == 200 || httpStatus == 206))
|
||||
{
|
||||
const qint64 actualSize = QFileInfo(partPath).size();
|
||||
if ((expectedSize < 0 || actualSize == expectedSize)
|
||||
&& calcLocalFileSha256(partPath).compare(expectSha256, Qt::CaseInsensitive) == 0)
|
||||
{
|
||||
QFile::remove(savePath);
|
||||
if (QFile::rename(partPath, savePath))
|
||||
{
|
||||
qDebug() << "Download completed/resumed & sha pass:" << savePath;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (expectedSize >= 0 && actualSize >= expectedSize)
|
||||
{
|
||||
qDebug() << "Completed partial file has invalid size or SHA; restart:" << savePath;
|
||||
QFile::remove(partPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "Download attempt failed; partial file retained:" << attempt + 1
|
||||
<< savePath << httpStatus << networkError << "bytes" << QFileInfo(partPath).size();
|
||||
}
|
||||
if ((expectedSize < 0 || actualSize == expectedSize)
|
||||
&& calcLocalFileSha256(partPath).compare(expectSha256, Qt::CaseInsensitive) == 0)
|
||||
{
|
||||
QFile::remove(savePath);
|
||||
if (QFile::rename(partPath, savePath))
|
||||
{
|
||||
qDebug() << "Download completed/resumed & sha pass:" << savePath;
|
||||
m_error.clear();
|
||||
return true;
|
||||
}
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Downloaded file passed SHA-256 verification, but cannot move it into the staging directory. Stage: download file. File: %1. From: %2. To: %3.")
|
||||
.arg(displayPath, partPath, savePath);
|
||||
return false;
|
||||
}
|
||||
else if (expectedSize >= 0 && actualSize >= expectedSize)
|
||||
{
|
||||
qDebug() << "Completed partial file has invalid size or SHA; restart:" << savePath;
|
||||
const QString actualSha = calcLocalFileSha256(partPath);
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Downloaded file does not match the signed manifest. Stage: download file. File: %1. HTTP status: %2.\nExpected size: %3 bytes\nActual size: %4 bytes\nExpected SHA-256: %5\nActual SHA-256: %6\nThe partial cache will be deleted and downloaded again.")
|
||||
.arg(displayPath, QString::number(httpStatus), QString::number(expectedSize), QString::number(actualSize),
|
||||
expectSha256, actualSha.isEmpty() ? QCoreApplication::translate("UpdaterLogic", "<cannot read file>") : actualSha);
|
||||
QFile::remove(partPath);
|
||||
}
|
||||
else {
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Downloaded file is incomplete. Stage: download file. File: %1. HTTP status: %2. Expected size: %3 bytes, current size: %4 bytes.")
|
||||
.arg(displayPath, QString::number(httpStatus), QString::number(expectedSize), QString::number(actualSize));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "Download attempt failed; partial file retained:" << attempt + 1
|
||||
<< savePath << httpStatus << networkError << "bytes" << QFileInfo(partPath).size();
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Download request failed. Stage: download file. File: %1. Attempt: %2/4. HTTP status: %3. Network error: %4. Partial file: %5.")
|
||||
.arg(displayPath, QString::number(attempt + 1), QString::number(httpStatus), networkError, partPath);
|
||||
}
|
||||
|
||||
if (attempt < 3)
|
||||
{
|
||||
@@ -566,9 +701,14 @@ bool UpdaterLogic::downloadSingleFile(const QString& url, const QString& savePat
|
||||
QThread::msleep(50);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (m_error.isEmpty()) {
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"File download failed after retries. Stage: download file. File: %1.")
|
||||
.arg(displayPath);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UpdaterLogic::isRuntimeProtectedPath(const QString& path) const
|
||||
{
|
||||
@@ -632,20 +772,31 @@ qint64 UpdaterLogic::estimateAdditionalDiskBytes(const QString& targetDir,
|
||||
return required + reserve;
|
||||
}
|
||||
|
||||
bool UpdaterLogic::downloadAllFiles(const QString& tempDir, const QString& targetDir)
|
||||
{
|
||||
if (m_fileItems.isEmpty())
|
||||
{
|
||||
m_downloadAllOk = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
QDir stagingDir(tempDir);
|
||||
if (!FileHelper::createDir(tempDir))
|
||||
return false;
|
||||
const QString resumeCacheDir = QDir(ConfigHelper::instance().updateRoot()).filePath("download_cache");
|
||||
if (!FileHelper::createDir(resumeCacheDir))
|
||||
return false;
|
||||
bool UpdaterLogic::downloadAllFiles(const QString& tempDir, const QString& targetDir)
|
||||
{
|
||||
m_error.clear();
|
||||
if (m_fileItems.isEmpty())
|
||||
{
|
||||
m_downloadAllOk = false;
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"The target version manifest contains no downloadable files. Stage: prepare downloads.");
|
||||
return false;
|
||||
}
|
||||
|
||||
QDir stagingDir(tempDir);
|
||||
if (!FileHelper::createDir(tempDir)) {
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cannot create update staging directory. Stage: prepare downloads. Directory: %1.")
|
||||
.arg(tempDir);
|
||||
return false;
|
||||
}
|
||||
const QString resumeCacheDir = QDir(ConfigHelper::instance().updateRoot()).filePath("download_cache");
|
||||
if (!FileHelper::createDir(resumeCacheDir)) {
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cannot create download cache directory. Stage: prepare downloads. Directory: %1.")
|
||||
.arg(resumeCacheDir);
|
||||
return false;
|
||||
}
|
||||
QSet<QString> activePartialNames;
|
||||
for (const auto& item : m_fileItems)
|
||||
activePartialNames.insert(item.sha256.toLower() + ".part");
|
||||
@@ -671,12 +822,15 @@ bool UpdaterLogic::downloadAllFiles(const QString& tempDir, const QString& targe
|
||||
|
||||
for (const auto& fi : m_fileItems)
|
||||
{
|
||||
if (!isSafeRelativePath(fi.path))
|
||||
{
|
||||
qDebug() << "Unsafe relative path in manifest:" << fi.path;
|
||||
m_downloadAllOk = false;
|
||||
return false;
|
||||
}
|
||||
if (!isSafeRelativePath(fi.path))
|
||||
{
|
||||
qDebug() << "Unsafe relative path in manifest:" << fi.path;
|
||||
m_downloadAllOk = false;
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Manifest contains an unsafe file path. Stage: prepare file download. Path: %1.")
|
||||
.arg(fi.path);
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString relativePath = QDir::fromNativeSeparators(fi.path);
|
||||
if (isRuntimeProtectedPath(relativePath)) {
|
||||
@@ -693,12 +847,15 @@ bool UpdaterLogic::downloadAllFiles(const QString& tempDir, const QString& targe
|
||||
|
||||
const QString tempFile = QDir(tempDir).filePath(relativePath);
|
||||
const QString parentDir = QFileInfo(tempFile).path();
|
||||
if (!QDir().mkpath(parentDir))
|
||||
{
|
||||
qDebug() << "Cannot create staging subdirectory:" << parentDir;
|
||||
m_downloadAllOk = false;
|
||||
return false;
|
||||
}
|
||||
if (!QDir().mkpath(parentDir))
|
||||
{
|
||||
qDebug() << "Cannot create staging subdirectory:" << parentDir;
|
||||
m_downloadAllOk = false;
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"Cannot create staging subdirectory. Stage: prepare file download. File: %1. Directory: %2.")
|
||||
.arg(relativePath, parentDir);
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString resumePartPath = QDir(resumeCacheDir).filePath(fi.sha256.toLower() + ".part");
|
||||
m_currentDownloadPath = relativePath;
|
||||
@@ -717,9 +874,10 @@ bool UpdaterLogic::downloadAllFiles(const QString& tempDir, const QString& targe
|
||||
m_currentDownloadPath, speed);
|
||||
}
|
||||
|
||||
m_downloadAllOk = true;
|
||||
return true;
|
||||
}
|
||||
m_downloadAllOk = true;
|
||||
m_error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
void UpdaterLogic::reportDownloadResult(const QString& appId, const QString& channel,
|
||||
const QString& version, bool success)
|
||||
|
||||
@@ -29,8 +29,9 @@ public:
|
||||
bool validateLocalFiles(const QString& stagingDir, const QString& installedDir = QString()) const;
|
||||
bool saveManifestCache(const QString& cacheDir) const;
|
||||
bool loadManifestCache(const QString& cacheDir, const QString& version);
|
||||
bool loadOfflinePackage(const QString& packagePath, const QString& stagingDir = QString());
|
||||
QString offlineError() const { return m_offlineError; }
|
||||
bool loadOfflinePackage(const QString& packagePath, const QString& stagingDir = QString());
|
||||
QString offlineError() const { return m_offlineError; }
|
||||
QString errorString() const { return m_error; }
|
||||
|
||||
void getDownloadUrl(const QString& appId, const QString& channel, const QString& targetVer, int versionId);
|
||||
void reportResult(const QString& deviceId, const QString& fromVer, const QString& toVer, bool success);
|
||||
@@ -63,11 +64,12 @@ private:
|
||||
QJsonObject m_manifest;
|
||||
QString m_manifestText;
|
||||
QList<FileDownloadItem> m_fileItems;
|
||||
bool m_downloadAllOk = false;
|
||||
bool m_downloadAllOk = false;
|
||||
qint64 m_downloadTotalBytes = 0;
|
||||
qint64 m_downloadCompletedBytes = 0;
|
||||
qint64 m_sessionDownloadedBytes = 0;
|
||||
QString m_currentDownloadPath;
|
||||
QString m_offlineError;
|
||||
QElapsedTimer m_downloadTimer;
|
||||
};
|
||||
QString m_currentDownloadPath;
|
||||
QString m_offlineError;
|
||||
mutable QString m_error;
|
||||
QElapsedTimer m_downloadTimer;
|
||||
};
|
||||
|
||||
+76
-40
@@ -3,9 +3,10 @@
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
#include <QElapsedTimer>
|
||||
#include <QFile>
|
||||
#include <QMessageBox>
|
||||
#include <QElapsedTimer>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QMessageBox>
|
||||
#include <QProcess>
|
||||
#include <QProgressDialog>
|
||||
#include <QSaveFile>
|
||||
@@ -164,14 +165,19 @@ int main(int argc, char* argv[])
|
||||
if (offlinePackagePath.isEmpty())
|
||||
logic.reportResult(deviceId, fromVersion, targetVersion, success);
|
||||
};
|
||||
const auto fail = [&](const QString& title, const QString& message,
|
||||
const QString& errorCode = QString("update_failed")) {
|
||||
transaction.markFailed(errorCode, message);
|
||||
reportUpdateResult(false);
|
||||
progress.close();
|
||||
QMessageBox::critical(nullptr, title, message);
|
||||
return -1;
|
||||
};
|
||||
const auto fail = [&](const QString& title, const QString& message,
|
||||
const QString& errorCode = QString("update_failed")) {
|
||||
transaction.markFailed(errorCode, message);
|
||||
reportUpdateResult(false);
|
||||
progress.close();
|
||||
QMessageBox::critical(nullptr, title, message);
|
||||
return -1;
|
||||
};
|
||||
const auto withDetails = [](const QString& message, const QString& details) {
|
||||
return details.trimmed().isEmpty()
|
||||
? message
|
||||
: message + QCoreApplication::translate("Updater", "\n\nDetails:\n%1").arg(details);
|
||||
};
|
||||
const auto configuredName = [&](const QString& key, const QString& fallback) {
|
||||
return ConfigHelper::executableNameForCurrentPlatform(
|
||||
config.getValue("Runtime", key), fallback);
|
||||
@@ -182,25 +188,44 @@ int main(int argc, char* argv[])
|
||||
bool timeoutOk = false;
|
||||
int healthCheckTimeoutMs = config.getValue("Runtime", "health_check_timeout_ms").toInt(&timeoutOk);
|
||||
if (!timeoutOk || healthCheckTimeoutMs < 1000) healthCheckTimeoutMs = 15000;
|
||||
const QString mainAppPath = QDir(runtimeDir).filePath(mainExecutable);
|
||||
const QString updaterPath = QDir(runtimeDir).filePath(updaterExecutable);
|
||||
const QString bootstrapPath = QDir(runtimeDir).filePath(bootstrapExecutable);
|
||||
const QString launchToken = config.getValue("App", "launch_token");
|
||||
const auto launchMainApp = [&](const QString& healthFile = QString()) {
|
||||
QString ticketPath;
|
||||
QString ticketError;
|
||||
const QString launchVersion = config.getValue("App", "current_version");
|
||||
if (!TicketHelper::createTicket(appId, deviceId, launchVersion, launchToken,
|
||||
&ticketPath, &ticketError)) {
|
||||
qDebug() << "Cannot create launch ticket:" << ticketError;
|
||||
return false;
|
||||
}
|
||||
QStringList args{QString("--ticket-file=%1").arg(ticketPath)};
|
||||
if (!healthFile.isEmpty()) args.append(QString("--health-file=%1").arg(healthFile));
|
||||
const bool started = QProcess::startDetached(mainAppPath, args);
|
||||
if (!started) QFile::remove(ticketPath);
|
||||
return started;
|
||||
};
|
||||
const QString mainAppPath = QDir(runtimeDir).filePath(mainExecutable);
|
||||
const QString updaterPath = QDir(runtimeDir).filePath(updaterExecutable);
|
||||
const QString bootstrapPath = QDir(runtimeDir).filePath(bootstrapExecutable);
|
||||
const QString launchToken = config.getValue("App", "launch_token");
|
||||
QString mainStartupError;
|
||||
const auto launchMainApp = [&](const QString& healthFile = QString()) {
|
||||
mainStartupError.clear();
|
||||
if (!QFileInfo::exists(mainAppPath)) {
|
||||
mainStartupError = QCoreApplication::translate(
|
||||
"Updater",
|
||||
"Cannot start the main application because the executable file does not exist.\nExecutable: %1\nCheck main_executable and install_root in the generated client configuration.")
|
||||
.arg(mainAppPath);
|
||||
return false;
|
||||
}
|
||||
QString ticketPath;
|
||||
QString ticketError;
|
||||
const QString launchVersion = config.getValue("App", "current_version");
|
||||
if (!TicketHelper::createTicket(appId, deviceId, launchVersion, launchToken,
|
||||
&ticketPath, &ticketError)) {
|
||||
qDebug() << "Cannot create launch ticket:" << ticketError;
|
||||
mainStartupError = QCoreApplication::translate(
|
||||
"Updater",
|
||||
"Cannot start the main application because the one-time launch ticket could not be created.\nExecutable: %1\nDetails: %2")
|
||||
.arg(mainAppPath, ticketError);
|
||||
return false;
|
||||
}
|
||||
QStringList args{QString("--ticket-file=%1").arg(ticketPath)};
|
||||
if (!healthFile.isEmpty()) args.append(QString("--health-file=%1").arg(healthFile));
|
||||
const bool started = QProcess::startDetached(mainAppPath, args);
|
||||
if (!started) {
|
||||
QFile::remove(ticketPath);
|
||||
mainStartupError = QCoreApplication::translate(
|
||||
"Updater",
|
||||
"Cannot start the main application process.\nExecutable: %1\nTicket file: %2\nHealth file: %3\nCheck file permissions, dependent DLLs/shared libraries, and whether the executable can run independently.")
|
||||
.arg(mainAppPath, ticketPath, healthFile.isEmpty() ? QCoreApplication::translate("Updater", "<not used>") : healthFile);
|
||||
}
|
||||
return started;
|
||||
};
|
||||
const auto bootstrapPlanFile = [&]() {
|
||||
return QDir(updateDir).filePath("bootstrap_plan_" + transaction.transactionId() + ".txt");
|
||||
};
|
||||
@@ -282,16 +307,19 @@ int main(int argc, char* argv[])
|
||||
if (resumingFromBootstrap) {
|
||||
if (!logic.loadManifestCache(manifestCacheDir, targetVersion))
|
||||
return delegateRollback(QCoreApplication::translate("Updater", "Manifest Cache Failed"),
|
||||
QCoreApplication::translate("Updater", "Cannot read the signed manifest cache after Bootstrap installation."));
|
||||
withDetails(QCoreApplication::translate("Updater", "Cannot read the signed manifest cache after Bootstrap installation."),
|
||||
logic.errorString()));
|
||||
} else if (offlinePackagePath.isEmpty()) {
|
||||
logic.getManifest(appId, channel, targetVersion, targetVersionId);
|
||||
}
|
||||
if (!logic.verifyManifestSignature()) {
|
||||
if (resumingFromBootstrap)
|
||||
return delegateRollback(QCoreApplication::translate("Updater", "Security Verification Failed"),
|
||||
QCoreApplication::translate("Updater", "Cannot reverify the manifest signature after Bootstrap installation."));
|
||||
withDetails(QCoreApplication::translate("Updater", "Cannot reverify the manifest signature after Bootstrap installation."),
|
||||
logic.errorString()));
|
||||
return fail(QCoreApplication::translate("Updater", "Security Verification Failed"),
|
||||
QCoreApplication::translate("Updater", "The version manifest signature is invalid. The update has stopped. Please contact the administrator."),
|
||||
withDetails(QCoreApplication::translate("Updater", "The version manifest signature is invalid. The update has stopped. Please contact the administrator."),
|
||||
logic.errorString()),
|
||||
"manifest_signature_invalid");
|
||||
}
|
||||
QStringList obsoletePaths;
|
||||
@@ -309,9 +337,11 @@ int main(int argc, char* argv[])
|
||||
if (!logic.saveManifestCache(manifestCacheDir)) {
|
||||
if (resumingFromBootstrap)
|
||||
return delegateRollback(QCoreApplication::translate("Updater", "Manifest Cache Failed"),
|
||||
QCoreApplication::translate("Updater", "Cannot save the new version manifest cache."));
|
||||
withDetails(QCoreApplication::translate("Updater", "Cannot save the new version manifest cache."),
|
||||
logic.errorString()));
|
||||
return fail(QCoreApplication::translate("Updater", "Manifest Cache Failed"),
|
||||
QCoreApplication::translate("Updater", "Cannot save the new version manifest cache. The update has stopped."),
|
||||
withDetails(QCoreApplication::translate("Updater", "Cannot save the new version manifest cache. The update has stopped."),
|
||||
logic.errorString()),
|
||||
"manifest_cache_failed");
|
||||
}
|
||||
|
||||
@@ -324,7 +354,8 @@ int main(int argc, char* argv[])
|
||||
logic.getDownloadUrl(appId, channel, targetVersion, targetVersionId);
|
||||
if (logic.getFileList().isEmpty())
|
||||
return fail(QCoreApplication::translate("Updater", "No Files to Update"),
|
||||
QCoreApplication::translate("Updater", "The server did not return any version files. The update has stopped."),
|
||||
withDetails(QCoreApplication::translate("Updater", "The server did not return any version files. The update has stopped."),
|
||||
logic.errorString()),
|
||||
"empty_file_list");
|
||||
|
||||
QStorageInfo storage(updateDir);
|
||||
@@ -353,7 +384,8 @@ int main(int argc, char* argv[])
|
||||
if (!logic.downloadAllFiles(stagingDir, targetDir)) {
|
||||
logic.reportDownloadResult(appId, channel, targetVersion, false);
|
||||
return fail(QCoreApplication::translate("Updater", "Download Failed"),
|
||||
QCoreApplication::translate("Updater", "Some files failed to download or failed SHA-256 verification. Please check the network and try again."),
|
||||
withDetails(QCoreApplication::translate("Updater", "Some files failed to download or failed SHA-256 verification. Please check the network, update cache, or server release files and try again."),
|
||||
logic.errorString()),
|
||||
"download_failed");
|
||||
}
|
||||
logic.reportDownloadResult(appId, channel, targetVersion, true);
|
||||
@@ -362,7 +394,8 @@ int main(int argc, char* argv[])
|
||||
setProgress(58, QCoreApplication::translate("Updater", "Verifying complete version files..."));
|
||||
if (!logic.validateLocalFiles(stagingDir, targetDir))
|
||||
return fail(QCoreApplication::translate("Updater", "File Verification Failed"),
|
||||
QCoreApplication::translate("Updater", "The staged files do not match the version manifest. The update has stopped."),
|
||||
withDetails(QCoreApplication::translate("Updater", "The downloaded/staged files do not match the target version manifest. The update has stopped before replacing installed files."),
|
||||
logic.errorString()),
|
||||
"staging_verify_failed");
|
||||
|
||||
QStringList changedPaths;
|
||||
@@ -441,7 +474,8 @@ int main(int argc, char* argv[])
|
||||
setProgress(82, QCoreApplication::translate("Updater", "Verifying Bootstrap installation result..."));
|
||||
if (!transaction.markPostVerify() || !logic.validateLocalFiles(targetDir))
|
||||
return delegateRollback(QCoreApplication::translate("Updater", "Installation Verification Failed"),
|
||||
QCoreApplication::translate("Updater", "New version files failed verification after installation."));
|
||||
withDetails(QCoreApplication::translate("Updater", "New version files failed verification after installation. The updater will roll back to the previous version."),
|
||||
logic.errorString()));
|
||||
for (const QString& path : transaction.obsoletePaths()) {
|
||||
if (QFile::exists(QDir(targetDir).filePath(path)))
|
||||
return delegateRollback(QCoreApplication::translate("Updater", "Obsolete File Cleanup Failed"),
|
||||
@@ -459,7 +493,9 @@ int main(int argc, char* argv[])
|
||||
setProgress(94, QCoreApplication::translate("Updater", "Starting the new version and waiting for health confirmation..."));
|
||||
if (!launchMainApp(healthFile))
|
||||
return delegateRollback(QCoreApplication::translate("Updater", "Startup Failed"),
|
||||
QCoreApplication::translate("Updater", "%1 cannot be started.").arg(mainExecutable));
|
||||
mainStartupError.isEmpty()
|
||||
? QCoreApplication::translate("Updater", "%1 cannot be started.").arg(mainExecutable)
|
||||
: mainStartupError);
|
||||
|
||||
QElapsedTimer healthTimer;
|
||||
healthTimer.start();
|
||||
|
||||
Binary file not shown.
+801
-180
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user