feat(hardening): 内嵌产品配置 + 系统级状态 + 全量 i18n + 依赖治理 + 资料清理
update-client-hardening 四工作流合入(构建/静态检查已过): - 配置:不可变产品策略经 UpdateClientResources.cmake 归一化校验后 内嵌 qrc(URL/相对路径/主程序必须落 install_root/版本/UUID 格式, CMake override 写入前复验);可变机器状态迁系统级原生存储; 运行目录不再落可编辑 app_config.json。 - i18n:生产源全英文(no-Han 检查 41/41),中文进 translations/ zh_CN 目录;翻译加载收敛 Common/TranslationHelper 唯一入口; Bootstrap 补资源文件。 - 依赖:Qt/OpenSSL/架构/Perl 机器路径改 imported targets + UpdateClientDependencies.cmake,3rdparty 由父工程契约供给。 - 清理:旧 README/SDK README/3 个重复 PowerShell 打包脚本/PDF 提取 文本删除,canonical README + 5 份结构化英文文档替代;Launcher requireAdministrator manifest;统一 MSVC /utf-8 删运行时编码设置。
This commit is contained in:
+311
-61
@@ -14,7 +14,10 @@
|
||||
#include <QJsonDocument>
|
||||
#include <QSaveFile>
|
||||
#include <QApplication>
|
||||
#include <QCoreApplication>
|
||||
#include <QUrl>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include "ConfigHelper.h"
|
||||
|
||||
#ifdef HAVE_OPENSSL
|
||||
@@ -24,6 +27,41 @@
|
||||
#include <openssl/err.h>
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
bool jsonNonNegativeInteger(const QJsonValue& value, qint64* result)
|
||||
{
|
||||
constexpr double maxExactJsonInteger = 9007199254740991.0;
|
||||
if (!value.isDouble())
|
||||
return false;
|
||||
|
||||
const double number = value.toDouble();
|
||||
if (!std::isfinite(number) || number < 0.0 || number > maxExactJsonInteger
|
||||
|| std::floor(number) != number)
|
||||
return false;
|
||||
|
||||
if (result)
|
||||
*result = static_cast<qint64>(number);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool isSha256(const QString& value)
|
||||
{
|
||||
if (value.size() != 64)
|
||||
return false;
|
||||
|
||||
for (const QChar ch : value)
|
||||
{
|
||||
const ushort code = ch.unicode();
|
||||
if (!((code >= '0' && code <= '9')
|
||||
|| (code >= 'a' && code <= 'f')
|
||||
|| (code >= 'A' && code <= 'F')))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
UpdaterLogic::UpdaterLogic(QObject* parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
@@ -32,6 +70,11 @@ UpdaterLogic::UpdaterLogic(QObject* parent)
|
||||
|
||||
void UpdaterLogic::getManifest(const QString& appId, const QString& channel, const QString& targetVer, int versionId)
|
||||
{
|
||||
m_manifest = QJsonObject();
|
||||
m_manifestText.clear();
|
||||
m_fileItems.clear();
|
||||
m_manifestSignatureVerified = false;
|
||||
|
||||
QString url = m_serverAddr + "/api/v1/update/manifest";
|
||||
QJsonObject body;
|
||||
body["app_id"] = appId;
|
||||
@@ -39,35 +82,64 @@ 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();
|
||||
m_manifestText.clear();
|
||||
m_fileItems.clear();
|
||||
m_manifestSignatureVerified = false;
|
||||
|
||||
if (code == 200)
|
||||
{
|
||||
if (resp.contains("manifest_text") && resp.contains("manifest"))
|
||||
const QJsonValue manifestTextValue = resp.value("manifest_text");
|
||||
const QJsonValue responseManifestValue = resp.value("manifest");
|
||||
if (!manifestTextValue.isString() || !responseManifestValue.isObject())
|
||||
{
|
||||
m_manifestText = resp["manifest_text"].toString();
|
||||
m_manifest = resp["manifest"].toObject();
|
||||
qDebug() << "Received manifest version:" << m_manifest.value("version").toString();
|
||||
m_fileItems.clear();
|
||||
QJsonArray files = m_manifest.value("files").toArray();
|
||||
for (const QJsonValue& fileItem : files)
|
||||
{
|
||||
QJsonObject fileObj = fileItem.toObject();
|
||||
FileDownloadItem fi;
|
||||
fi.path = fileObj.value("path").toString();
|
||||
fi.sha256 = fileObj.value("sha256").toString();
|
||||
fi.size = fileObj.value("size").toVariant().toLongLong();
|
||||
fi.url = m_serverAddr + "/api/v1/update/file/" + fi.path; // placeholder, actual download URL uses download-url or signed object URL
|
||||
m_fileItems.append(fi);
|
||||
}
|
||||
qDebug() << "Manifest response fields have invalid types";
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "Manifest response missing fields";
|
||||
const QString manifestText = manifestTextValue.toString();
|
||||
const QJsonObject responseManifest = responseManifestValue.toObject();
|
||||
const QJsonValue signatureValue = responseManifest.value("signature");
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument manifestDocument =
|
||||
QJsonDocument::fromJson(manifestText.toUtf8(), &parseError);
|
||||
|
||||
if (manifestText.isEmpty() || !signatureValue.isString()
|
||||
|| signatureValue.toString().trimmed().isEmpty()
|
||||
|| parseError.error != QJsonParseError::NoError
|
||||
|| !manifestDocument.isObject())
|
||||
{
|
||||
qDebug() << "Manifest response JSON or signature field is invalid";
|
||||
}
|
||||
else
|
||||
{
|
||||
QJsonObject trustedManifest = manifestDocument.object();
|
||||
QList<FileDownloadItem> trustedFiles;
|
||||
if (trustedManifest.contains("signature"))
|
||||
{
|
||||
qDebug() << "Signed manifest text unexpectedly contains a signature field";
|
||||
}
|
||||
else
|
||||
{
|
||||
trustedManifest.insert("signature", signatureValue.toString());
|
||||
if (validateOnlineManifest(trustedManifest, appId, channel,
|
||||
targetVer, versionId, &trustedFiles))
|
||||
{
|
||||
m_manifestText = manifestText;
|
||||
m_manifest = trustedManifest;
|
||||
m_fileItems = trustedFiles;
|
||||
qDebug() << "Received trusted manifest version:"
|
||||
<< m_manifest.value("version").toString();
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "Manifest fields or request identity are invalid";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -78,6 +150,76 @@ void UpdaterLogic::getManifest(const QString& appId, const QString& channel, con
|
||||
});
|
||||
}
|
||||
|
||||
bool UpdaterLogic::validateOnlineManifest(const QJsonObject& manifest, const QString& appId,
|
||||
const QString& channel, const QString& targetVer,
|
||||
int versionId, QList<FileDownloadItem>* fileItems) const
|
||||
{
|
||||
if (!fileItems)
|
||||
return false;
|
||||
fileItems->clear();
|
||||
|
||||
const QJsonValue appValue = manifest.value("app_id");
|
||||
const QJsonValue channelValue = manifest.value("channel");
|
||||
const QJsonValue versionValue = manifest.value("version");
|
||||
const QJsonValue sequenceValue = manifest.value("manifest_seq");
|
||||
const QJsonValue filesValue = manifest.value("files");
|
||||
const QJsonValue signatureValue = manifest.value("signature");
|
||||
qint64 manifestSequence = -1;
|
||||
if (!appValue.isString() || appValue.toString().isEmpty()
|
||||
|| !channelValue.isString() || channelValue.toString().isEmpty()
|
||||
|| !versionValue.isString() || versionValue.toString().isEmpty()
|
||||
|| !jsonNonNegativeInteger(sequenceValue, &manifestSequence)
|
||||
|| !filesValue.isArray()
|
||||
|| !signatureValue.isString() || signatureValue.toString().trimmed().isEmpty()
|
||||
|| appValue.toString() != appId
|
||||
|| channelValue.toString() != channel
|
||||
|| versionValue.toString() != targetVer
|
||||
|| manifestSequence != static_cast<qint64>(versionId))
|
||||
return false;
|
||||
|
||||
QSet<QString> pathKeys;
|
||||
const QJsonArray files = filesValue.toArray();
|
||||
fileItems->reserve(files.size());
|
||||
for (const QJsonValue& value : files)
|
||||
{
|
||||
if (!value.isObject())
|
||||
{
|
||||
fileItems->clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
const QJsonObject file = value.toObject();
|
||||
const QJsonValue pathValue = file.value("path");
|
||||
const QJsonValue shaValue = file.value("sha256");
|
||||
const QJsonValue sizeValue = file.value("size");
|
||||
qint64 size = -1;
|
||||
if (!pathValue.isString() || pathValue.toString().isEmpty()
|
||||
|| !shaValue.isString() || !isSha256(shaValue.toString())
|
||||
|| !jsonNonNegativeInteger(sizeValue, &size))
|
||||
{
|
||||
fileItems->clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString path = pathValue.toString();
|
||||
const QString normalizedPath = QDir::cleanPath(QDir::fromNativeSeparators(path));
|
||||
const QString pathKey = normalizedPath.toCaseFolded();
|
||||
if (!isSafeRelativePath(path) || pathKeys.contains(pathKey))
|
||||
{
|
||||
fileItems->clear();
|
||||
return false;
|
||||
}
|
||||
pathKeys.insert(pathKey);
|
||||
|
||||
FileDownloadItem item;
|
||||
item.path = path;
|
||||
item.sha256 = shaValue.toString();
|
||||
item.size = size;
|
||||
fileItems->append(item);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UpdaterLogic::verifySignature(const QByteArray& payload, const QString& signatureBase64, const QString& publicKeyPath) const
|
||||
{
|
||||
#ifndef HAVE_OPENSSL
|
||||
@@ -146,6 +288,7 @@ bool UpdaterLogic::verifySignature(const QByteArray& payload, const QString& sig
|
||||
|
||||
bool UpdaterLogic::verifyManifestSignature(const QString& publicKeyPath) const
|
||||
{
|
||||
m_manifestSignatureVerified = false;
|
||||
if (m_manifest.isEmpty() || m_manifestText.isEmpty())
|
||||
{
|
||||
qDebug() << "No manifest available to verify";
|
||||
@@ -158,17 +301,13 @@ bool UpdaterLogic::verifyManifestSignature(const QString& publicKeyPath) const
|
||||
return false;
|
||||
}
|
||||
|
||||
QString path = publicKeyPath;
|
||||
if (!QFile::exists(path))
|
||||
{
|
||||
path = QApplication::applicationDirPath() + "/" + publicKeyPath;
|
||||
}
|
||||
return verifySignature(m_manifestText.toUtf8(), signature, path);
|
||||
m_manifestSignatureVerified = verifySignature(m_manifestText.toUtf8(), signature, publicKeyPath);
|
||||
return m_manifestSignatureVerified;
|
||||
}
|
||||
|
||||
bool UpdaterLogic::saveManifestCache(const QString& cacheDir) const
|
||||
{
|
||||
if (m_manifest.isEmpty())
|
||||
if (m_manifest.isEmpty() || !m_manifestSignatureVerified)
|
||||
return false;
|
||||
|
||||
QDir dir(cacheDir);
|
||||
@@ -194,6 +333,11 @@ bool UpdaterLogic::saveManifestCache(const QString& cacheDir) const
|
||||
|
||||
bool UpdaterLogic::loadManifestCache(const QString& cacheDir, const QString& version)
|
||||
{
|
||||
m_manifest = QJsonObject();
|
||||
m_manifestText.clear();
|
||||
m_fileItems.clear();
|
||||
m_manifestSignatureVerified = false;
|
||||
|
||||
QString filePath = cacheDir + "/manifest_" + version + ".json";
|
||||
QFile file(filePath);
|
||||
if (!file.exists() || !file.open(QIODevice::ReadOnly))
|
||||
@@ -201,16 +345,37 @@ bool UpdaterLogic::loadManifestCache(const QString& cacheDir, const QString& ver
|
||||
|
||||
QByteArray raw = file.readAll();
|
||||
file.close();
|
||||
QJsonDocument doc = QJsonDocument::fromJson(raw);
|
||||
if (!doc.isObject())
|
||||
QJsonParseError wrapperError;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(raw, &wrapperError);
|
||||
if (wrapperError.error != QJsonParseError::NoError || !doc.isObject())
|
||||
return false;
|
||||
|
||||
QJsonObject wrapper = doc.object();
|
||||
if (!wrapper.contains("manifest") || !wrapper.contains("manifest_text"))
|
||||
const QJsonObject wrapper = doc.object();
|
||||
const QJsonValue manifestTextValue = wrapper.value("manifest_text");
|
||||
const QJsonValue cachedManifestValue = wrapper.value("manifest");
|
||||
if (!manifestTextValue.isString() || manifestTextValue.toString().isEmpty()
|
||||
|| !cachedManifestValue.isObject())
|
||||
return false;
|
||||
|
||||
m_manifest = wrapper["manifest"].toObject();
|
||||
m_manifestText = wrapper["manifest_text"].toString();
|
||||
const QJsonValue signatureValue = cachedManifestValue.toObject().value("signature");
|
||||
QJsonParseError manifestError;
|
||||
const QString manifestText = manifestTextValue.toString();
|
||||
const QJsonDocument manifestDocument =
|
||||
QJsonDocument::fromJson(manifestText.toUtf8(), &manifestError);
|
||||
if (!signatureValue.isString() || signatureValue.toString().trimmed().isEmpty()
|
||||
|| manifestError.error != QJsonParseError::NoError
|
||||
|| !manifestDocument.isObject())
|
||||
return false;
|
||||
|
||||
QJsonObject trustedManifest = manifestDocument.object();
|
||||
const QJsonValue versionValue = trustedManifest.value("version");
|
||||
if (trustedManifest.contains("signature") || !versionValue.isString()
|
||||
|| versionValue.toString() != version)
|
||||
return false;
|
||||
|
||||
trustedManifest.insert("signature", signatureValue.toString());
|
||||
m_manifest = trustedManifest;
|
||||
m_manifestText = manifestText;
|
||||
qDebug() << "Loaded cached manifest" << version;
|
||||
return true;
|
||||
}
|
||||
@@ -341,50 +506,66 @@ bool UpdaterLogic::loadOfflinePackage(const QString& packagePath, const QString&
|
||||
{
|
||||
m_offlineError.clear();
|
||||
QFile package(packagePath);
|
||||
if (!package.open(QIODevice::ReadOnly)) { m_offlineError = "无法打开离线更新包"; return false; }
|
||||
if (package.read(8) != QByteArray("MUPD0001", 8)) { m_offlineError = "离线包格式标识无效"; return false; }
|
||||
if (!package.open(QIODevice::ReadOnly)) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "Cannot open the offline update package."); return false; }
|
||||
if (package.read(8) != QByteArray("MUPD0001", 8)) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package format identifier is invalid."); return false; }
|
||||
const QByteArray lengthBytes = package.read(8);
|
||||
if (lengthBytes.size() != 8) { m_offlineError = "离线包头不完整"; return false; }
|
||||
if (lengthBytes.size() != 8) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package header is incomplete."); return false; }
|
||||
quint64 headerSize = 0;
|
||||
for (int i = 0; i < 8; ++i) headerSize |= quint64(static_cast<unsigned char>(lengthBytes[i])) << (i * 8);
|
||||
if (headerSize == 0 || headerSize > 64ULL * 1024 * 1024 || headerSize > quint64(package.size() - 16)) { m_offlineError = "离线包头长度无效"; return false; }
|
||||
if (headerSize == 0 || headerSize > 64ULL * 1024 * 1024 || headerSize > quint64(package.size() - 16)) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package header length is invalid."); return false; }
|
||||
QJsonParseError error;
|
||||
const QJsonDocument wrapperDoc = QJsonDocument::fromJson(package.read(qint64(headerSize)), &error);
|
||||
if (error.error != QJsonParseError::NoError || !wrapperDoc.isObject()) { m_offlineError = "离线包头 JSON 无效"; return false; }
|
||||
if (error.error != QJsonParseError::NoError || !wrapperDoc.isObject()) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package header contains invalid JSON."); return false; }
|
||||
const QJsonObject wrapper = wrapperDoc.object();
|
||||
const QByteArray packageText = wrapper.value("package_text").toString().toUtf8();
|
||||
const QByteArray manifestText = wrapper.value("manifest_text").toString().toUtf8();
|
||||
const QString publicKey = QApplication::applicationDirPath() + "/config/manifest_public_key.pem";
|
||||
if (!verifySignature(packageText, wrapper.value("package_signature").toString(), publicKey)) { m_offlineError = "离线包 RSA 签名无效"; return false; }
|
||||
const QString publicKey = QStringLiteral(":/simcae/update-client/manifest-public-key.pem");
|
||||
if (!verifySignature(packageText, wrapper.value("package_signature").toString(), publicKey)) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package RSA signature is invalid."); return false; }
|
||||
const QJsonObject packageMeta = QJsonDocument::fromJson(packageText, &error).object();
|
||||
if (error.error != QJsonParseError::NoError || packageMeta.value("format").toString() != "MUPD0001") { m_offlineError = "离线包签名元数据无效"; return false; }
|
||||
if (QString::fromLatin1(QCryptographicHash::hash(manifestText, QCryptographicHash::Sha256).toHex()) != packageMeta.value("manifest_sha256").toString()) { m_offlineError = "Manifest 摘要与包签名不一致"; return false; }
|
||||
if (error.error != QJsonParseError::NoError || packageMeta.value("format").toString() != "MUPD0001") { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The signed offline package metadata is invalid."); return false; }
|
||||
if (QString::fromLatin1(QCryptographicHash::hash(manifestText, QCryptographicHash::Sha256).toHex()) != packageMeta.value("manifest_sha256").toString()) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The manifest digest does not match the package signature."); return false; }
|
||||
QJsonObject manifest = QJsonDocument::fromJson(manifestText, &error).object();
|
||||
if (error.error != QJsonParseError::NoError || manifest.isEmpty()) { m_offlineError = "离线 Manifest 无效"; return false; }
|
||||
if (error.error != QJsonParseError::NoError || manifest.isEmpty()) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline manifest is invalid."); return false; }
|
||||
manifest.insert("signature", wrapper.value("manifest_signature").toString());
|
||||
m_manifest = manifest; m_manifestText = QString::fromUtf8(manifestText); m_fileItems.clear();
|
||||
if (manifest.value("app_id") != packageMeta.value("app_id") || manifest.value("channel") != packageMeta.value("channel") || manifest.value("version") != packageMeta.value("version")) { m_offlineError = "包信息与 Manifest 身份不一致"; return false; }
|
||||
if (!verifyManifestSignature()) { m_offlineError = "离线 Manifest RSA 签名无效"; return false; }
|
||||
if (manifest.value("app_id") != packageMeta.value("app_id") || manifest.value("channel") != packageMeta.value("channel") || manifest.value("version") != packageMeta.value("version")) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The package metadata and manifest identity do not match."); return false; }
|
||||
if (!verifyManifestSignature()) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline manifest RSA signature is invalid."); return false; }
|
||||
const qint64 payloadStart = 16 + qint64(headerSize);
|
||||
const QJsonArray entries = packageMeta.value("files").toArray();
|
||||
for (const QJsonValue& value : entries) {
|
||||
const QJsonObject item = value.toObject(); const QString path = QDir::fromNativeSeparators(item.value("path").toString());
|
||||
const qint64 offset = item.value("offset").toVariant().toLongLong(); const qint64 size = item.value("size").toVariant().toLongLong();
|
||||
if (!isSafeRelativePath(path) || offset < 0 || size < 0 || payloadStart + offset + size > package.size()) { m_offlineError = "离线包包含不安全路径或越界数据: " + path; return false; }
|
||||
if (!isSafeRelativePath(path) || offset < 0 || size < 0 || payloadStart + offset + size > package.size()) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package contains an unsafe path or out-of-bounds data: %1").arg(path); return false; }
|
||||
FileDownloadItem fi{path, QString(), item.value("sha256").toString(), size}; m_fileItems.append(fi);
|
||||
if (stagingDir.isEmpty()) continue;
|
||||
const QString target = QDir(stagingDir).filePath(path); if (!QDir().mkpath(QFileInfo(target).path()) || !package.seek(payloadStart + offset)) { m_offlineError = "无法准备离线文件: " + path; return false; }
|
||||
QSaveFile output(target); if (!output.open(QIODevice::WriteOnly)) { m_offlineError = "无法创建暂存文件: " + path; return false; }
|
||||
const QString target = QDir(stagingDir).filePath(path); if (!QDir().mkpath(QFileInfo(target).path()) || !package.seek(payloadStart + offset)) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "Cannot prepare the offline file: %1").arg(path); return false; }
|
||||
QSaveFile output(target); if (!output.open(QIODevice::WriteOnly)) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "Cannot create the staged file: %1").arg(path); return false; }
|
||||
QCryptographicHash hash(QCryptographicHash::Sha256); qint64 remaining = size;
|
||||
while (remaining > 0) { const QByteArray block = package.read(qMin<qint64>(remaining, 1024 * 1024)); if (block.isEmpty() || output.write(block) != block.size()) { output.cancelWriting(); m_offlineError = "离线文件读取失败: " + path; return false; } hash.addData(block); remaining -= block.size(); }
|
||||
if (QString::fromLatin1(hash.result().toHex()).compare(fi.sha256, Qt::CaseInsensitive) != 0 || !output.commit()) { output.cancelWriting(); m_offlineError = "离线文件 Hash 或写入失败: " + path; return false; }
|
||||
while (remaining > 0) { const QByteArray block = package.read(qMin<qint64>(remaining, 1024 * 1024)); if (block.isEmpty() || output.write(block) != block.size()) { output.cancelWriting(); m_offlineError = QCoreApplication::translate("UpdaterLogic", "Failed to read the offline file: %1").arg(path); return false; } hash.addData(block); remaining -= block.size(); }
|
||||
if (QString::fromLatin1(hash.result().toHex()).compare(fi.sha256, Qt::CaseInsensitive) != 0 || !output.commit()) { output.cancelWriting(); m_offlineError = QCoreApplication::translate("UpdaterLogic", "Offline file hash validation or writing failed: %1").arg(path); return false; }
|
||||
}
|
||||
if (entries.size() != m_manifest.value("files").toArray().size()) { m_offlineError = "离线包文件数量与 Manifest 不一致"; return false; }
|
||||
if (entries.size() != m_manifest.value("files").toArray().size()) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The number of files in the offline package does not match the manifest."); return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
void UpdaterLogic::getDownloadUrl(const QString& appId, const QString& channel, const QString& targetVer, int versionId)
|
||||
{
|
||||
QList<FileDownloadItem> signedFiles;
|
||||
m_fileItems.clear();
|
||||
if (!m_manifestSignatureVerified
|
||||
|| !validateOnlineManifest(m_manifest, appId, channel, targetVer, versionId, &signedFiles))
|
||||
{
|
||||
qDebug() << "Download URL request rejected because the signed manifest is not valid for the request";
|
||||
emit fetchUrlFinished();
|
||||
return;
|
||||
}
|
||||
if (signedFiles.isEmpty())
|
||||
{
|
||||
qDebug() << "Download URL request rejected because the signed manifest has no files";
|
||||
emit fetchUrlFinished();
|
||||
return;
|
||||
}
|
||||
|
||||
QString url = m_serverAddr + "/api/v1/update/download-url";
|
||||
QJsonObject body;
|
||||
body["app_id"] = appId;
|
||||
@@ -395,25 +576,94 @@ 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, signedFiles](int code, const QJsonObject& resp)
|
||||
{
|
||||
qDebug() << "Download URL API returned code:" << code;
|
||||
m_fileItems.clear();
|
||||
|
||||
if (code == 200)
|
||||
{
|
||||
QJsonArray fileArr = resp["files"].toArray();
|
||||
for (auto item : fileArr)
|
||||
const QJsonValue filesValue = resp.value("files");
|
||||
if (!filesValue.isArray())
|
||||
{
|
||||
QJsonObject obj = item.toObject();
|
||||
FileDownloadItem fi;
|
||||
fi.path = obj["path"].toString();
|
||||
fi.url = obj["url"].toString();
|
||||
fi.sha256 = obj["sha256"].toString();
|
||||
fi.size = obj["size"].toVariant().toLongLong();
|
||||
m_fileItems.append(fi);
|
||||
qDebug() << "File info:" << fi.path << fi.url << fi.sha256;
|
||||
qDebug() << "Download URL response files field is invalid";
|
||||
emit fetchUrlFinished();
|
||||
return;
|
||||
}
|
||||
|
||||
const QJsonArray responseFiles = filesValue.toArray();
|
||||
if (responseFiles.size() != signedFiles.size())
|
||||
{
|
||||
qDebug() << "Download URL response file count does not match the signed manifest";
|
||||
emit fetchUrlFinished();
|
||||
return;
|
||||
}
|
||||
|
||||
QMap<QString, int> signedIndexes;
|
||||
for (int index = 0; index < signedFiles.size(); ++index)
|
||||
signedIndexes.insert(signedFiles.at(index).path, index);
|
||||
|
||||
QList<FileDownloadItem> boundFiles = signedFiles;
|
||||
QSet<QString> responsePaths;
|
||||
for (const QJsonValue& value : responseFiles)
|
||||
{
|
||||
if (!value.isObject())
|
||||
{
|
||||
qDebug() << "Download URL response contains a non-object file item";
|
||||
emit fetchUrlFinished();
|
||||
return;
|
||||
}
|
||||
|
||||
const QJsonObject responseFile = value.toObject();
|
||||
const QJsonValue pathValue = responseFile.value("path");
|
||||
const QJsonValue urlValue = responseFile.value("url");
|
||||
const QJsonValue shaValue = responseFile.value("sha256");
|
||||
const QUrl downloadUrl(urlValue.toString());
|
||||
qint64 size = -1;
|
||||
if (!pathValue.isString() || pathValue.toString().isEmpty()
|
||||
|| !urlValue.isString() || urlValue.toString().trimmed().isEmpty()
|
||||
|| !downloadUrl.isValid() || downloadUrl.host().isEmpty()
|
||||
|| (downloadUrl.scheme().compare("http", Qt::CaseInsensitive) != 0
|
||||
&& downloadUrl.scheme().compare("https", Qt::CaseInsensitive) != 0)
|
||||
|| !shaValue.isString() || !isSha256(shaValue.toString())
|
||||
|| !jsonNonNegativeInteger(responseFile.value("size"), &size))
|
||||
{
|
||||
qDebug() << "Download URL response contains invalid file metadata";
|
||||
emit fetchUrlFinished();
|
||||
return;
|
||||
}
|
||||
|
||||
const QString path = pathValue.toString();
|
||||
if (responsePaths.contains(path) || !signedIndexes.contains(path))
|
||||
{
|
||||
qDebug() << "Download URL response contains a duplicate or extra file:" << path;
|
||||
emit fetchUrlFinished();
|
||||
return;
|
||||
}
|
||||
responsePaths.insert(path);
|
||||
|
||||
const int index = signedIndexes.value(path);
|
||||
const FileDownloadItem& signedFile = signedFiles.at(index);
|
||||
if (shaValue.toString().compare(signedFile.sha256, Qt::CaseInsensitive) != 0
|
||||
|| size != signedFile.size)
|
||||
{
|
||||
qDebug() << "Download URL response metadata differs from the signed manifest:" << path;
|
||||
emit fetchUrlFinished();
|
||||
return;
|
||||
}
|
||||
boundFiles[index].url = urlValue.toString();
|
||||
}
|
||||
|
||||
if (responsePaths.size() != signedFiles.size())
|
||||
{
|
||||
qDebug() << "Download URL response is missing signed manifest files";
|
||||
emit fetchUrlFinished();
|
||||
return;
|
||||
}
|
||||
|
||||
m_fileItems = boundFiles;
|
||||
qDebug() << "Bound download URLs to" << m_fileItems.size()
|
||||
<< "signed manifest files";
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -514,7 +764,7 @@ bool UpdaterLogic::downloadSingleFile(const QString& url, const QString& savePat
|
||||
|
||||
if (existingSize > 0 && httpStatus == 200)
|
||||
{
|
||||
// 服务端忽略 Range,当前文件是“旧片段 + 完整响应”,必须安全重下。
|
||||
// The server ignored Range, so restart instead of appending a full response to the partial file.
|
||||
qDebug() << "Server ignored Range; restart full download:" << savePath;
|
||||
QFile::remove(partPath);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user