fix: 优化客户端错误提示和校验诊断信息

This commit is contained in:
2026-07-21 03:02:42 +00:00
parent 1c8ec37d2b
commit d9e32c6cea
14 changed files with 1739 additions and 627 deletions
+1 -1
View File
@@ -228,7 +228,7 @@ bool runElevatedSelfCommand(const QStringList& arguments, const QString& targetP
const QMessageBox::StandardButton choice = QMessageBox::question( const QMessageBox::StandardButton choice = QMessageBox::question(
nullptr, nullptr,
QCoreApplication::translate("ConfigHelper", "Administrator Permission Required"), 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), .arg(QDir::toNativeSeparators(targetPath), originalError),
QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok | QMessageBox::Cancel,
QMessageBox::Ok); QMessageBox::Ok);
+25 -3
View File
@@ -257,12 +257,34 @@ bool DeviceIdentityHelper::ensureIssued(
timer.stop(); timer.stop();
const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
const QString networkError = reply->errorString();
const QByteArray raw = reply->readAll(); const QByteArray raw = reply->readAll();
reply->deleteLater(); reply->deleteLater();
if (status != 200) { if (status != 200) {
m_error = QCoreApplication::translate("DeviceIdentityHelper", "Device identity request failed (HTTP %1): %2") QJsonParseError responseError;
.arg(status) const QJsonDocument errorDoc = QJsonDocument::fromJson(raw, &responseError);
.arg(QString::fromUtf8(raw)); 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; return false;
} }
+70 -13
View File
@@ -6,6 +6,7 @@
#include <QFile> #include <QFile>
#include <QFileInfo> #include <QFileInfo>
#include <QJsonArray> #include <QJsonArray>
#include <QCoreApplication>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QSet> #include <QSet>
@@ -59,18 +60,31 @@ QString IntegrityHelper::sha256(const QString& filePath) const
bool IntegrityHelper::verifySignature(const QByteArray& payload, const QString& signatureBase64) bool IntegrityHelper::verifySignature(const QByteArray& payload, const QString& signatureBase64)
{ {
#ifndef HAVE_OPENSSL #ifndef HAVE_OPENSSL
Q_UNUSED(payload); Q_UNUSED(signatureBase64); m_error = "OpenSSL unavailable"; return false; 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 #else
QString keyPath = QDir(m_installDir).filePath("config/manifest_public_key.pem"); QString keyPath = QDir(m_installDir).filePath("config/manifest_public_key.pem");
if (!QFile::exists(keyPath)) if (!QFile::exists(keyPath))
keyPath = QFileInfo(ConfigHelper::instance().configPath()).dir().filePath("manifest_public_key.pem"); keyPath = QFileInfo(ConfigHelper::instance().configPath()).dir().filePath("manifest_public_key.pem");
QFile keyFile(keyPath); QFile keyFile(keyPath);
if (!keyFile.open(QIODevice::ReadOnly)) { m_error = "manifest public key missing"; return false; } 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(); const QByteArray keyData = keyFile.readAll();
BIO* bio = BIO_new_mem_buf(keyData.constData(), keyData.size()); BIO* bio = BIO_new_mem_buf(keyData.constData(), keyData.size());
EVP_PKEY* key = bio ? PEM_read_bio_PUBKEY(bio, nullptr, nullptr, nullptr) : nullptr; EVP_PKEY* key = bio ? PEM_read_bio_PUBKEY(bio, nullptr, nullptr, nullptr) : nullptr;
if (bio) BIO_free(bio); 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(); EVP_MD_CTX* ctx = EVP_MD_CTX_new();
const QByteArray signature = QByteArray::fromBase64(signatureBase64.toUtf8()); const QByteArray signature = QByteArray::fromBase64(signatureBase64.toUtf8());
const bool ok = ctx && EVP_DigestVerifyInit(ctx, nullptr, EVP_sha256(), nullptr, key) == 1 const bool ok = ctx && EVP_DigestVerifyInit(ctx, nullptr, EVP_sha256(), nullptr, key) == 1
@@ -78,7 +92,10 @@ bool IntegrityHelper::verifySignature(const QByteArray& payload, const QString&
&& EVP_DigestVerifyFinal(ctx, reinterpret_cast<const unsigned char*>(signature.constData()), signature.size()) == 1; && EVP_DigestVerifyFinal(ctx, reinterpret_cast<const unsigned char*>(signature.constData()), signature.size()) == 1;
if (ctx) EVP_MD_CTX_free(ctx); if (ctx) EVP_MD_CTX_free(ctx);
EVP_PKEY_free(key); EVP_PKEY_free(key);
if (!ok) m_error = "manifest RSA signature invalid"; 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; return ok;
#endif #endif
} }
@@ -96,41 +113,78 @@ bool IntegrityHelper::verifyInstalledVersion(const QString& appId, const QString
if (!QFile::exists(cachePath)) if (!QFile::exists(cachePath))
cachePath = legacyCachePath; cachePath = legacyCachePath;
QFile cache(cachePath); 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; QJsonParseError wrapperError;
const QJsonDocument wrapperDoc = QJsonDocument::fromJson(cache.readAll(), &wrapperError); const QJsonDocument wrapperDoc = QJsonDocument::fromJson(cache.readAll(), &wrapperError);
if (wrapperError.error != QJsonParseError::NoError || !wrapperDoc.isObject()) { if (wrapperError.error != QJsonParseError::NoError || !wrapperDoc.isObject()) {
m_error = "manifest cache JSON invalid"; return false; 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 QJsonObject wrapper = wrapperDoc.object();
const QByteArray manifestText = wrapper.value("manifest_text").toString().toUtf8(); const QByteArray manifestText = wrapper.value("manifest_text").toString().toUtf8();
const QString signature = wrapper.value("manifest").toObject().value("signature").toString(); 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; QJsonParseError manifestError;
const QJsonDocument manifestDoc = QJsonDocument::fromJson(manifestText, &manifestError); const QJsonDocument manifestDoc = QJsonDocument::fromJson(manifestText, &manifestError);
if (manifestError.error != QJsonParseError::NoError || !manifestDoc.isObject()) { if (manifestError.error != QJsonParseError::NoError || !manifestDoc.isObject()) {
m_error = "signed manifest payload invalid"; return false; 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(); const QJsonObject manifest = manifestDoc.object();
if (manifest.value("app_id").toString() != appId if (manifest.value("app_id").toString() != appId
|| manifest.value("channel").toString() != channel || manifest.value("channel").toString() != channel
|| manifest.value("version").toString() != version) { || manifest.value("version").toString() != version) {
m_error = "manifest identity does not match local application"; return false; 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; QSet<QString> declaredExecutables;
for (const QJsonValue& value : manifest.value("files").toArray()) { for (const QJsonValue& value : manifest.value("files").toArray()) {
const QJsonObject item = value.toObject(); const QJsonObject item = value.toObject();
const QString path = QDir::fromNativeSeparators(item.value("path").toString()); const QString path = QDir::fromNativeSeparators(item.value("path").toString());
if (!safeRelativePath(path)) { m_error = "unsafe manifest path: " + path; return false; } 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; if (runtimeProtectedPath(path)) continue;
const QString fullPath = QDir(m_installDir).filePath(path); const QString fullPath = QDir(m_installDir).filePath(path);
if (!QFile::exists(fullPath)) { m_error = "required file missing: " + path; return false; } 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 expected = item.value("sha256").toString();
const QString actual = sha256(fullPath); const QString actual = sha256(fullPath);
if (actual.isEmpty() || actual.compare(expected, Qt::CaseInsensitive) != 0) { if (actual.isEmpty() || actual.compare(expected, Qt::CaseInsensitive) != 0) {
m_error = "file hash mismatch: " + path; return false; 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(); const QString suffix = QFileInfo(path).suffix().toCaseFolded();
if (suffix == "exe" || suffix == "dll") declaredExecutables.insert(path.toCaseFolded()); if (suffix == "exe" || suffix == "dll") declaredExecutables.insert(path.toCaseFolded());
@@ -152,7 +206,10 @@ bool IntegrityHelper::verifyInstalledVersion(const QString& appId, const QString
|| runtimeWorkDir || runtimeProtectedPath(relative)) continue; || runtimeWorkDir || runtimeProtectedPath(relative)) continue;
const QString suffix = QFileInfo(relative).suffix().toCaseFolded(); const QString suffix = QFileInfo(relative).suffix().toCaseFolded();
if ((suffix == "exe" || suffix == "dll") && !declaredExecutables.contains(folded)) { if ((suffix == "exe" || suffix == "dll") && !declaredExecutables.contains(folded)) {
m_error = "undeclared executable or plugin: " + relative; return false; 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; return true;
+20 -5
View File
@@ -1,5 +1,6 @@
#include "LocalStateHelper.h" #include "LocalStateHelper.h"
#include "ConfigHelper.h" #include "ConfigHelper.h"
#include <QCoreApplication>
#include <QDir> #include <QDir>
#include <QFile> #include <QFile>
#include <QJsonDocument> #include <QJsonDocument>
@@ -31,7 +32,10 @@ bool LocalStateHelper::loadState(const QString& relativePath)
m_loaded = true; m_loaded = true;
if (!saveState()) if (!saveState())
{ {
m_error = QString("Cannot write new state file: %1").arg(m_filePath); m_error = QCoreApplication::translate(
"LocalStateHelper",
"Cannot create local state file: %1. Error: %2.")
.arg(m_filePath, m_error);
return false; return false;
} }
return true; return true;
@@ -39,7 +43,10 @@ bool LocalStateHelper::loadState(const QString& relativePath)
if (!file.open(QIODevice::ReadOnly)) if (!file.open(QIODevice::ReadOnly))
{ {
m_error = QString("Cannot open state file: %1").arg(m_filePath); m_error = QCoreApplication::translate(
"LocalStateHelper",
"Cannot open local state file: %1. Error: %2.")
.arg(m_filePath, file.errorString());
return false; return false;
} }
@@ -50,7 +57,10 @@ bool LocalStateHelper::loadState(const QString& relativePath)
QJsonDocument doc = QJsonDocument::fromJson(raw, &parseError); QJsonDocument doc = QJsonDocument::fromJson(raw, &parseError);
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) if (parseError.error != QJsonParseError::NoError || !doc.isObject())
{ {
m_error = QString("Invalid state JSON: %1").arg(parseError.errorString()); m_error = QCoreApplication::translate(
"LocalStateHelper",
"Local state file is not valid JSON. File: %1. JSON error: %2.")
.arg(m_filePath, parseError.errorString());
return false; return false;
} }
@@ -63,7 +73,9 @@ bool LocalStateHelper::saveState() const
{ {
if (!m_loaded) if (!m_loaded)
{ {
m_error = "State is not loaded"; m_error = QCoreApplication::translate(
"LocalStateHelper",
"Local state has not been loaded.");
return false; return false;
} }
@@ -71,7 +83,10 @@ bool LocalStateHelper::saveState() const
QString writeError; QString writeError;
if (!ConfigHelper::writeFileWithElevationIfNeeded(m_filePath, doc.toJson(QJsonDocument::Indented), &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; return false;
} }
m_error.clear(); m_error.clear();
+64 -11
View File
@@ -1,6 +1,7 @@
#include "PolicyHelper.h" #include "PolicyHelper.h"
#include "ConfigHelper.h" #include "ConfigHelper.h"
#include <QApplication> #include <QApplication>
#include <QCoreApplication>
#include <QDateTime> #include <QDateTime>
#include <QDir> #include <QDir>
#include <QFile> #include <QFile>
@@ -29,12 +30,23 @@ static QString resolvePolicyPath(const QString& baseDir, const QString& relative
bool PolicyHelper::loadPolicy(const QString& relativePath) bool PolicyHelper::loadPolicy(const QString& relativePath)
{ {
QFile file(resolvePolicyPath(m_baseDir, relativePath, false)); const QString path = resolvePolicyPath(m_baseDir, relativePath, false);
if (!file.open(QIODevice::ReadOnly)) { m_error = "Cannot open policy file"; return 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; QJsonParseError error;
const QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error); const QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error);
if (error.error != QJsonParseError::NoError || !doc.isObject()) { if (error.error != QJsonParseError::NoError || !doc.isObject()) {
m_error = "Invalid policy JSON: " + error.errorString(); return false; 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()); return loadPolicyObject(doc.object());
} }
@@ -51,7 +63,10 @@ bool PolicyHelper::savePolicy(const QString& relativePath) const
QString writeError; QString writeError;
const QString path = resolvePolicyPath(m_baseDir, relativePath, true); const QString path = resolvePolicyPath(m_baseDir, relativePath, true);
if (!ConfigHelper::writeFileWithElevationIfNeeded(path, bytes, &writeError)) { 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; return false;
} }
m_error.clear(); m_error.clear();
@@ -85,34 +100,72 @@ QByteArray PolicyHelper::canonicalPolicyBytes(const QJsonObject& policy) const
bool PolicyHelper::verifySignature(const QByteArray& payload, const QString& signatureBase64) const bool PolicyHelper::verifySignature(const QByteArray& payload, const QString& signatureBase64) const
{ {
#ifndef HAVE_OPENSSL #ifndef HAVE_OPENSSL
Q_UNUSED(payload); Q_UNUSED(signatureBase64); m_error = "OpenSSL unavailable"; return false; 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 #else
QString keyPath = m_baseDir + "/config/manifest_public_key.pem"; QString keyPath = m_baseDir + "/config/manifest_public_key.pem";
QFile keyFile(keyPath); QFile keyFile(keyPath);
if (!keyFile.open(QIODevice::ReadOnly)) { m_error = "Cannot open policy public key"; return false; } 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(); const QByteArray keyData = keyFile.readAll();
BIO* bio = BIO_new_mem_buf(keyData.constData(), keyData.size()); BIO* bio = BIO_new_mem_buf(keyData.constData(), keyData.size());
EVP_PKEY* key = bio ? PEM_read_bio_PUBKEY(bio, nullptr, nullptr, nullptr) : nullptr; EVP_PKEY* key = bio ? PEM_read_bio_PUBKEY(bio, nullptr, nullptr, nullptr) : nullptr;
if (bio) BIO_free(bio); 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(); EVP_MD_CTX* ctx = EVP_MD_CTX_new();
const QByteArray signature = QByteArray::fromBase64(signatureBase64.toUtf8()); const QByteArray signature = QByteArray::fromBase64(signatureBase64.toUtf8());
bool ok = ctx && EVP_DigestVerifyInit(ctx, nullptr, EVP_sha256(), nullptr, key) == 1 bool ok = ctx && EVP_DigestVerifyInit(ctx, nullptr, EVP_sha256(), nullptr, key) == 1
&& EVP_DigestVerifyUpdate(ctx, payload.constData(), payload.size()) == 1 && EVP_DigestVerifyUpdate(ctx, payload.constData(), payload.size()) == 1
&& EVP_DigestVerifyFinal(ctx, reinterpret_cast<const unsigned char*>(signature.constData()), signature.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 (ctx) EVP_MD_CTX_free(ctx); EVP_PKEY_free(key);
if (!ok) m_error = "Invalid RSA policy signature"; 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; return ok;
#endif #endif
} }
bool PolicyHelper::isValid() const bool PolicyHelper::isValid() const
{ {
if (!m_loaded) 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", 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"}; "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; } for (const QString& key : required) {
if (m_policy.value("signature_alg").toString() != "RSA-2048-SHA256") return false; 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(); const QByteArray payload = m_signedText.isEmpty() ? canonicalPolicyBytes(m_policy) : m_signedText.toUtf8();
return verifySignature(payload, m_policy.value("signature").toString()); return verifySignature(payload, m_policy.value("signature").toString());
} }
+83 -13
View File
@@ -8,6 +8,7 @@
#include <QMessageAuthenticationCode> #include <QMessageAuthenticationCode>
#include <QSaveFile> #include <QSaveFile>
#include <QStandardPaths> #include <QStandardPaths>
#include <QStringList>
#include <QUuid> #include <QUuid>
static QByteArray ticketMac(const QJsonObject& payload, const QString& secret) static QByteArray ticketMac(const QJsonObject& payload, const QString& secret)
@@ -22,8 +23,18 @@ bool TicketHelper::createTicket(const QString& appId, const QString& deviceId,
{ {
// Launcher 启动业务主程序前生成一次性 ticket。 // Launcher 启动业务主程序前生成一次性 ticket。
// ticket 只保存在临时目录、有效期 60 秒,并用 launch_token 做 HMAC,防止用户绕过 Launcher 直接启动主程序。 // ticket 只保存在临时目录、有效期 60 秒,并用 launch_token 做 HMAC,防止用户绕过 Launcher 直接启动主程序。
if (appId.isEmpty() || version.isEmpty() || secret.isEmpty()) { if (appId.isEmpty() || deviceId.isEmpty() || version.isEmpty() || secret.isEmpty()) {
if (errorMessage) *errorMessage = "ticket identity or secret is empty"; 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; return false;
} }
const QDateTime now = QDateTime::currentDateTimeUtc(); const QDateTime now = QDateTime::currentDateTimeUtc();
@@ -36,12 +47,25 @@ bool TicketHelper::createTicket(const QString& appId, const QString& deviceId,
}; };
QJsonObject wrapper{{"payload", payload}, {"signature", QString::fromLatin1(ticketMac(payload, secret))}}; QJsonObject wrapper{{"payload", payload}, {"signature", QString::fromLatin1(ticketMac(payload, secret))}};
const QString dirPath = QDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation)).filePath("marsco_tickets"); 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"); const QString path = QDir(dirPath).filePath("ticket_" + QUuid::createUuid().toString(QUuid::WithoutBraces) + ".json");
QSaveFile file(path); QSaveFile file(path);
const QByteArray bytes = QJsonDocument(wrapper).toJson(QJsonDocument::Compact); const QByteArray bytes = QJsonDocument(wrapper).toJson(QJsonDocument::Compact);
if (!file.open(QIODevice::WriteOnly) || file.write(bytes) != bytes.size() || !file.commit()) { if (!file.open(QIODevice::WriteOnly) || file.write(bytes) != bytes.size() || !file.commit()) {
if (errorMessage) *errorMessage = "cannot save ticket"; if (errorMessage) {
*errorMessage = QCoreApplication::translate(
"TicketHelper",
"Cannot save launch ticket file: %1. Error: %2.")
.arg(path, file.errorString());
}
return false; return false;
} }
QFile::setPermissions(path, QFileDevice::ReadOwner | QFileDevice::WriteOwner); QFile::setPermissions(path, QFileDevice::ReadOwner | QFileDevice::WriteOwner);
@@ -58,13 +82,23 @@ bool TicketHelper::consumeAndVerify(const QString& ticketPath, const QString& ex
const QString consumingPath = ticketPath + ".consuming." const QString consumingPath = ticketPath + ".consuming."
+ QString::number(QCoreApplication::applicationPid()); + QString::number(QCoreApplication::applicationPid());
if (!QFile::rename(ticketPath, consumingPath)) { if (!QFile::rename(ticketPath, consumingPath)) {
if (errorMessage) *errorMessage = "ticket missing or already consumed"; 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; return false;
} }
QFile file(consumingPath); QFile file(consumingPath);
if (!file.open(QIODevice::ReadOnly)) { if (!file.open(QIODevice::ReadOnly)) {
QFile::remove(consumingPath); QFile::remove(consumingPath);
if (errorMessage) *errorMessage = "cannot read claimed ticket"; if (errorMessage) {
*errorMessage = QCoreApplication::translate(
"TicketHelper",
"Cannot read claimed launch ticket: %1. Error: %2.")
.arg(consumingPath, file.errorString());
}
return false; return false;
} }
const QByteArray raw = file.readAll(); file.close(); const QByteArray raw = file.readAll(); file.close();
@@ -72,7 +106,13 @@ bool TicketHelper::consumeAndVerify(const QString& ticketPath, const QString& ex
QJsonParseError parseError; QJsonParseError parseError;
const QJsonDocument doc = QJsonDocument::fromJson(raw, &parseError); const QJsonDocument doc = QJsonDocument::fromJson(raw, &parseError);
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) { if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
if (errorMessage) *errorMessage = "invalid ticket JSON"; return false; 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 wrapper = doc.object();
const QJsonObject payload = wrapper.value("payload").toObject(); const QJsonObject payload = wrapper.value("payload").toObject();
@@ -84,27 +124,57 @@ bool TicketHelper::consumeAndVerify(const QString& ticketPath, const QString& ex
const bool timeOk = issued.isValid() && expires.isValid() && issued <= now.addSecs(5) const bool timeOk = issued.isValid() && expires.isValid() && issued <= now.addSecs(5)
&& expires >= now && issued.secsTo(expires) <= 65; && expires >= now && issued.secsTo(expires) <= 65;
if (actual.isEmpty() || actual != expected) { 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; return false;
} }
if (payload.value("app_id").toString() != expectedAppId) { 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; return false;
} }
if (payload.value("device_id").toString() != expectedDeviceId) { 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; return false;
} }
if (payload.value("version").toString() != expectedVersion) { 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; return false;
} }
if (!timeOk) { 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; return false;
} }
if (payload.value("nonce").toString().isEmpty()) { 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 false;
} }
return true; return true;
+26 -6
View File
@@ -106,7 +106,9 @@ bool UpdateLogic::cacheManifest(const QString& appId, const QString& channel, co
{ {
m_error.clear(); m_error.clear();
if (appId.isEmpty() || channel.isEmpty() || version.isEmpty() || versionId <= 0) { if (appId.isEmpty() || channel.isEmpty() || version.isEmpty() || versionId <= 0) {
m_error = "manifest identity is incomplete"; 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; return false;
} }
@@ -124,26 +126,42 @@ bool UpdateLogic::cacheManifest(const QString& appId, const QString& channel, co
}); });
if (statusCode != 200) { if (statusCode != 200) {
m_error = QString("manifest request failed (HTTP %1)").arg(statusCode); 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; return false;
} }
const QJsonObject manifest = response.value("manifest").toObject(); const QJsonObject manifest = response.value("manifest").toObject();
const QString manifestText = response.value("manifest_text").toString(); const QString manifestText = response.value("manifest_text").toString();
if (manifest.isEmpty() || manifestText.isEmpty()) { if (manifest.isEmpty() || manifestText.isEmpty()) {
m_error = "manifest response is incomplete"; 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; return false;
} }
if (manifest.value("app_id").toString() != appId if (manifest.value("app_id").toString() != appId
|| manifest.value("channel").toString() != channel || manifest.value("channel").toString() != channel
|| manifest.value("version").toString() != version) { || manifest.value("version").toString() != version) {
m_error = "manifest identity does not match current 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; return false;
} }
QDir dir(cacheDir); QDir dir(cacheDir);
if (!dir.exists() && !dir.mkpath(".")) { if (!dir.exists() && !dir.mkpath(".")) {
m_error = "cannot create manifest cache directory"; m_error = QCoreApplication::translate("UpdateLogic",
"Cannot create manifest cache directory. Stage: cache current version manifest. Directory: %1.")
.arg(cacheDir);
return false; return false;
} }
@@ -153,7 +171,9 @@ bool UpdateLogic::cacheManifest(const QString& appId, const QString& channel, co
QSaveFile file(dir.filePath("manifest_" + version + ".json")); QSaveFile file(dir.filePath("manifest_" + version + ".json"));
const QByteArray bytes = QJsonDocument(wrapper).toJson(QJsonDocument::Indented); const QByteArray bytes = QJsonDocument(wrapper).toJson(QJsonDocument::Indented);
if (!file.open(QIODevice::WriteOnly) || file.write(bytes) != bytes.size() || !file.commit()) { if (!file.open(QIODevice::WriteOnly) || file.write(bytes) != bytes.size() || !file.commit()) {
m_error = "cannot save signed manifest cache"; 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; return false;
} }
qDebug() << "Current version manifest cached to" << file.fileName(); qDebug() << "Current version manifest cached to" << file.fileName();
+61 -5
View File
@@ -176,7 +176,27 @@ int main(int argc, char* argv[])
QCoreApplication::translate("Launcher", "Select Offline Update Package"), QCoreApplication::translate("Launcher", "Select Offline Update Package"),
QString(), QString(),
QCoreApplication::translate("Launcher", "Marsco offline update package (*.upd)")); 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")) { if (QCoreApplication::arguments().contains("--import-offline")) {
@@ -188,19 +208,38 @@ int main(int argc, char* argv[])
return 0; return 0;
} }
QString mainStartupError;
const auto startMainApp = [&]() { const auto startMainApp = [&]() {
// 只允许 Launcher 启动业务主程序:先生成一次性 ticket,再通过 --ticket-file 传给主程序。 // 只允许 Launcher 启动业务主程序:先生成一次性 ticket,再通过 --ticket-file 传给主程序。
// 业务主程序必须消费并校验 ticket,避免用户直接双击 SimCAE/MainApp 绕过更新和授权检查。 // 业务主程序必须消费并校验 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 ticketPath;
QString ticketError; QString ticketError;
if (!TicketHelper::createTicket(logic.getAppId(), deviceId, currentVersion, if (!TicketHelper::createTicket(logic.getAppId(), deviceId, currentVersion,
launchToken, &ticketPath, &ticketError)) { launchToken, &ticketPath, &ticketError)) {
qDebug() << "Cannot create launch ticket:" << 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; return false;
} }
const bool started = QProcess::startDetached(mainAppPath, const bool started = QProcess::startDetached(mainAppPath,
QStringList{QString("--ticket-file=%1").arg(ticketPath)}); QStringList{QString("--ticket-file=%1").arg(ticketPath)});
if (!started) QFile::remove(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; return started;
}; };
@@ -298,17 +337,32 @@ int main(int argc, char* argv[])
if (!startMainApp()) { if (!startMainApp()) {
QMessageBox::critical(nullptr, QMessageBox::critical(nullptr,
QCoreApplication::translate("Launcher", "Startup Failed"), 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 -1;
} }
return 0; return 0;
} }
const QStringList updaterArgs{appId, channel, latestVer, QString::number(targetVersionId)}; 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)) if (!QProcess::startDetached(updaterPath, updaterArgs))
{ {
QMessageBox::critical(nullptr, QMessageBox::critical(nullptr,
QCoreApplication::translate("Launcher", "Updater Startup Failed"), 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 -1;
} }
return 0; return 0;
@@ -362,7 +416,9 @@ int main(int argc, char* argv[])
progress.close(); progress.close();
QMessageBox::critical(nullptr, QMessageBox::critical(nullptr,
QCoreApplication::translate("Launcher", "Startup Failed"), 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 -1;
} }
progress.close(); progress.close();
+3 -1
View File
@@ -99,7 +99,9 @@ int main(int argc, char* argv[])
config.getValue("App", "current_version"))) config.getValue("App", "current_version")))
{ {
QMessageBox::critical(nullptr, "Integrity Check Failed", QMessageBox::critical(nullptr, "Integrity Check Failed",
QString("Application files failed signed Manifest verification:\n%1") 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())); .arg(integrity.errorString()));
return -1; return -1;
} }
+173 -15
View File
@@ -31,10 +31,26 @@ UpdaterLogic::UpdaterLogic(QObject* parent)
m_serverAddr = ConfigHelper::instance().getValue("Server", "api_base_url"); 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) void UpdaterLogic::getManifest(const QString& appId, const QString& channel, const QString& targetVer, int versionId)
{ {
// Manifest 由服务端按版本动态生成,描述目标版本包含哪些文件以及每个文件的 SHA256。 // Manifest 由服务端按版本动态生成,描述目标版本包含哪些文件以及每个文件的 SHA256。
// Updater 先拿到 Manifest,再请求下载 URL,最后按 Manifest 校验本地文件。 // Updater 先拿到 Manifest,再请求下载 URL,最后按 Manifest 校验本地文件。
m_error.clear();
QString url = m_serverAddr + "/api/v1/update/manifest"; QString url = m_serverAddr + "/api/v1/update/manifest";
QJsonObject body; QJsonObject body;
body["app_id"] = appId; body["app_id"] = appId;
@@ -42,7 +58,7 @@ void UpdaterLogic::getManifest(const QString& appId, const QString& channel, con
body["version"] = targetVer; body["version"] = targetVer;
body["version_id"] = versionId; 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; qDebug() << "Manifest API returned code:" << code;
m_manifest = QJsonObject(); m_manifest = QJsonObject();
@@ -71,11 +87,19 @@ void UpdaterLogic::getManifest(const QString& appId, const QString& channel, con
else else
{ {
qDebug() << "Manifest response missing fields"; 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 else
{ {
qDebug() << "Failed to get manifest"; 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(); emit fetchUrlFinished();
}); });
@@ -88,12 +112,17 @@ bool UpdaterLogic::verifySignature(const QByteArray& payload, const QString& sig
Q_UNUSED(signatureBase64); Q_UNUSED(signatureBase64);
Q_UNUSED(publicKeyPath); Q_UNUSED(publicKeyPath);
qDebug() << "OpenSSL not available, cannot verify manifest signature"; 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; return false;
#else #else
QFile keyFile(publicKeyPath); QFile keyFile(publicKeyPath);
if (!keyFile.open(QIODevice::ReadOnly)) if (!keyFile.open(QIODevice::ReadOnly))
{ {
qDebug() << "Cannot open public key file:" << publicKeyPath; 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; return false;
} }
@@ -104,6 +133,9 @@ bool UpdaterLogic::verifySignature(const QByteArray& payload, const QString& sig
if (!bio) if (!bio)
{ {
qDebug() << "BIO_new_mem_buf failed"; 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; return false;
} }
@@ -112,6 +144,9 @@ bool UpdaterLogic::verifySignature(const QByteArray& payload, const QString& sig
if (!pkey) if (!pkey)
{ {
qDebug() << "PEM_read_bio_PUBKEY failed"; 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; return false;
} }
@@ -121,6 +156,8 @@ bool UpdaterLogic::verifySignature(const QByteArray& payload, const QString& sig
{ {
EVP_PKEY_free(pkey); EVP_PKEY_free(pkey);
qDebug() << "EVP_MD_CTX_new failed"; qDebug() << "EVP_MD_CTX_new failed";
m_error = QCoreApplication::translate("UpdaterLogic",
"Cannot create OpenSSL verification context. Stage: manifest signature verification.");
return false; return false;
} }
@@ -142,6 +179,8 @@ bool UpdaterLogic::verifySignature(const QByteArray& payload, const QString& sig
if (!ok) if (!ok)
{ {
qDebug() << "Manifest signature verification failed"; 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; return ok;
#endif #endif
@@ -154,12 +193,16 @@ bool UpdaterLogic::verifyManifestSignature(const QString& publicKeyPath) const
if (m_manifest.isEmpty() || m_manifestText.isEmpty()) if (m_manifest.isEmpty() || m_manifestText.isEmpty())
{ {
qDebug() << "No manifest available to verify"; 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; return false;
} }
QString signature = m_manifest.value("signature").toString(); QString signature = m_manifest.value("signature").toString();
if (signature.isEmpty()) if (signature.isEmpty())
{ {
qDebug() << "Manifest signature empty"; qDebug() << "Manifest signature empty";
m_error = QCoreApplication::translate("UpdaterLogic",
"The manifest does not contain a signature. Stage: manifest signature verification.");
return false; return false;
} }
@@ -175,18 +218,29 @@ bool UpdaterLogic::saveManifestCache(const QString& cacheDir) const
{ {
// 启动后的完整性检查依赖本地 Manifest 缓存。 // 启动后的完整性检查依赖本地 Manifest 缓存。
// 升级成功后缓存签名清单,下一次离线启动也能校验当前版本文件。 // 升级成功后缓存签名清单,下一次离线启动也能校验当前版本文件。
if (m_manifest.isEmpty()) 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; return false;
}
QDir dir(cacheDir); QDir dir(cacheDir);
if (!dir.exists() && !dir.mkpath(".")) 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; return false;
}
QString version = m_manifest.value("version").toString(); QString version = m_manifest.value("version").toString();
QString filePath = cacheDir + "/manifest_" + version + ".json"; QString filePath = cacheDir + "/manifest_" + version + ".json";
QFile file(filePath); QFile file(filePath);
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) 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; return false;
}
QJsonObject wrapper; QJsonObject wrapper;
wrapper["manifest"] = m_manifest; wrapper["manifest"] = m_manifest;
@@ -196,29 +250,44 @@ bool UpdaterLogic::saveManifestCache(const QString& cacheDir) const
file.write(doc.toJson(QJsonDocument::Indented)); file.write(doc.toJson(QJsonDocument::Indented));
file.close(); file.close();
qDebug() << "Manifest cached to" << filePath; qDebug() << "Manifest cached to" << filePath;
m_error.clear();
return true; return true;
} }
bool UpdaterLogic::loadManifestCache(const QString& cacheDir, const QString& version) bool UpdaterLogic::loadManifestCache(const QString& cacheDir, const QString& version)
{ {
m_error.clear();
QString filePath = cacheDir + "/manifest_" + version + ".json"; QString filePath = cacheDir + "/manifest_" + version + ".json";
QFile file(filePath); QFile file(filePath);
if (!file.exists() || !file.open(QIODevice::ReadOnly)) 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; return false;
}
QByteArray raw = file.readAll(); QByteArray raw = file.readAll();
file.close(); file.close();
QJsonDocument doc = QJsonDocument::fromJson(raw); QJsonDocument doc = QJsonDocument::fromJson(raw);
if (!doc.isObject()) 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; return false;
}
QJsonObject wrapper = doc.object(); QJsonObject wrapper = doc.object();
if (!wrapper.contains("manifest") || !wrapper.contains("manifest_text")) 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; return false;
}
m_manifest = wrapper["manifest"].toObject(); m_manifest = wrapper["manifest"].toObject();
m_manifestText = wrapper["manifest_text"].toString(); m_manifestText = wrapper["manifest_text"].toString();
qDebug() << "Loaded cached manifest" << version; qDebug() << "Loaded cached manifest" << version;
m_error.clear();
return true; return true;
} }
@@ -313,8 +382,17 @@ QStringList UpdaterLogic::obsoleteFilesComparedTo(const QJsonObject& oldManifest
bool UpdaterLogic::validateLocalFiles(const QString& stagingDir, const QString& installedDir) const bool UpdaterLogic::validateLocalFiles(const QString& stagingDir, const QString& installedDir) const
{ {
if (m_manifest.isEmpty()) 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; return false;
}
const QJsonArray files = m_manifest.value("files").toArray(); const QJsonArray files = m_manifest.value("files").toArray();
for (const auto& item : files) for (const auto& item : files)
@@ -322,8 +400,12 @@ bool UpdaterLogic::validateLocalFiles(const QString& stagingDir, const QString&
const QJsonObject fileObject = item.toObject(); const QJsonObject fileObject = item.toObject();
const QString path = QDir::fromNativeSeparators(fileObject.value("path").toString()); const QString path = QDir::fromNativeSeparators(fileObject.value("path").toString());
const QString expectedSha = fileObject.value("sha256").toString(); const QString expectedSha = fileObject.value("sha256").toString();
if (!isSafeRelativePath(path)) 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; return false;
}
if (isRuntimeProtectedPath(path)) { if (isRuntimeProtectedPath(path)) {
qDebug() << "Runtime-protected manifest entry ignored:" << path; qDebug() << "Runtime-protected manifest entry ignored:" << path;
continue; continue;
@@ -336,16 +418,23 @@ bool UpdaterLogic::validateLocalFiles(const QString& stagingDir, const QString&
if (!QFile::exists(fullPath)) if (!QFile::exists(fullPath))
{ {
qDebug() << "Manifest file missing from staging and installation:" << path; 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; return false;
} }
const QString actualSha = calcLocalFileSha256(fullPath); const QString actualSha = calcLocalFileSha256(fullPath);
if (actualSha.compare(expectedSha, Qt::CaseInsensitive) != 0) if (actualSha.compare(expectedSha, Qt::CaseInsensitive) != 0)
{ {
qDebug() << "File hash mismatch:" << path << actualSha << expectedSha; 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; return false;
} }
} }
qDebug() << "Full manifest validation passed using staging plus installed files"; qDebug() << "Full manifest validation passed using staging plus installed files";
m_error.clear();
return true; return true;
} }
@@ -397,6 +486,7 @@ bool UpdaterLogic::loadOfflinePackage(const QString& packagePath, const QString&
void UpdaterLogic::getDownloadUrl(const QString& appId, const QString& channel, const QString& targetVer, int versionId) 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"; QString url = m_serverAddr + "/api/v1/update/download-url";
QJsonObject body; QJsonObject body;
body["app_id"] = appId; body["app_id"] = appId;
@@ -407,7 +497,7 @@ void UpdaterLogic::getDownloadUrl(const QString& appId, const QString& channel,
QJsonArray emptyFiles; QJsonArray emptyFiles;
body["files"] = 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; qDebug() << "Download URL API returned code:" << code;
m_fileItems.clear(); m_fileItems.clear();
@@ -430,6 +520,11 @@ void UpdaterLogic::getDownloadUrl(const QString& appId, const QString& channel,
else else
{ {
qDebug() << "Failed to get download URL"; 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(); emit fetchUrlFinished();
}); });
@@ -456,10 +551,18 @@ bool UpdaterLogic::downloadSingleFile(const QString& url, const QString& savePat
const QString& resumePartPath) const QString& resumePartPath)
{ {
const QString partPath = resumePartPath.isEmpty() ? savePath + ".part" : resumePartPath; const QString partPath = resumePartPath.isEmpty() ? savePath + ".part" : resumePartPath;
if (!QDir().mkpath(QFileInfo(partPath).path())) return false; 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) if (QFile::exists(savePath)
&& calcLocalFileSha256(savePath).compare(expectSha256, Qt::CaseInsensitive) == 0) && calcLocalFileSha256(savePath).compare(expectSha256, Qt::CaseInsensitive) == 0) {
m_error.clear();
return true; return true;
}
QFile::remove(savePath); QFile::remove(savePath);
for (int attempt = 0; attempt < 4; ++attempt) for (int attempt = 0; attempt < 4; ++attempt)
@@ -475,8 +578,19 @@ bool UpdaterLogic::downloadSingleFile(const QString& url, const QString& savePat
if (calcLocalFileSha256(partPath).compare(expectSha256, Qt::CaseInsensitive) == 0) if (calcLocalFileSha256(partPath).compare(expectSha256, Qt::CaseInsensitive) == 0)
{ {
QFile::remove(savePath); QFile::remove(savePath);
return QFile::rename(partPath, 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); QFile::remove(partPath);
existingSize = 0; existingSize = 0;
} }
@@ -487,6 +601,9 @@ bool UpdaterLogic::downloadSingleFile(const QString& url, const QString& savePat
if (!partFile.open(mode)) if (!partFile.open(mode))
{ {
qDebug() << "Cannot open partial download:" << partPath; 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; return false;
} }
@@ -540,19 +657,37 @@ bool UpdaterLogic::downloadSingleFile(const QString& url, const QString& savePat
if (QFile::rename(partPath, savePath)) if (QFile::rename(partPath, savePath))
{ {
qDebug() << "Download completed/resumed & sha pass:" << savePath; qDebug() << "Download completed/resumed & sha pass:" << savePath;
m_error.clear();
return true; 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) else if (expectedSize >= 0 && actualSize >= expectedSize)
{ {
qDebug() << "Completed partial file has invalid size or SHA; restart:" << savePath; 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); 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 else
{ {
qDebug() << "Download attempt failed; partial file retained:" << attempt + 1 qDebug() << "Download attempt failed; partial file retained:" << attempt + 1
<< savePath << httpStatus << networkError << "bytes" << QFileInfo(partPath).size(); << 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) if (attempt < 3)
@@ -567,6 +702,11 @@ bool UpdaterLogic::downloadSingleFile(const QString& url, const QString& savePat
} }
} }
} }
if (m_error.isEmpty()) {
m_error = QCoreApplication::translate("UpdaterLogic",
"File download failed after retries. Stage: download file. File: %1.")
.arg(displayPath);
}
return false; return false;
} }
@@ -634,18 +774,29 @@ qint64 UpdaterLogic::estimateAdditionalDiskBytes(const QString& targetDir,
bool UpdaterLogic::downloadAllFiles(const QString& tempDir, const QString& targetDir) bool UpdaterLogic::downloadAllFiles(const QString& tempDir, const QString& targetDir)
{ {
m_error.clear();
if (m_fileItems.isEmpty()) if (m_fileItems.isEmpty())
{ {
m_downloadAllOk = false; m_downloadAllOk = false;
m_error = QCoreApplication::translate("UpdaterLogic",
"The target version manifest contains no downloadable files. Stage: prepare downloads.");
return false; return false;
} }
QDir stagingDir(tempDir); QDir stagingDir(tempDir);
if (!FileHelper::createDir(tempDir)) if (!FileHelper::createDir(tempDir)) {
m_error = QCoreApplication::translate("UpdaterLogic",
"Cannot create update staging directory. Stage: prepare downloads. Directory: %1.")
.arg(tempDir);
return false; return false;
}
const QString resumeCacheDir = QDir(ConfigHelper::instance().updateRoot()).filePath("download_cache"); const QString resumeCacheDir = QDir(ConfigHelper::instance().updateRoot()).filePath("download_cache");
if (!FileHelper::createDir(resumeCacheDir)) if (!FileHelper::createDir(resumeCacheDir)) {
m_error = QCoreApplication::translate("UpdaterLogic",
"Cannot create download cache directory. Stage: prepare downloads. Directory: %1.")
.arg(resumeCacheDir);
return false; return false;
}
QSet<QString> activePartialNames; QSet<QString> activePartialNames;
for (const auto& item : m_fileItems) for (const auto& item : m_fileItems)
activePartialNames.insert(item.sha256.toLower() + ".part"); activePartialNames.insert(item.sha256.toLower() + ".part");
@@ -675,6 +826,9 @@ bool UpdaterLogic::downloadAllFiles(const QString& tempDir, const QString& targe
{ {
qDebug() << "Unsafe relative path in manifest:" << fi.path; qDebug() << "Unsafe relative path in manifest:" << fi.path;
m_downloadAllOk = false; 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; return false;
} }
@@ -697,6 +851,9 @@ bool UpdaterLogic::downloadAllFiles(const QString& tempDir, const QString& targe
{ {
qDebug() << "Cannot create staging subdirectory:" << parentDir; qDebug() << "Cannot create staging subdirectory:" << parentDir;
m_downloadAllOk = false; 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; return false;
} }
@@ -718,6 +875,7 @@ bool UpdaterLogic::downloadAllFiles(const QString& tempDir, const QString& targe
} }
m_downloadAllOk = true; m_downloadAllOk = true;
m_error.clear();
return true; return true;
} }
+2
View File
@@ -31,6 +31,7 @@ public:
bool loadManifestCache(const QString& cacheDir, const QString& version); bool loadManifestCache(const QString& cacheDir, const QString& version);
bool loadOfflinePackage(const QString& packagePath, const QString& stagingDir = QString()); bool loadOfflinePackage(const QString& packagePath, const QString& stagingDir = QString());
QString offlineError() const { return m_offlineError; } 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 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); void reportResult(const QString& deviceId, const QString& fromVer, const QString& toVer, bool success);
@@ -69,5 +70,6 @@ private:
qint64 m_sessionDownloadedBytes = 0; qint64 m_sessionDownloadedBytes = 0;
QString m_currentDownloadPath; QString m_currentDownloadPath;
QString m_offlineError; QString m_offlineError;
mutable QString m_error;
QElapsedTimer m_downloadTimer; QElapsedTimer m_downloadTimer;
}; };
+47 -11
View File
@@ -5,6 +5,7 @@
#include <QDirIterator> #include <QDirIterator>
#include <QElapsedTimer> #include <QElapsedTimer>
#include <QFile> #include <QFile>
#include <QFileInfo>
#include <QMessageBox> #include <QMessageBox>
#include <QProcess> #include <QProcess>
#include <QProgressDialog> #include <QProgressDialog>
@@ -172,6 +173,11 @@ int main(int argc, char* argv[])
QMessageBox::critical(nullptr, title, message); QMessageBox::critical(nullptr, title, message);
return -1; 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) { const auto configuredName = [&](const QString& key, const QString& fallback) {
return ConfigHelper::executableNameForCurrentPlatform( return ConfigHelper::executableNameForCurrentPlatform(
config.getValue("Runtime", key), fallback); config.getValue("Runtime", key), fallback);
@@ -186,19 +192,38 @@ int main(int argc, char* argv[])
const QString updaterPath = QDir(runtimeDir).filePath(updaterExecutable); const QString updaterPath = QDir(runtimeDir).filePath(updaterExecutable);
const QString bootstrapPath = QDir(runtimeDir).filePath(bootstrapExecutable); const QString bootstrapPath = QDir(runtimeDir).filePath(bootstrapExecutable);
const QString launchToken = config.getValue("App", "launch_token"); const QString launchToken = config.getValue("App", "launch_token");
QString mainStartupError;
const auto launchMainApp = [&](const QString& healthFile = QString()) { 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 ticketPath;
QString ticketError; QString ticketError;
const QString launchVersion = config.getValue("App", "current_version"); const QString launchVersion = config.getValue("App", "current_version");
if (!TicketHelper::createTicket(appId, deviceId, launchVersion, launchToken, if (!TicketHelper::createTicket(appId, deviceId, launchVersion, launchToken,
&ticketPath, &ticketError)) { &ticketPath, &ticketError)) {
qDebug() << "Cannot create launch ticket:" << 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; return false;
} }
QStringList args{QString("--ticket-file=%1").arg(ticketPath)}; QStringList args{QString("--ticket-file=%1").arg(ticketPath)};
if (!healthFile.isEmpty()) args.append(QString("--health-file=%1").arg(healthFile)); if (!healthFile.isEmpty()) args.append(QString("--health-file=%1").arg(healthFile));
const bool started = QProcess::startDetached(mainAppPath, args); const bool started = QProcess::startDetached(mainAppPath, args);
if (!started) QFile::remove(ticketPath); 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; return started;
}; };
const auto bootstrapPlanFile = [&]() { const auto bootstrapPlanFile = [&]() {
@@ -282,16 +307,19 @@ int main(int argc, char* argv[])
if (resumingFromBootstrap) { if (resumingFromBootstrap) {
if (!logic.loadManifestCache(manifestCacheDir, targetVersion)) if (!logic.loadManifestCache(manifestCacheDir, targetVersion))
return delegateRollback(QCoreApplication::translate("Updater", "Manifest Cache Failed"), 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()) { } else if (offlinePackagePath.isEmpty()) {
logic.getManifest(appId, channel, targetVersion, targetVersionId); logic.getManifest(appId, channel, targetVersion, targetVersionId);
} }
if (!logic.verifyManifestSignature()) { if (!logic.verifyManifestSignature()) {
if (resumingFromBootstrap) if (resumingFromBootstrap)
return delegateRollback(QCoreApplication::translate("Updater", "Security Verification Failed"), 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"), 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"); "manifest_signature_invalid");
} }
QStringList obsoletePaths; QStringList obsoletePaths;
@@ -309,9 +337,11 @@ int main(int argc, char* argv[])
if (!logic.saveManifestCache(manifestCacheDir)) { if (!logic.saveManifestCache(manifestCacheDir)) {
if (resumingFromBootstrap) if (resumingFromBootstrap)
return delegateRollback(QCoreApplication::translate("Updater", "Manifest Cache Failed"), 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"), 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"); "manifest_cache_failed");
} }
@@ -324,7 +354,8 @@ int main(int argc, char* argv[])
logic.getDownloadUrl(appId, channel, targetVersion, targetVersionId); logic.getDownloadUrl(appId, channel, targetVersion, targetVersionId);
if (logic.getFileList().isEmpty()) if (logic.getFileList().isEmpty())
return fail(QCoreApplication::translate("Updater", "No Files to Update"), 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"); "empty_file_list");
QStorageInfo storage(updateDir); QStorageInfo storage(updateDir);
@@ -353,7 +384,8 @@ int main(int argc, char* argv[])
if (!logic.downloadAllFiles(stagingDir, targetDir)) { if (!logic.downloadAllFiles(stagingDir, targetDir)) {
logic.reportDownloadResult(appId, channel, targetVersion, false); logic.reportDownloadResult(appId, channel, targetVersion, false);
return fail(QCoreApplication::translate("Updater", "Download Failed"), 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"); "download_failed");
} }
logic.reportDownloadResult(appId, channel, targetVersion, true); 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...")); setProgress(58, QCoreApplication::translate("Updater", "Verifying complete version files..."));
if (!logic.validateLocalFiles(stagingDir, targetDir)) if (!logic.validateLocalFiles(stagingDir, targetDir))
return fail(QCoreApplication::translate("Updater", "File Verification Failed"), 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"); "staging_verify_failed");
QStringList changedPaths; QStringList changedPaths;
@@ -441,7 +474,8 @@ int main(int argc, char* argv[])
setProgress(82, QCoreApplication::translate("Updater", "Verifying Bootstrap installation result...")); setProgress(82, QCoreApplication::translate("Updater", "Verifying Bootstrap installation result..."));
if (!transaction.markPostVerify() || !logic.validateLocalFiles(targetDir)) if (!transaction.markPostVerify() || !logic.validateLocalFiles(targetDir))
return delegateRollback(QCoreApplication::translate("Updater", "Installation Verification Failed"), 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()) { for (const QString& path : transaction.obsoletePaths()) {
if (QFile::exists(QDir(targetDir).filePath(path))) if (QFile::exists(QDir(targetDir).filePath(path)))
return delegateRollback(QCoreApplication::translate("Updater", "Obsolete File Cleanup Failed"), 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...")); setProgress(94, QCoreApplication::translate("Updater", "Starting the new version and waiting for health confirmation..."));
if (!launchMainApp(healthFile)) if (!launchMainApp(healthFile))
return delegateRollback(QCoreApplication::translate("Updater", "Startup Failed"), 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; QElapsedTimer healthTimer;
healthTimer.start(); healthTimer.start();
Binary file not shown.
File diff suppressed because it is too large Load Diff