feat(client): 迁移Hub更新客户端实现
This commit is contained in:
+11
-11
@@ -1,23 +1,23 @@
|
||||
project(Common LANGUAGES C CXX)
|
||||
|
||||
set(SRC
|
||||
HttpHelper.h
|
||||
HttpHelper.cpp
|
||||
FileHelper.h
|
||||
FileHelper.cpp
|
||||
HttpHelper.h
|
||||
HttpHelper.cpp
|
||||
FileHelper.h
|
||||
FileHelper.cpp
|
||||
ConfigHelper.h
|
||||
ConfigHelper.cpp
|
||||
PolicyHelper.h
|
||||
PolicyHelper.cpp
|
||||
LocalStateHelper.h
|
||||
LocalStateHelper.cpp
|
||||
TicketHelper.h
|
||||
TicketHelper.cpp
|
||||
IntegrityHelper.h
|
||||
IntegrityHelper.cpp
|
||||
DeviceIdentityHelper.h
|
||||
DeviceIdentityHelper.cpp
|
||||
)
|
||||
TicketHelper.h
|
||||
TicketHelper.cpp
|
||||
UpdatePathPolicy.h
|
||||
UpdatePathPolicy.cpp
|
||||
IntegrityHelper.h
|
||||
IntegrityHelper.cpp
|
||||
)
|
||||
add_library(Common STATIC ${SRC})
|
||||
|
||||
# Common编译自身需要OpenSSL头文件
|
||||
|
||||
+22
-17
@@ -35,8 +35,8 @@ const QString kConfigKeyPrefix = QStringLiteral("--config-key-b64=");
|
||||
const QString kConfigValuePrefix = QStringLiteral("--config-value-b64=");
|
||||
const QString kFilePathPrefix = QStringLiteral("--file-path-b64=");
|
||||
const QString kFileDataPrefix = QStringLiteral("--file-data-b64=");
|
||||
const QString kRegistryOrganization = QStringLiteral("Marsco");
|
||||
const QString kRegistryApplication = QStringLiteral("UpdateClientSDK");
|
||||
const QString kRegistryOrganization = QStringLiteral("SimCAE");
|
||||
const QString kRegistryApplication = QStringLiteral("HubUpdateClient");
|
||||
const QString kRegistryInstallationsGroup = QStringLiteral("installations");
|
||||
const QString kRegistryConfigGroup = QStringLiteral("config");
|
||||
const QString kRegistryMetaGroup = QStringLiteral("_meta");
|
||||
@@ -178,7 +178,8 @@ bool isRegistryManagedConfigKey(const QString& key)
|
||||
{
|
||||
// 服务端地址是编译期 qrc 配置,不进入注册表。
|
||||
// 其他运行配置会在 Launcher 首次启动时导入注册表,之后以注册表为准。
|
||||
return key != kApiBaseUrlKey;
|
||||
Q_UNUSED(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool isPathInsideDirectory(const QString& path, const QString& directory)
|
||||
@@ -478,7 +479,6 @@ ConfigHelper::ConfigHelper()
|
||||
QCryptographicHash::Sha256).toHex());
|
||||
migrateLegacyIniIfNeeded();
|
||||
syncRegistryFromConfigFileIfChanged();
|
||||
removeRegistryValue(kApiBaseUrlKey);
|
||||
qDebug() << "Loading app config path:" << m_configPath;
|
||||
qDebug() << "File exists?" << QFile::exists(m_configPath);
|
||||
qDebug() << "Registry installation id:" << m_registryInstallId;
|
||||
@@ -508,7 +508,7 @@ QString ConfigHelper::dataRoot() const
|
||||
if (base.isEmpty())
|
||||
base = QDir::homePath();
|
||||
return QDir::cleanPath(QDir(base).filePath(
|
||||
QStringLiteral("Marsco/UpdateClientSDK/installations/%1").arg(m_registryInstallId)));
|
||||
QStringLiteral("SimCAE/HubUpdateClient/installations/%1").arg(m_registryInstallId)));
|
||||
}
|
||||
|
||||
QString ConfigHelper::dataConfigDir() const
|
||||
@@ -788,16 +788,18 @@ QString ConfigHelper::readFileValue(const QString& key) const
|
||||
QString ConfigHelper::getValue(const QString& section, const QString& key) const
|
||||
{
|
||||
Q_UNUSED(section);
|
||||
const QString embeddedValue = readEmbeddedValue(key);
|
||||
if (!embeddedValue.isEmpty())
|
||||
return embeddedValue;
|
||||
if (!isRegistryManagedConfigKey(key))
|
||||
return QString();
|
||||
|
||||
QString value;
|
||||
if (readRegistryValue(key, &value))
|
||||
return value;
|
||||
return readFileValue(key);
|
||||
|
||||
value = readFileValue(key).trimmed();
|
||||
if (!value.isEmpty())
|
||||
return value;
|
||||
|
||||
return readEmbeddedValue(key);
|
||||
}
|
||||
|
||||
bool ConfigHelper::setValue(const QString& section, const QString& key, const QString& value)
|
||||
@@ -824,15 +826,15 @@ bool ConfigHelper::migrateLegacyIniIfNeeded()
|
||||
config.insert(key, value);
|
||||
};
|
||||
|
||||
copyText("App", "app_id");
|
||||
copyText("App", "app_name", "Marsco Demo App");
|
||||
copyText("App", "app_id");
|
||||
copyText("App", "product_code", ini.value("App/app_id").toString());
|
||||
copyText("App", "app_name", "SimCAE");
|
||||
copyText("App", "channel", "stable");
|
||||
copyText("App", "current_version", "1.0.0");
|
||||
copyText("App", "client_protocol", "3");
|
||||
copyText("App", "launch_token");
|
||||
copyText("License", "license_key");
|
||||
copyText("Server", "api_base_url");
|
||||
copyText("Server", "client_token");
|
||||
copyText("App", "client_protocol", "3");
|
||||
copyText("Server", "client_token");
|
||||
copyText("App", "launch_token");
|
||||
copyText("Server", "api_base_url");
|
||||
copyText("Update", "request_timeout_ms", "5000");
|
||||
copyText("Update", "temp_folder", "update_temp");
|
||||
copyText("Update", "device_id");
|
||||
@@ -842,6 +844,9 @@ bool ConfigHelper::migrateLegacyIniIfNeeded()
|
||||
copyText("Runtime", "updater_executable", ConfigHelper::executableNameForCurrentPlatform(QString(), "Updater"));
|
||||
copyText("Runtime", "bootstrap_executable", ConfigHelper::executableNameForCurrentPlatform(QString(), "Bootstrap"));
|
||||
copyText("Runtime", "health_check_timeout_ms", "15000");
|
||||
copyText("Security", "require_manifest_signature", "false");
|
||||
copyText("Security", "verify_installed_on_start", "false");
|
||||
copyText("Platform", "abi");
|
||||
#ifdef Q_OS_WIN
|
||||
config.insert("platform", "windows");
|
||||
#elif defined(Q_OS_LINUX)
|
||||
@@ -849,7 +854,7 @@ bool ConfigHelper::migrateLegacyIniIfNeeded()
|
||||
#else
|
||||
config.insert("platform", "unknown");
|
||||
#endif
|
||||
config.insert("arch", "x64");
|
||||
config.insert("arch", "x86_64");
|
||||
|
||||
QDir().mkpath(QFileInfo(m_configPath).path());
|
||||
QSaveFile output(m_configPath);
|
||||
|
||||
@@ -1,323 +0,0 @@
|
||||
#include "DeviceIdentityHelper.h"
|
||||
#include "ConfigHelper.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QCryptographicHash>
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QEventLoop>
|
||||
#include <QFile>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QSysInfo>
|
||||
#include <QTimer>
|
||||
#include <QUuid>
|
||||
|
||||
#ifdef HAVE_OPENSSL
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/pem.h>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
QString manifestPublicKeyPath(const QString &installDir)
|
||||
{
|
||||
return QDir(installDir).filePath(QStringLiteral("config/manifest_public_key.pem"));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
DeviceIdentityHelper::DeviceIdentityHelper(const QString &installDir)
|
||||
: m_installDir(installDir)
|
||||
{
|
||||
}
|
||||
|
||||
QString DeviceIdentityHelper::deviceId() const
|
||||
{
|
||||
return m_deviceId;
|
||||
}
|
||||
|
||||
QString DeviceIdentityHelper::errorString() const
|
||||
{
|
||||
return m_error;
|
||||
}
|
||||
|
||||
bool DeviceIdentityHelper::verifySignature(const QByteArray &payload, const QString &signatureBase64)
|
||||
{
|
||||
#ifndef HAVE_OPENSSL
|
||||
Q_UNUSED(payload);
|
||||
Q_UNUSED(signatureBase64);
|
||||
m_error = QCoreApplication::translate(
|
||||
"DeviceIdentityHelper",
|
||||
"OpenSSL is unavailable, so device credential signature cannot be verified.");
|
||||
return false;
|
||||
#else
|
||||
const QString keyPath = manifestPublicKeyPath(m_installDir);
|
||||
QFile keyFile(keyPath);
|
||||
if (!keyFile.open(QIODevice::ReadOnly)) {
|
||||
m_error = QCoreApplication::translate("DeviceIdentityHelper", "Device public key is missing: %1").arg(keyPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
const QByteArray keyData = keyFile.readAll();
|
||||
BIO *bio = BIO_new_mem_buf(keyData.constData(), keyData.size());
|
||||
EVP_PKEY *publicKey = bio ? PEM_read_bio_PUBKEY(bio, nullptr, nullptr, nullptr) : nullptr;
|
||||
if (bio) {
|
||||
BIO_free(bio);
|
||||
}
|
||||
if (!publicKey) {
|
||||
m_error = QCoreApplication::translate("DeviceIdentityHelper", "Device public key is invalid: %1").arg(keyPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
EVP_MD_CTX *ctx = EVP_MD_CTX_new();
|
||||
const QByteArray signature = QByteArray::fromBase64(signatureBase64.toUtf8());
|
||||
const bool ok = ctx
|
||||
&& EVP_DigestVerifyInit(ctx, nullptr, EVP_sha256(), nullptr, publicKey) == 1
|
||||
&& EVP_DigestVerifyUpdate(ctx, payload.constData(), payload.size()) == 1
|
||||
&& EVP_DigestVerifyFinal(
|
||||
ctx,
|
||||
reinterpret_cast<const unsigned char *>(signature.constData()),
|
||||
signature.size()) == 1;
|
||||
|
||||
if (ctx) {
|
||||
EVP_MD_CTX_free(ctx);
|
||||
}
|
||||
EVP_PKEY_free(publicKey);
|
||||
|
||||
if (!ok) {
|
||||
m_error = QCoreApplication::translate(
|
||||
"DeviceIdentityHelper",
|
||||
"Device credential signature is invalid. The local identity file may not match this server.");
|
||||
}
|
||||
return ok;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool DeviceIdentityHelper::loadAndVerify(const QString &expectedAppId, const QString &expectedChannel)
|
||||
{
|
||||
// client_identity.dat 是服务端签发的本机设备凭证,不是用户可手写配置。
|
||||
// 本地启动时先用公钥校验签名,再校验 app/channel/license/installation/device 和有效期。
|
||||
QString credentialPath = ConfigHelper::instance().clientIdentityPath();
|
||||
if (!QFile::exists(credentialPath)) {
|
||||
credentialPath = QDir(m_installDir).filePath(QStringLiteral("config/client_identity.dat"));
|
||||
}
|
||||
|
||||
QFile credentialFile(credentialPath);
|
||||
if (!credentialFile.open(QIODevice::ReadOnly)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument wrapperDoc = QJsonDocument::fromJson(credentialFile.readAll(), &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !wrapperDoc.isObject()) {
|
||||
m_error = QCoreApplication::translate("DeviceIdentityHelper", "Device credential file is not valid JSON: %1")
|
||||
.arg(credentialPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
const QJsonObject wrapper = wrapperDoc.object();
|
||||
const QByteArray identityText = wrapper.value(QStringLiteral("identity_text")).toString().toUtf8();
|
||||
const QString signature = wrapper.value(QStringLiteral("signature")).toString();
|
||||
if (identityText.isEmpty() || !verifySignature(identityText, signature)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QJsonDocument identityDoc = QJsonDocument::fromJson(identityText);
|
||||
const QJsonObject identity = identityDoc.object();
|
||||
const QDateTime expiry = QDateTime::fromString(
|
||||
identity.value(QStringLiteral("valid_until")).toString(),
|
||||
Qt::ISODate);
|
||||
|
||||
const bool identityMatches = identity.value(QStringLiteral("app_id")).toString() == expectedAppId
|
||||
&& identity.value(QStringLiteral("channel")).toString() == expectedChannel
|
||||
&& !identity.value(QStringLiteral("license_id")).toString().isEmpty()
|
||||
&& !identity.value(QStringLiteral("installation_id")).toString().isEmpty()
|
||||
&& !identity.value(QStringLiteral("device_id")).toString().isEmpty();
|
||||
if (!identityMatches) {
|
||||
m_error = QCoreApplication::translate(
|
||||
"DeviceIdentityHelper",
|
||||
"Device credential does not match this application, channel, license, installation or device.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!expiry.isValid() || expiry <= QDateTime::currentDateTimeUtc()) {
|
||||
m_error = QCoreApplication::translate(
|
||||
"DeviceIdentityHelper",
|
||||
"License has expired. Please ask the administrator to issue a new License.");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_deviceId = identity.value(QStringLiteral("device_id")).toString();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DeviceIdentityHelper::verifyLocal(const QString &appId, const QString &channel)
|
||||
{
|
||||
m_error.clear();
|
||||
return loadAndVerify(appId, channel);
|
||||
}
|
||||
|
||||
bool DeviceIdentityHelper::ensureIssued(
|
||||
const QString &apiBaseUrl,
|
||||
const QString &clientToken,
|
||||
const QString &appId,
|
||||
const QString &channel,
|
||||
const QString &licenseKey)
|
||||
{
|
||||
// 首次启动或本地凭证失效时,Launcher 会拿 License 向服务端登记设备。
|
||||
// 服务端返回签名后的 identity_text,客户端保存为 client_identity.dat,并把真实 device_id 写入运行配置。
|
||||
m_error.clear();
|
||||
if (loadAndVerify(appId, channel)) {
|
||||
ConfigHelper::instance().setValue(QStringLiteral("Update"), QStringLiteral("device_id"), m_deviceId);
|
||||
return true;
|
||||
}
|
||||
|
||||
const QString trimmedBaseUrl = apiBaseUrl.trimmed();
|
||||
if (appId.trimmed().isEmpty()) {
|
||||
m_error = QCoreApplication::translate("DeviceIdentityHelper", "app_id is empty in app_config.json.");
|
||||
return false;
|
||||
}
|
||||
if (channel.trimmed().isEmpty()) {
|
||||
m_error = QCoreApplication::translate("DeviceIdentityHelper", "channel is empty in app_config.json.");
|
||||
return false;
|
||||
}
|
||||
if (trimmedBaseUrl.isEmpty() || trimmedBaseUrl.contains(QStringLiteral("YOUR_SERVER_IP"), Qt::CaseInsensitive)) {
|
||||
m_error = QCoreApplication::translate(
|
||||
"DeviceIdentityHelper",
|
||||
"Server address is not configured. Set config/server_config.json before building Launcher, for example: http://192.168.229.128:8000");
|
||||
return false;
|
||||
}
|
||||
if (clientToken.trimmed().isEmpty()) {
|
||||
m_error = QCoreApplication::translate(
|
||||
"DeviceIdentityHelper",
|
||||
"client_token is empty. Copy the client_token generated by the admin page into app_config.json.");
|
||||
return false;
|
||||
}
|
||||
if (licenseKey.trimmed().isEmpty()) {
|
||||
m_error = QCoreApplication::translate(
|
||||
"DeviceIdentityHelper",
|
||||
"License is empty. Create or select a License in the admin page, then copy the generated client configuration.");
|
||||
return false;
|
||||
}
|
||||
|
||||
ConfigHelper &config = ConfigHelper::instance();
|
||||
QString installationId = config.getValue(QStringLiteral("Device"), QStringLiteral("installation_id"));
|
||||
if (installationId.isEmpty()) {
|
||||
installationId = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
if (!config.setValue(QStringLiteral("Device"), QStringLiteral("installation_id"), installationId)) {
|
||||
m_error = QCoreApplication::translate("DeviceIdentityHelper", "Cannot save installation id to %1: %2")
|
||||
.arg(config.configPath(), config.lastError());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const QByteArray machine = QSysInfo::machineUniqueId() + installationId.toUtf8();
|
||||
const QString machineHash = QString::fromLatin1(
|
||||
QCryptographicHash::hash(machine, QCryptographicHash::Sha256).toHex());
|
||||
const QJsonObject body{
|
||||
{QStringLiteral("app_id"), appId},
|
||||
{QStringLiteral("channel"), channel},
|
||||
{QStringLiteral("license_key"), licenseKey},
|
||||
{QStringLiteral("installation_id"), installationId},
|
||||
{QStringLiteral("machine_hash"), machineHash},
|
||||
};
|
||||
|
||||
QNetworkAccessManager manager;
|
||||
QNetworkRequest request{QUrl(trimmedBaseUrl + QStringLiteral("/api/v1/device/issue"))};
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
|
||||
request.setRawHeader("X-Client-Token", clientToken.toUtf8());
|
||||
|
||||
QNetworkReply *reply = manager.post(request, QJsonDocument(body).toJson(QJsonDocument::Compact));
|
||||
QEventLoop loop;
|
||||
QTimer timer;
|
||||
timer.setSingleShot(true);
|
||||
|
||||
bool timeoutOk = false;
|
||||
int timeoutMs = ConfigHelper::instance()
|
||||
.getValue(QStringLiteral("Update"), QStringLiteral("request_timeout_ms"))
|
||||
.toInt(&timeoutOk);
|
||||
if (!timeoutOk || timeoutMs < 1000) {
|
||||
timeoutMs = 5000;
|
||||
}
|
||||
|
||||
QObject::connect(&timer, &QTimer::timeout, [&]() {
|
||||
if (reply && reply->isRunning()) {
|
||||
reply->abort();
|
||||
}
|
||||
});
|
||||
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
|
||||
timer.start(timeoutMs);
|
||||
loop.exec();
|
||||
timer.stop();
|
||||
|
||||
const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
const QString networkError = reply->errorString();
|
||||
const QByteArray raw = reply->readAll();
|
||||
reply->deleteLater();
|
||||
if (status != 200) {
|
||||
QJsonParseError responseError;
|
||||
const QJsonDocument errorDoc = QJsonDocument::fromJson(raw, &responseError);
|
||||
QString serverMessage;
|
||||
if (responseError.error == QJsonParseError::NoError && errorDoc.isObject()) {
|
||||
const QJsonValue detail = errorDoc.object().value(QStringLiteral("detail"));
|
||||
serverMessage = detail.isObject()
|
||||
? detail.toObject().value(QStringLiteral("msg")).toString()
|
||||
: detail.toString();
|
||||
}
|
||||
if (serverMessage.isEmpty())
|
||||
serverMessage = QString::fromUtf8(raw).trimmed();
|
||||
|
||||
if (status == 0) {
|
||||
m_error = QCoreApplication::translate(
|
||||
"DeviceIdentityHelper",
|
||||
"Cannot contact the update server to issue device identity.\nServer: %1\nApp: %2\nChannel: %3\nNetwork error: %4\nTimeout: %5 ms")
|
||||
.arg(trimmedBaseUrl, appId, channel, networkError, QString::number(timeoutMs));
|
||||
} else {
|
||||
m_error = QCoreApplication::translate(
|
||||
"DeviceIdentityHelper",
|
||||
"Device identity request was rejected by the update server.\nServer: %1\nHTTP status: %2\nApp: %3\nChannel: %4\nServer message: %5")
|
||||
.arg(trimmedBaseUrl, QString::number(status), appId, channel,
|
||||
serverMessage.isEmpty() ? QCoreApplication::translate("DeviceIdentityHelper", "<empty response>") : serverMessage);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const QJsonDocument responseDoc = QJsonDocument::fromJson(raw);
|
||||
const QJsonObject response = responseDoc.object();
|
||||
if (response.value(QStringLiteral("identity_text")).toString().isEmpty()
|
||||
|| response.value(QStringLiteral("signature")).toString().isEmpty()) {
|
||||
m_error = QCoreApplication::translate(
|
||||
"DeviceIdentityHelper",
|
||||
"Server returned an invalid device identity response.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const QJsonObject wrapper{
|
||||
{QStringLiteral("identity_text"), response.value(QStringLiteral("identity_text"))},
|
||||
{QStringLiteral("signature"), response.value(QStringLiteral("signature"))},
|
||||
};
|
||||
const QByteArray credentialBytes = QJsonDocument(wrapper).toJson(QJsonDocument::Compact);
|
||||
const QString credentialPath = config.clientIdentityPath();
|
||||
|
||||
QString writeError;
|
||||
if (!ConfigHelper::writeFileWithElevationIfNeeded(credentialPath, credentialBytes, &writeError)) {
|
||||
m_error = QCoreApplication::translate("DeviceIdentityHelper", "Cannot save device credential to %1: %2")
|
||||
.arg(credentialPath, writeError);
|
||||
return false;
|
||||
}
|
||||
if (!loadAndVerify(appId, channel)) {
|
||||
return false;
|
||||
}
|
||||
if (!config.setValue(QStringLiteral("Update"), QStringLiteral("device_id"), m_deviceId)) {
|
||||
m_error = QCoreApplication::translate("DeviceIdentityHelper", "Cannot save server device id to %1: %2")
|
||||
.arg(config.configPath(), config.lastError());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
|
||||
class DeviceIdentityHelper {
|
||||
public:
|
||||
explicit DeviceIdentityHelper(const QString &installDir);
|
||||
|
||||
bool ensureIssued(
|
||||
const QString &apiBaseUrl,
|
||||
const QString &clientToken,
|
||||
const QString &appId,
|
||||
const QString &channel,
|
||||
const QString &licenseKey);
|
||||
bool verifyLocal(const QString &appId, const QString &channel);
|
||||
|
||||
QString deviceId() const;
|
||||
QString errorString() const;
|
||||
|
||||
private:
|
||||
bool loadAndVerify(const QString &expectedAppId, const QString &expectedChannel);
|
||||
bool verifySignature(const QByteArray &payload, const QString &signatureBase64);
|
||||
|
||||
QString m_installDir;
|
||||
QString m_deviceId;
|
||||
QString m_error;
|
||||
};
|
||||
+128
-64
@@ -3,68 +3,132 @@
|
||||
#include "ConfigHelper.h"
|
||||
#include <QFile>
|
||||
#include <QDir>
|
||||
#include <QApplication>
|
||||
#include <QTimer>
|
||||
|
||||
void HttpHelper::postRequest(const QString& url, const QJsonObject& jsonBody,
|
||||
std::function<void(int code, const QJsonObject& resp)> callback)
|
||||
{
|
||||
QNetworkAccessManager* manager = new QNetworkAccessManager();
|
||||
manager->setProxy(QNetworkProxy::NoProxy);
|
||||
|
||||
QNetworkRequest req(url);
|
||||
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
// Add auth token header
|
||||
QString token = ConfigHelper::instance().getValue("Server", "client_token");
|
||||
req.setRawHeader("X-Client-Token", token.toUtf8());
|
||||
QString identityPath = ConfigHelper::instance().clientIdentityPath();
|
||||
if (!QFile::exists(identityPath))
|
||||
identityPath = QDir(QApplication::applicationDirPath()).filePath("config/client_identity.dat");
|
||||
QFile identity(identityPath);
|
||||
if (identity.open(QIODevice::ReadOnly))
|
||||
req.setRawHeader("X-Device-Credential", identity.readAll().toBase64());
|
||||
|
||||
QByteArray data = QJsonDocument(jsonBody).toJson(QJsonDocument::Compact);
|
||||
qDebug() << "=== POST Request ===";
|
||||
qDebug() << "Url:" << url;
|
||||
qDebug() << "Body:" << data;
|
||||
|
||||
QNetworkReply* reply = manager->post(req, data);
|
||||
QEventLoop loop;
|
||||
bool timeoutOk = false;
|
||||
int timeoutMs = ConfigHelper::instance().getValue("Update", "request_timeout_ms").toInt(&timeoutOk);
|
||||
if (!timeoutOk || timeoutMs < 1000) timeoutMs = 5000;
|
||||
QTimer timer;
|
||||
timer.setSingleShot(true);
|
||||
QObject::connect(&timer, &QTimer::timeout, [&]() {
|
||||
if (reply && reply->isRunning()) {
|
||||
qDebug() << "Request timeout, abort:" << url;
|
||||
reply->abort();
|
||||
}
|
||||
});
|
||||
|
||||
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
timer.start(timeoutMs);
|
||||
loop.exec();
|
||||
timer.stop();
|
||||
|
||||
int retCode = 0;
|
||||
QJsonObject retObj;
|
||||
|
||||
retCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
const QByteArray respData = reply->readAll();
|
||||
if (!respData.isEmpty()) {
|
||||
qDebug() << "Server raw response:" << respData;
|
||||
retObj = QJsonDocument::fromJson(respData).object();
|
||||
}
|
||||
if (reply->error() != QNetworkReply::NoError)
|
||||
{
|
||||
qDebug() << "Network error code:" << reply->error();
|
||||
qDebug() << "HTTP status:" << retCode << "detail:" << reply->errorString();
|
||||
}
|
||||
|
||||
callback(retCode, retObj);
|
||||
|
||||
reply->deleteLater();
|
||||
manager->deleteLater();
|
||||
#include <QApplication>
|
||||
#include <QJsonParseError>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
|
||||
namespace {
|
||||
|
||||
int requestTimeoutMs()
|
||||
{
|
||||
bool timeoutOk = false;
|
||||
int timeoutMs = ConfigHelper::instance().getValue("Update", "request_timeout_ms").toInt(&timeoutOk);
|
||||
if (!timeoutOk || timeoutMs < 1000)
|
||||
timeoutMs = 5000;
|
||||
return timeoutMs;
|
||||
}
|
||||
|
||||
void applyCommonHeaders(QNetworkRequest& req, const QString& bearerToken)
|
||||
{
|
||||
const QString clientToken = ConfigHelper::instance().getValue(QStringLiteral("Server"), QStringLiteral("client_token")).trimmed();
|
||||
if (!clientToken.isEmpty())
|
||||
req.setRawHeader("X-Client-Token", clientToken.toUtf8());
|
||||
|
||||
const QString token = bearerToken.trimmed();
|
||||
if (!token.isEmpty())
|
||||
req.setRawHeader("Authorization", QByteArray("Bearer ") + token.toUtf8());
|
||||
}
|
||||
|
||||
void readJsonReply(QNetworkReply* reply, int* retCode, QJsonObject* retObj)
|
||||
{
|
||||
*retCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
const QByteArray respData = reply->readAll();
|
||||
if (!respData.isEmpty()) {
|
||||
qDebug() << "Server raw response:" << respData;
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument document = QJsonDocument::fromJson(respData, &parseError);
|
||||
if (parseError.error == QJsonParseError::NoError && document.isObject())
|
||||
*retObj = document.object();
|
||||
}
|
||||
if (reply->error() != QNetworkReply::NoError)
|
||||
{
|
||||
qDebug() << "Network error code:" << reply->error();
|
||||
qDebug() << "HTTP status:" << *retCode << "detail:" << reply->errorString();
|
||||
}
|
||||
}
|
||||
|
||||
void waitForReply(const QString& url, QNetworkReply* reply)
|
||||
{
|
||||
QEventLoop loop;
|
||||
QTimer timer;
|
||||
timer.setSingleShot(true);
|
||||
QObject::connect(&timer, &QTimer::timeout, [&]() {
|
||||
if (reply && reply->isRunning()) {
|
||||
qDebug() << "Request timeout, abort:" << url;
|
||||
reply->abort();
|
||||
}
|
||||
});
|
||||
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
timer.start(requestTimeoutMs());
|
||||
loop.exec();
|
||||
timer.stop();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void HttpHelper::postRequest(const QString& url, const QJsonObject& jsonBody,
|
||||
std::function<void(int code, const QJsonObject& resp)> callback)
|
||||
{
|
||||
postRequest(url, jsonBody, QString(), callback);
|
||||
}
|
||||
|
||||
void HttpHelper::postRequest(const QString& url, const QJsonObject& jsonBody,
|
||||
const QString& bearerToken,
|
||||
std::function<void(int code, const QJsonObject& resp)> callback)
|
||||
{
|
||||
QNetworkAccessManager* manager = new QNetworkAccessManager();
|
||||
manager->setProxy(QNetworkProxy::NoProxy);
|
||||
|
||||
QNetworkRequest req{QUrl(url)};
|
||||
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
applyCommonHeaders(req, bearerToken);
|
||||
|
||||
QByteArray data = QJsonDocument(jsonBody).toJson(QJsonDocument::Compact);
|
||||
qDebug() << "=== POST Request ===";
|
||||
qDebug() << "Url:" << url;
|
||||
qDebug() << "Body:" << data;
|
||||
|
||||
QNetworkReply* reply = manager->post(req, data);
|
||||
waitForReply(url, reply);
|
||||
|
||||
int retCode = 0;
|
||||
QJsonObject retObj;
|
||||
readJsonReply(reply, &retCode, &retObj);
|
||||
|
||||
callback(retCode, retObj);
|
||||
|
||||
reply->deleteLater();
|
||||
manager->deleteLater();
|
||||
}
|
||||
|
||||
void HttpHelper::getRequest(const QString& url,
|
||||
std::function<void(int code, const QJsonObject& resp)> callback)
|
||||
{
|
||||
getRequest(url, QString(), callback);
|
||||
}
|
||||
|
||||
void HttpHelper::getRequest(const QString& url, const QString& bearerToken,
|
||||
std::function<void(int code, const QJsonObject& resp)> callback)
|
||||
{
|
||||
QNetworkAccessManager* manager = new QNetworkAccessManager();
|
||||
manager->setProxy(QNetworkProxy::NoProxy);
|
||||
|
||||
QNetworkRequest req{QUrl(url)};
|
||||
applyCommonHeaders(req, bearerToken);
|
||||
|
||||
qDebug() << "=== GET Request ===";
|
||||
qDebug() << "Url:" << url;
|
||||
|
||||
QNetworkReply* reply = manager->get(req);
|
||||
waitForReply(url, reply);
|
||||
|
||||
int retCode = 0;
|
||||
QJsonObject retObj;
|
||||
readJsonReply(reply, &retCode, &retObj);
|
||||
|
||||
callback(retCode, retObj);
|
||||
|
||||
reply->deleteLater();
|
||||
manager->deleteLater();
|
||||
}
|
||||
|
||||
+18
-10
@@ -3,15 +3,23 @@
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonDocument>
|
||||
#include <QEventLoop>
|
||||
#include <QDebug>
|
||||
|
||||
class HttpHelper
|
||||
{
|
||||
public:
|
||||
#include <QJsonObject>
|
||||
#include <QJsonDocument>
|
||||
#include <QEventLoop>
|
||||
#include <QDebug>
|
||||
#include <functional>
|
||||
|
||||
class HttpHelper
|
||||
{
|
||||
public:
|
||||
// Create an independent manager for each call instead of keeping it as a member.
|
||||
static void postRequest(const QString& url, const QJsonObject& jsonBody,
|
||||
std::function<void(int code, const QJsonObject& resp)> callback);
|
||||
static void postRequest(const QString& url, const QJsonObject& jsonBody,
|
||||
std::function<void(int code, const QJsonObject& resp)> callback);
|
||||
static void postRequest(const QString& url, const QJsonObject& jsonBody,
|
||||
const QString& bearerToken,
|
||||
std::function<void(int code, const QJsonObject& resp)> callback);
|
||||
static void getRequest(const QString& url,
|
||||
std::function<void(int code, const QJsonObject& resp)> callback);
|
||||
static void getRequest(const QString& url, const QString& bearerToken,
|
||||
std::function<void(int code, const QJsonObject& resp)> callback);
|
||||
};
|
||||
|
||||
+115
-54
@@ -1,52 +1,62 @@
|
||||
#include "IntegrityHelper.h"
|
||||
#include "ConfigHelper.h"
|
||||
#include <QCryptographicHash>
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
#include <QFile>
|
||||
#include "IntegrityHelper.h"
|
||||
#include "ConfigHelper.h"
|
||||
#include "UpdatePathPolicy.h"
|
||||
#include <QCryptographicHash>
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonArray>
|
||||
#include <QCoreApplication>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QSet>
|
||||
#ifdef HAVE_OPENSSL
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/pem.h>
|
||||
#endif
|
||||
|
||||
IntegrityHelper::IntegrityHelper(const QString& installDir)
|
||||
#ifdef HAVE_OPENSSL
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/pem.h>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
QString configValue(const QString& key, const QString& fallback = QString())
|
||||
{
|
||||
const QString value = ConfigHelper::instance().getValue(QString(), key).trimmed();
|
||||
return value.isEmpty() ? fallback : value;
|
||||
}
|
||||
|
||||
bool configFlag(const QString& key)
|
||||
{
|
||||
const QString value = configValue(key).toLower();
|
||||
return value == QStringLiteral("true")
|
||||
|| value == QStringLiteral("1")
|
||||
|| value == QStringLiteral("yes")
|
||||
|| value == QStringLiteral("on");
|
||||
}
|
||||
|
||||
bool manifestFileRequired(const QJsonObject& item)
|
||||
{
|
||||
if (!item.contains(QStringLiteral("required")))
|
||||
return true;
|
||||
return item.value(QStringLiteral("required")).toBool(true);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
IntegrityHelper::IntegrityHelper(const QString& installDir)
|
||||
: m_installDir(QDir::cleanPath(installDir)) {}
|
||||
|
||||
QString IntegrityHelper::errorString() const { return m_error; }
|
||||
|
||||
bool IntegrityHelper::safeRelativePath(const QString& path) const
|
||||
{
|
||||
const QString clean = QDir::cleanPath(QDir::fromNativeSeparators(path));
|
||||
return !clean.isEmpty() && !QDir::isAbsolutePath(clean) && clean != ".."
|
||||
&& !clean.startsWith("../") && !clean.contains(":");
|
||||
}
|
||||
|
||||
bool IntegrityHelper::safeRelativePath(const QString& path) const
|
||||
{
|
||||
return UpdatePathPolicy::isSafeRelativePath(path);
|
||||
}
|
||||
|
||||
bool IntegrityHelper::runtimeProtectedPath(const QString& path) const
|
||||
{
|
||||
// 这些文件属于 SDK 运行态,不参与业务版本文件的 Manifest 校验。
|
||||
// 例如 app_config.json、client_identity.dat 会随安装机器变化,不能要求它们和发布包 hash 完全一致。
|
||||
const QString p = QDir::fromNativeSeparators(path).toCaseFolded();
|
||||
QSet<QString> protectedPaths{
|
||||
"bootstrap", "bootstrap.exe", "client.ini", "config/app_config.json", "config/local_state.json",
|
||||
"config/client_identity.dat", "config/version_policy.dat"
|
||||
};
|
||||
const QString runtimePrefix = ConfigHelper::instance().runtimeRelativePath().toCaseFolded();
|
||||
if (!runtimePrefix.isEmpty()) {
|
||||
const QStringList runtimeProtected{
|
||||
"bootstrap", "bootstrap.exe", "client.ini", "config/app_config.json", "config/local_state.json",
|
||||
"config/client_identity.dat", "config/version_policy.dat"
|
||||
};
|
||||
for (const QString& protectedPath : runtimeProtected)
|
||||
protectedPaths.insert(runtimePrefix + "/" + protectedPath);
|
||||
}
|
||||
return protectedPaths.contains(p);
|
||||
}
|
||||
return UpdatePathPolicy::isFullUpdateProtectedPath(
|
||||
path, ConfigHelper::instance().runtimeRelativePath());
|
||||
}
|
||||
|
||||
QString IntegrityHelper::sha256(const QString& filePath) const
|
||||
{
|
||||
@@ -127,16 +137,39 @@ bool IntegrityHelper::verifyInstalledVersion(const QString& appId, const QString
|
||||
.arg(version, cachePath, wrapperError.errorString());
|
||||
return false;
|
||||
}
|
||||
const QJsonObject wrapper = wrapperDoc.object();
|
||||
const QByteArray manifestText = wrapper.value("manifest_text").toString().toUtf8();
|
||||
const QString signature = wrapper.value("manifest").toObject().value("signature").toString();
|
||||
if (manifestText.isEmpty() || signature.isEmpty()) {
|
||||
const QJsonObject wrapper = wrapperDoc.object();
|
||||
QByteArray manifestText = wrapper.value("manifestText").toString().toUtf8();
|
||||
if (manifestText.isEmpty())
|
||||
manifestText = wrapper.value("manifest_text").toString().toUtf8();
|
||||
const QString manifestSha256 = wrapper.value("manifestSha256").toString(
|
||||
wrapper.value("manifest_sha256").toString());
|
||||
const QString signature = wrapper.value("signature").toString(
|
||||
wrapper.value("manifest").toObject().value("signature").toString());
|
||||
const bool signedManifest = wrapper.value("signed").toBool(!signature.isEmpty());
|
||||
if (manifestText.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;
|
||||
if (!manifestSha256.isEmpty()) {
|
||||
const QString actualSha = QString::fromLatin1(
|
||||
QCryptographicHash::hash(manifestText, QCryptographicHash::Sha256).toHex());
|
||||
if (actualSha.compare(manifestSha256, Qt::CaseInsensitive) != 0) {
|
||||
m_error = QCoreApplication::translate("IntegrityHelper",
|
||||
"Local manifest SHA-256 does not match the cached envelope. Stage: installed version verification. Version: %1.\nExpected SHA-256: %2\nActual SHA-256: %3")
|
||||
.arg(version, manifestSha256, actualSha);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (signedManifest && !signature.isEmpty()) {
|
||||
if (!verifySignature(manifestText, signature)) return false;
|
||||
} else if (configFlag(QStringLiteral("require_manifest_signature"))) {
|
||||
m_error = QCoreApplication::translate("IntegrityHelper",
|
||||
"Local manifest cache is unsigned, but require_manifest_signature is enabled. Stage: installed version verification. Version: %1.")
|
||||
.arg(version);
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonParseError manifestError;
|
||||
const QJsonDocument manifestDoc = QJsonDocument::fromJson(manifestText, &manifestError);
|
||||
@@ -147,20 +180,23 @@ bool IntegrityHelper::verifyInstalledVersion(const QString& appId, const QString
|
||||
return false;
|
||||
}
|
||||
const QJsonObject manifest = manifestDoc.object();
|
||||
if (manifest.value("app_id").toString() != appId
|
||||
const QString manifestProduct = manifest.value("productCode").toString(
|
||||
manifest.value("app_id").toString());
|
||||
if (manifestProduct != appId
|
||||
|| manifest.value("channel").toString() != channel
|
||||
|| manifest.value("version").toString() != version) {
|
||||
m_error = QCoreApplication::translate("IntegrityHelper",
|
||||
"Local signed manifest identity does not match this application. Stage: installed version verification. Expected app/channel/version: %1 / %2 / %3. Manifest app/channel/version: %4 / %5 / %6.")
|
||||
"Local signed manifest identity does not match this application. Stage: installed version verification. Expected product/channel/version: %1 / %2 / %3. Manifest product/channel/version: %4 / %5 / %6.")
|
||||
.arg(appId, channel, version,
|
||||
manifest.value("app_id").toString(),
|
||||
manifestProduct,
|
||||
manifest.value("channel").toString(),
|
||||
manifest.value("version").toString());
|
||||
return false;
|
||||
}
|
||||
|
||||
QSet<QString> declaredExecutables;
|
||||
for (const QJsonValue& value : manifest.value("files").toArray()) {
|
||||
QSet<QString> declaredExecutables;
|
||||
QSet<QString> optionalComponentDirs;
|
||||
for (const QJsonValue& value : manifest.value("files").toArray()) {
|
||||
const QJsonObject item = value.toObject();
|
||||
const QString path = QDir::fromNativeSeparators(item.value("path").toString());
|
||||
if (!safeRelativePath(path)) {
|
||||
@@ -169,6 +205,14 @@ bool IntegrityHelper::verifyInstalledVersion(const QString& appId, const QString
|
||||
.arg(version, path);
|
||||
return false;
|
||||
}
|
||||
if (UpdatePathPolicy::isExecutableOrLibrary(path))
|
||||
declaredExecutables.insert(path.toCaseFolded());
|
||||
if (!manifestFileRequired(item)) {
|
||||
const QString dir = QDir::fromNativeSeparators(QFileInfo(path).path());
|
||||
if (!dir.isEmpty() && dir != QStringLiteral("."))
|
||||
optionalComponentDirs.insert((dir + QStringLiteral("/")).toCaseFolded());
|
||||
continue;
|
||||
}
|
||||
if (runtimeProtectedPath(path)) continue;
|
||||
const QString fullPath = QDir(m_installDir).filePath(path);
|
||||
if (!QFile::exists(fullPath)) {
|
||||
@@ -177,6 +221,17 @@ bool IntegrityHelper::verifyInstalledVersion(const QString& appId, const QString
|
||||
.arg(version, path, fullPath);
|
||||
return false;
|
||||
}
|
||||
const qint64 expectedSize = item.contains("sizeBytes")
|
||||
? item.value("sizeBytes").toVariant().toLongLong()
|
||||
: item.value("size").toVariant().toLongLong();
|
||||
if ((item.contains("sizeBytes") || item.contains("size"))
|
||||
&& QFileInfo(fullPath).size() != expectedSize) {
|
||||
m_error = QCoreApplication::translate("IntegrityHelper",
|
||||
"Installed file size does not match the local manifest. Stage: installed version verification. Version: %1. Manifest path: %2. Local path: %3.\nExpected size: %4 bytes\nActual size: %5 bytes")
|
||||
.arg(version, path, fullPath,
|
||||
QString::number(expectedSize), QString::number(QFileInfo(fullPath).size()));
|
||||
return false;
|
||||
}
|
||||
const QString expected = item.value("sha256").toString();
|
||||
const QString actual = sha256(fullPath);
|
||||
if (actual.isEmpty() || actual.compare(expected, Qt::CaseInsensitive) != 0) {
|
||||
@@ -186,9 +241,7 @@ bool IntegrityHelper::verifyInstalledVersion(const QString& appId, const QString
|
||||
actual.isEmpty() ? QCoreApplication::translate("IntegrityHelper", "<cannot read file>") : actual);
|
||||
return false;
|
||||
}
|
||||
const QString suffix = QFileInfo(path).suffix().toCaseFolded();
|
||||
if (suffix == "exe" || suffix == "dll") declaredExecutables.insert(path.toCaseFolded());
|
||||
}
|
||||
}
|
||||
|
||||
QDir root(m_installDir);
|
||||
QDirIterator it(m_installDir, QDir::Files, QDirIterator::Subdirectories);
|
||||
@@ -202,10 +255,18 @@ bool IntegrityHelper::verifyInstalledVersion(const QString& appId, const QString
|
||||
const bool runtimeWorkDir = !runtimePrefix.isEmpty()
|
||||
&& (folded.startsWith(runtimePrefix + "/update/")
|
||||
|| folded.startsWith(runtimePrefix + "/update_temp/"));
|
||||
if (folded.startsWith("update/") || folded.startsWith("update_temp/")
|
||||
|| runtimeWorkDir || runtimeProtectedPath(relative)) continue;
|
||||
const QString suffix = QFileInfo(relative).suffix().toCaseFolded();
|
||||
if ((suffix == "exe" || suffix == "dll") && !declaredExecutables.contains(folded)) {
|
||||
if (folded.startsWith("update/") || folded.startsWith("update_temp/")
|
||||
|| runtimeWorkDir || runtimeProtectedPath(relative)) continue;
|
||||
bool optionalComponentFile = false;
|
||||
for (const QString& prefix : optionalComponentDirs) {
|
||||
if (folded.startsWith(prefix)) {
|
||||
optionalComponentFile = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (optionalComponentFile)
|
||||
continue;
|
||||
if (UpdatePathPolicy::isExecutableOrLibrary(relative) && !declaredExecutables.contains(folded)) {
|
||||
m_error = QCoreApplication::translate("IntegrityHelper",
|
||||
"An executable or DLL exists locally but is not declared in the signed manifest. Stage: installed version verification. Version: %1. Extra file: %2. Remove unexpected executable/plugin files or publish a new version that declares them.")
|
||||
.arg(version, relative);
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
#include "UpdatePathPolicy.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QSet>
|
||||
#include <QStringList>
|
||||
|
||||
namespace {
|
||||
|
||||
bool exactOrRuntimeMatch(const QString& folded, const QString& runtimePrefix,
|
||||
const QSet<QString>& exactPaths)
|
||||
{
|
||||
if (exactPaths.contains(folded))
|
||||
return true;
|
||||
if (runtimePrefix.isEmpty() || !folded.startsWith(runtimePrefix + QStringLiteral("/")))
|
||||
return false;
|
||||
return exactPaths.contains(folded.mid(runtimePrefix.size() + 1));
|
||||
}
|
||||
|
||||
bool prefixOrRuntimePrefixMatch(const QString& folded, const QString& runtimePrefix,
|
||||
const QString& prefix)
|
||||
{
|
||||
if (folded.startsWith(prefix))
|
||||
return true;
|
||||
if (runtimePrefix.isEmpty())
|
||||
return false;
|
||||
return folded.startsWith(runtimePrefix + QStringLiteral("/") + prefix);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace UpdatePathPolicy {
|
||||
|
||||
QString normalizeRelativePath(const QString& path)
|
||||
{
|
||||
QString normalized = QDir::cleanPath(QDir::fromNativeSeparators(path.trimmed()));
|
||||
if (normalized == QStringLiteral("."))
|
||||
return QString();
|
||||
while (normalized.startsWith(QStringLiteral("./")))
|
||||
normalized = normalized.mid(2);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
bool isSafeRelativePath(const QString& path)
|
||||
{
|
||||
const QString clean = normalizeRelativePath(path);
|
||||
return !clean.isEmpty() && !QDir::isAbsolutePath(clean) && clean != QStringLiteral("..")
|
||||
&& !clean.startsWith(QStringLiteral("../")) && !clean.contains(QLatin1Char(':'));
|
||||
}
|
||||
|
||||
bool isUpdaterRuntimeProtectedPath(const QString& path, const QString& runtimeRelativePath)
|
||||
{
|
||||
const QString folded = normalizeRelativePath(path).toCaseFolded();
|
||||
const QString runtimePrefix = normalizeRelativePath(runtimeRelativePath).toCaseFolded();
|
||||
const QSet<QString> exactPaths{
|
||||
QStringLiteral("bootstrap"),
|
||||
QStringLiteral("bootstrap.exe"),
|
||||
QStringLiteral("launcher"),
|
||||
QStringLiteral("launcher.exe"),
|
||||
QStringLiteral("updater"),
|
||||
QStringLiteral("updater.exe"),
|
||||
QStringLiteral("client.ini"),
|
||||
QStringLiteral("config/app_config.json"),
|
||||
QStringLiteral("config/local_state.json"),
|
||||
QStringLiteral("config/client_identity.dat"),
|
||||
QStringLiteral("config/version_policy.dat")
|
||||
};
|
||||
if (exactOrRuntimeMatch(folded, runtimePrefix, exactPaths))
|
||||
return true;
|
||||
return prefixOrRuntimePrefixMatch(folded, runtimePrefix, QStringLiteral("update/"))
|
||||
|| prefixOrRuntimePrefixMatch(folded, runtimePrefix, QStringLiteral("update_temp/"));
|
||||
}
|
||||
|
||||
bool isIFWInstallerManagedPath(const QString& path)
|
||||
{
|
||||
const QString folded = normalizeRelativePath(path).toCaseFolded();
|
||||
if (folded.isEmpty())
|
||||
return false;
|
||||
|
||||
const bool rootFile = !folded.contains(QLatin1Char('/'));
|
||||
if (rootFile && (folded == QStringLiteral("maintenancetool")
|
||||
|| folded == QStringLiteral("maintenancetool.exe")
|
||||
|| folded.startsWith(QStringLiteral("maintenancetool.")))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const QSet<QString> exactPaths{
|
||||
QStringLiteral("components.xml"),
|
||||
QStringLiteral("components.xml.new"),
|
||||
QStringLiteral("components.xml.old"),
|
||||
QStringLiteral("installation.xml"),
|
||||
QStringLiteral("installation.dat"),
|
||||
QStringLiteral("installer.dat"),
|
||||
QStringLiteral("installer.ini"),
|
||||
QStringLiteral("network.xml"),
|
||||
QStringLiteral("repositories.xml"),
|
||||
QStringLiteral("repositories.cfg"),
|
||||
QStringLiteral("repository.xml")
|
||||
};
|
||||
if (exactPaths.contains(folded))
|
||||
return true;
|
||||
|
||||
const QStringList prefixes{
|
||||
QStringLiteral("installerresources/"),
|
||||
QStringLiteral("installationinformation/"),
|
||||
QStringLiteral("licenses/")
|
||||
};
|
||||
for (const QString& prefix : prefixes) {
|
||||
if (folded.startsWith(prefix))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isFullUpdateProtectedPath(const QString& path, const QString& runtimeRelativePath)
|
||||
{
|
||||
return isUpdaterRuntimeProtectedPath(path, runtimeRelativePath)
|
||||
|| isIFWInstallerManagedPath(path);
|
||||
}
|
||||
|
||||
bool isExecutableOrLibrary(const QString& path)
|
||||
{
|
||||
const QString suffix = QFileInfo(path).suffix().toCaseFolded();
|
||||
return suffix == QStringLiteral("exe") || suffix == QStringLiteral("dll");
|
||||
}
|
||||
|
||||
} // namespace UpdatePathPolicy
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
|
||||
namespace UpdatePathPolicy {
|
||||
|
||||
QString normalizeRelativePath(const QString& path);
|
||||
bool isSafeRelativePath(const QString& path);
|
||||
bool isUpdaterRuntimeProtectedPath(const QString& path, const QString& runtimeRelativePath);
|
||||
bool isIFWInstallerManagedPath(const QString& path);
|
||||
bool isFullUpdateProtectedPath(const QString& path, const QString& runtimeRelativePath);
|
||||
bool isExecutableOrLibrary(const QString& path);
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user