1.0.0
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
project(Common LANGUAGES C CXX)
|
||||
find_package(Qt5 REQUIRED COMPONENTS Core Network Widgets)
|
||||
|
||||
set(SRC
|
||||
HttpHelper.h
|
||||
HttpHelper.cpp
|
||||
FileHelper.h
|
||||
FileHelper.cpp
|
||||
ConfigHelper.h
|
||||
ConfigHelper.cpp
|
||||
PolicyHelper.h
|
||||
PolicyHelper.cpp
|
||||
LocalStateHelper.h
|
||||
LocalStateHelper.cpp
|
||||
)
|
||||
add_library(Common STATIC ${SRC})
|
||||
|
||||
# Common编译自身需要OpenSSL头文件
|
||||
target_include_directories(Common
|
||||
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
PRIVATE ${OPENSSL_INC}
|
||||
)
|
||||
|
||||
# 链接库、库目录通过INTERFACE传递给所有依赖Common的exe
|
||||
target_link_directories(Common INTERFACE
|
||||
$<$<CONFIG:Debug>:${OPENSSL_LIB_DEBUG}>
|
||||
$<$<CONFIG:Release>:${OPENSSL_LIB_RELEASE}>
|
||||
)
|
||||
target_link_libraries(Common
|
||||
PRIVATE Qt5::Core Qt5::Network Qt5::Widgets
|
||||
INTERFACE libssl.lib libcrypto.lib
|
||||
)
|
||||
@@ -0,0 +1,116 @@
|
||||
#include "ConfigHelper.h"
|
||||
#include <QApplication>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QSaveFile>
|
||||
#include <QSettings>
|
||||
#include <QDebug>
|
||||
|
||||
ConfigHelper& ConfigHelper::instance()
|
||||
{
|
||||
static ConfigHelper obj;
|
||||
return obj;
|
||||
}
|
||||
|
||||
ConfigHelper::ConfigHelper()
|
||||
{
|
||||
m_configPath = QApplication::applicationDirPath() + "/config/app_config.json";
|
||||
migrateLegacyIniIfNeeded();
|
||||
qDebug() << "Loading app config path:" << m_configPath;
|
||||
qDebug() << "File exists?" << QFile::exists(m_configPath);
|
||||
}
|
||||
|
||||
QString ConfigHelper::configPath() const
|
||||
{
|
||||
return m_configPath;
|
||||
}
|
||||
|
||||
QString ConfigHelper::getValue(const QString& section, const QString& key) const
|
||||
{
|
||||
Q_UNUSED(section);
|
||||
QFile file(m_configPath);
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
return QString();
|
||||
|
||||
QJsonParseError error;
|
||||
const QJsonDocument document = QJsonDocument::fromJson(file.readAll(), &error);
|
||||
if (error.error != QJsonParseError::NoError || !document.isObject())
|
||||
return QString();
|
||||
|
||||
return document.object().value(key).toVariant().toString();
|
||||
}
|
||||
|
||||
bool ConfigHelper::setValue(const QString& section, const QString& key, const QString& value)
|
||||
{
|
||||
Q_UNUSED(section);
|
||||
QFile input(m_configPath);
|
||||
QJsonObject config;
|
||||
if (input.exists())
|
||||
{
|
||||
if (!input.open(QIODevice::ReadOnly))
|
||||
return false;
|
||||
QJsonParseError parseError;
|
||||
const QByteArray raw = input.readAll();
|
||||
input.close();
|
||||
const QJsonDocument document = QJsonDocument::fromJson(raw, &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !document.isObject())
|
||||
return false;
|
||||
config = document.object();
|
||||
}
|
||||
|
||||
config.insert(key, value);
|
||||
QDir().mkpath(QFileInfo(m_configPath).path());
|
||||
QSaveFile output(m_configPath);
|
||||
if (!output.open(QIODevice::WriteOnly))
|
||||
return false;
|
||||
const QByteArray payload = QJsonDocument(config).toJson(QJsonDocument::Indented);
|
||||
if (output.write(payload) != payload.size())
|
||||
{
|
||||
output.cancelWriting();
|
||||
return false;
|
||||
}
|
||||
return output.commit();
|
||||
}
|
||||
|
||||
bool ConfigHelper::migrateLegacyIniIfNeeded()
|
||||
{
|
||||
if (QFile::exists(m_configPath))
|
||||
return true;
|
||||
|
||||
const QString legacyPath = QApplication::applicationDirPath() + "/client.ini";
|
||||
if (!QFile::exists(legacyPath))
|
||||
return false;
|
||||
|
||||
QSettings ini(legacyPath, QSettings::IniFormat);
|
||||
QJsonObject config;
|
||||
const auto copyText = [&](const QString& section, const QString& key, const QString& fallback = QString()) {
|
||||
const QString value = ini.value(section + "/" + key, fallback).toString();
|
||||
config.insert(key, value);
|
||||
};
|
||||
|
||||
copyText("App", "app_id");
|
||||
copyText("App", "app_name", "Marsco Demo App");
|
||||
copyText("App", "channel", "stable");
|
||||
copyText("App", "current_version", "1.0.0");
|
||||
copyText("App", "launch_token");
|
||||
copyText("Server", "api_base_url");
|
||||
copyText("Server", "client_token");
|
||||
copyText("Update", "request_timeout_ms", "5000");
|
||||
copyText("Update", "temp_folder", "update_temp");
|
||||
copyText("Update", "device_id");
|
||||
config.insert("platform", "windows");
|
||||
config.insert("arch", "x64");
|
||||
|
||||
QDir().mkpath(QFileInfo(m_configPath).path());
|
||||
QSaveFile output(m_configPath);
|
||||
if (!output.open(QIODevice::WriteOnly))
|
||||
return false;
|
||||
output.write(QJsonDocument(config).toJson(QJsonDocument::Indented));
|
||||
const bool saved = output.commit();
|
||||
if (saved)
|
||||
qDebug() << "Migrated legacy client.ini to" << m_configPath;
|
||||
return saved;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
#include <QString>
|
||||
|
||||
class ConfigHelper
|
||||
{
|
||||
public:
|
||||
static ConfigHelper& instance();
|
||||
QString getValue(const QString& section, const QString& key) const;
|
||||
bool setValue(const QString& section, const QString& key, const QString& value);
|
||||
QString configPath() const;
|
||||
|
||||
private:
|
||||
ConfigHelper();
|
||||
bool migrateLegacyIniIfNeeded();
|
||||
QString m_configPath;
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
#include "FileHelper.h"
|
||||
#include <QDebug>
|
||||
|
||||
bool FileHelper::createDir(const QString &path)
|
||||
{
|
||||
QDir dir(path);
|
||||
if (!dir.exists())
|
||||
return dir.mkpath(".");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FileHelper::copyFileOverwrite(const QString &src, const QString &dst)
|
||||
{
|
||||
if (QFile::exists(dst))
|
||||
{
|
||||
if (!QFile::remove(dst))
|
||||
{
|
||||
qDebug() << "无法删除旧文件:" << dst;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return QFile::copy(src, dst);
|
||||
}
|
||||
|
||||
bool FileHelper::isProcessRunning(const QString &exeName)
|
||||
{
|
||||
QProcess process;
|
||||
process.start("tasklist");
|
||||
process.waitForFinished();
|
||||
QString output = process.readAllStandardOutput();
|
||||
return output.contains(exeName, Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
bool FileHelper::killProcess(const QString &exeName)
|
||||
{
|
||||
if (!isProcessRunning(exeName))
|
||||
return true;
|
||||
QProcess process;
|
||||
process.start("taskkill /f /im " + exeName);
|
||||
process.waitForFinished(1000);
|
||||
return !isProcessRunning(exeName);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
#include <QString>
|
||||
#include <QFile>
|
||||
#include <QDir>
|
||||
#include <QProcess>
|
||||
|
||||
class FileHelper
|
||||
{
|
||||
public:
|
||||
static bool createDir(const QString& path);
|
||||
static bool copyFileOverwrite(const QString& src, const QString& dst);
|
||||
static bool isProcessRunning(const QString& exeName);
|
||||
static bool killProcess(const QString& exeName);
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
#include "HttpHelper.h"
|
||||
#include <QNetworkProxy>
|
||||
#include "ConfigHelper.h"
|
||||
|
||||
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());
|
||||
|
||||
QByteArray data = QJsonDocument(jsonBody).toJson(QJsonDocument::Compact);
|
||||
qDebug() << "=== POST Request ===";
|
||||
qDebug() << "Url:" << url;
|
||||
qDebug() << "Body:" << data;
|
||||
|
||||
QNetworkReply* reply = manager->post(req, data);
|
||||
QEventLoop loop;
|
||||
|
||||
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
|
||||
int retCode = 0;
|
||||
QJsonObject retObj;
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError)
|
||||
{
|
||||
qDebug() << "Network error code:" << reply->error();
|
||||
qDebug() << "Error detail:" << reply->errorString();
|
||||
}
|
||||
else
|
||||
{
|
||||
retCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
QByteArray respData = reply->readAll();
|
||||
qDebug() << "Server raw response:" << respData;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(respData);
|
||||
retObj = doc.object();
|
||||
}
|
||||
|
||||
callback(retCode, retObj);
|
||||
|
||||
reply->deleteLater();
|
||||
manager->deleteLater();
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
#include <QObject>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonDocument>
|
||||
#include <QEventLoop>
|
||||
#include <QDebug>
|
||||
|
||||
class HttpHelper
|
||||
{
|
||||
public:
|
||||
// 每次调用独立创建manager,不做成成员变量
|
||||
static void postRequest(const QString& url, const QJsonObject& jsonBody,
|
||||
std::function<void(int code, const QJsonObject& resp)> callback);
|
||||
};
|
||||
@@ -0,0 +1,147 @@
|
||||
#include "LocalStateHelper.h"
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonDocument>
|
||||
#include <QDir>
|
||||
|
||||
LocalStateHelper::LocalStateHelper(const QString& baseDir)
|
||||
: m_baseDir(baseDir)
|
||||
, m_filePath(baseDir + "/config/local_state.json")
|
||||
{
|
||||
}
|
||||
|
||||
bool LocalStateHelper::loadState(const QString& relativePath)
|
||||
{
|
||||
m_filePath = m_baseDir + "/" + relativePath;
|
||||
QFile file(m_filePath);
|
||||
if (!file.exists())
|
||||
{
|
||||
QDir dir(QFileInfo(m_filePath).path());
|
||||
if (!dir.exists() && !dir.mkpath("."))
|
||||
{
|
||||
m_error = QString("Cannot create state directory: %1").arg(dir.path());
|
||||
return false;
|
||||
}
|
||||
|
||||
m_state = QJsonObject{
|
||||
{"max_policy_seq", 0},
|
||||
{"last_success_run_at", QString()},
|
||||
{"last_online_verified_at", QString()},
|
||||
{"last_success_version", QString()}
|
||||
};
|
||||
m_loaded = true;
|
||||
if (!saveState())
|
||||
{
|
||||
m_error = QString("Cannot write new state file: %1").arg(m_filePath);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
{
|
||||
m_error = QString("Cannot open state file: %1").arg(m_filePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
QByteArray raw = file.readAll();
|
||||
file.close();
|
||||
|
||||
QJsonParseError parseError;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(raw, &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !doc.isObject())
|
||||
{
|
||||
m_error = QString("Invalid state JSON: %1").arg(parseError.errorString());
|
||||
return false;
|
||||
}
|
||||
|
||||
m_state = doc.object();
|
||||
m_loaded = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocalStateHelper::saveState() const
|
||||
{
|
||||
if (!m_loaded)
|
||||
return false;
|
||||
|
||||
QFile file(m_filePath);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonDocument doc(m_state);
|
||||
file.write(doc.toJson(QJsonDocument::Indented));
|
||||
file.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocalStateHelper::isLoaded() const
|
||||
{
|
||||
return m_loaded;
|
||||
}
|
||||
|
||||
QString LocalStateHelper::errorString() const
|
||||
{
|
||||
return m_error;
|
||||
}
|
||||
|
||||
qint64 LocalStateHelper::maxPolicySeq() const
|
||||
{
|
||||
return m_state.value("max_policy_seq").toVariant().toLongLong();
|
||||
}
|
||||
|
||||
QDateTime LocalStateHelper::lastSuccessRunAt() const
|
||||
{
|
||||
return QDateTime::fromString(m_state.value("last_success_run_at").toString(), Qt::ISODate);
|
||||
}
|
||||
|
||||
QDateTime LocalStateHelper::lastOnlineVerifiedAt() const
|
||||
{
|
||||
return QDateTime::fromString(m_state.value("last_online_verified_at").toString(), Qt::ISODate);
|
||||
}
|
||||
|
||||
QString LocalStateHelper::lastSuccessVersion() const
|
||||
{
|
||||
return m_state.value("last_success_version").toString();
|
||||
}
|
||||
|
||||
bool LocalStateHelper::isPolicySeqRolledBack(qint64 policySeq) const
|
||||
{
|
||||
if (!m_loaded)
|
||||
return false;
|
||||
return policySeq < maxPolicySeq();
|
||||
}
|
||||
|
||||
bool LocalStateHelper::isSystemTimeRewound() const
|
||||
{
|
||||
if (!m_loaded)
|
||||
return false;
|
||||
const QDateTime now = QDateTime::currentDateTimeUtc();
|
||||
const QDateTime lastRun = lastSuccessRunAt();
|
||||
const QDateTime lastOnline = lastOnlineVerifiedAt();
|
||||
return (lastRun.isValid() && now < lastRun)
|
||||
|| (lastOnline.isValid() && now < lastOnline);
|
||||
}
|
||||
|
||||
void LocalStateHelper::updateAfterSuccessfulRun(const QString& currentVersion, qint64 policySeq)
|
||||
{
|
||||
if (!m_loaded)
|
||||
m_loaded = true;
|
||||
|
||||
m_state["last_success_run_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
|
||||
m_state["last_success_version"] = currentVersion;
|
||||
if (policySeq > maxPolicySeq())
|
||||
m_state["max_policy_seq"] = policySeq;
|
||||
}
|
||||
|
||||
void LocalStateHelper::updateOnlineVerified(qint64 policySeq)
|
||||
{
|
||||
if (!m_loaded)
|
||||
m_loaded = true;
|
||||
|
||||
m_state["last_online_verified_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
|
||||
if (policySeq > maxPolicySeq())
|
||||
m_state["max_policy_seq"] = policySeq;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
#include <QDateTime>
|
||||
|
||||
class LocalStateHelper
|
||||
{
|
||||
public:
|
||||
explicit LocalStateHelper(const QString& baseDir);
|
||||
|
||||
bool loadState(const QString& relativePath = "config/local_state.json");
|
||||
bool saveState() const;
|
||||
bool isLoaded() const;
|
||||
QString errorString() const;
|
||||
|
||||
qint64 maxPolicySeq() const;
|
||||
QDateTime lastSuccessRunAt() const;
|
||||
QDateTime lastOnlineVerifiedAt() const;
|
||||
QString lastSuccessVersion() const;
|
||||
|
||||
bool isPolicySeqRolledBack(qint64 policySeq) const;
|
||||
bool isSystemTimeRewound() const;
|
||||
|
||||
void updateAfterSuccessfulRun(const QString& currentVersion, qint64 policySeq);
|
||||
void updateOnlineVerified(qint64 policySeq);
|
||||
|
||||
private:
|
||||
QString m_baseDir;
|
||||
QString m_filePath;
|
||||
QString m_error;
|
||||
QJsonObject m_state;
|
||||
bool m_loaded = false;
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
#include "PolicyHelper.h"
|
||||
#include <QApplication>
|
||||
#include <QDateTime>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QSaveFile>
|
||||
#include <algorithm>
|
||||
#ifdef HAVE_OPENSSL
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/pem.h>
|
||||
#endif
|
||||
|
||||
PolicyHelper::PolicyHelper(const QString& baseDir) : m_baseDir(baseDir) {}
|
||||
|
||||
bool PolicyHelper::loadPolicy(const QString& relativePath)
|
||||
{
|
||||
QFile file(m_baseDir + "/" + relativePath);
|
||||
if (!file.open(QIODevice::ReadOnly)) { m_error = "Cannot open policy file"; return false; }
|
||||
QJsonParseError error;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error);
|
||||
if (error.error != QJsonParseError::NoError || !doc.isObject()) {
|
||||
m_error = "Invalid policy JSON: " + error.errorString(); return false;
|
||||
}
|
||||
return loadPolicyObject(doc.object());
|
||||
}
|
||||
|
||||
bool PolicyHelper::loadPolicyObject(const QJsonObject& policy, const QString& signedText)
|
||||
{
|
||||
m_policy = policy; m_signedText = signedText; m_loaded = true; m_error.clear();
|
||||
return isValid();
|
||||
}
|
||||
|
||||
bool PolicyHelper::savePolicy(const QString& relativePath) const
|
||||
{
|
||||
QSaveFile file(m_baseDir + "/" + relativePath);
|
||||
if (!file.open(QIODevice::WriteOnly)) return false;
|
||||
const QByteArray bytes = QJsonDocument(m_policy).toJson(QJsonDocument::Indented);
|
||||
return file.write(bytes) == bytes.size() && file.commit();
|
||||
}
|
||||
|
||||
QByteArray PolicyHelper::canonicalPolicyBytes(const QJsonObject& policy) const
|
||||
{
|
||||
QJsonObject copy = policy; copy.remove("signature");
|
||||
auto sortObject = [&](auto&& self, const QJsonObject& obj) -> QJsonObject {
|
||||
QStringList keys = obj.keys(); std::sort(keys.begin(), keys.end());
|
||||
QJsonObject sorted;
|
||||
for (const QString& key : keys) {
|
||||
QJsonValue value = obj.value(key);
|
||||
if (value.isObject()) value = self(self, value.toObject());
|
||||
else if (value.isArray()) {
|
||||
QJsonArray array;
|
||||
for (QJsonValue item : value.toArray()) {
|
||||
if (item.isObject()) item = self(self, item.toObject());
|
||||
array.append(item);
|
||||
}
|
||||
value = array;
|
||||
}
|
||||
sorted.insert(key, value);
|
||||
}
|
||||
return sorted;
|
||||
};
|
||||
return QJsonDocument(sortObject(sortObject, copy)).toJson(QJsonDocument::Compact);
|
||||
}
|
||||
|
||||
bool PolicyHelper::verifySignature(const QByteArray& payload, const QString& signatureBase64) const
|
||||
{
|
||||
#ifndef HAVE_OPENSSL
|
||||
Q_UNUSED(payload); Q_UNUSED(signatureBase64); m_error = "OpenSSL unavailable"; return false;
|
||||
#else
|
||||
QString keyPath = m_baseDir + "/config/manifest_public_key.pem";
|
||||
QFile keyFile(keyPath);
|
||||
if (!keyFile.open(QIODevice::ReadOnly)) { m_error = "Cannot open policy public key"; return false; }
|
||||
const QByteArray keyData = keyFile.readAll();
|
||||
BIO* bio = BIO_new_mem_buf(keyData.constData(), keyData.size());
|
||||
EVP_PKEY* key = bio ? PEM_read_bio_PUBKEY(bio, nullptr, nullptr, nullptr) : nullptr;
|
||||
if (bio) BIO_free(bio);
|
||||
if (!key) { m_error = "Invalid policy public key"; return false; }
|
||||
EVP_MD_CTX* ctx = EVP_MD_CTX_new();
|
||||
const QByteArray signature = QByteArray::fromBase64(signatureBase64.toUtf8());
|
||||
bool ok = ctx && EVP_DigestVerifyInit(ctx, nullptr, EVP_sha256(), nullptr, key) == 1
|
||||
&& EVP_DigestVerifyUpdate(ctx, payload.constData(), payload.size()) == 1
|
||||
&& EVP_DigestVerifyFinal(ctx, reinterpret_cast<const unsigned char*>(signature.constData()), signature.size()) == 1;
|
||||
if (ctx) EVP_MD_CTX_free(ctx); EVP_PKEY_free(key);
|
||||
if (!ok) m_error = "Invalid RSA policy signature";
|
||||
return ok;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool PolicyHelper::isValid() const
|
||||
{
|
||||
if (!m_loaded) return false;
|
||||
const QStringList required{"app_id","channel","current_version","policy_seq","allow_run",
|
||||
"force_update","allow_rollback","offline_allowed","valid_until","signature_alg","key_id","signature"};
|
||||
for (const QString& key : required) if (!m_policy.contains(key)) { m_error = "Missing policy field: " + key; return false; }
|
||||
if (m_policy.value("signature_alg").toString() != "RSA-2048-SHA256") return false;
|
||||
const QByteArray payload = m_signedText.isEmpty() ? canonicalPolicyBytes(m_policy) : m_signedText.toUtf8();
|
||||
return verifySignature(payload, m_policy.value("signature").toString());
|
||||
}
|
||||
QString PolicyHelper::errorString() const { return m_error; }
|
||||
bool PolicyHelper::isVersionAllowed(const QString& version) const {
|
||||
if (!isValid()) return false;
|
||||
for (const QJsonValue& v : m_policy.value("disabled_versions").toArray()) if (v.toString() == version) return false;
|
||||
return true;
|
||||
}
|
||||
bool PolicyHelper::allowRun() const { return isValid() && m_policy.value("allow_run").toBool(); }
|
||||
bool PolicyHelper::forceUpdate() const { return isValid() && m_policy.value("force_update").toBool(); }
|
||||
bool PolicyHelper::isOfflineAllowed() const { return isValid() && m_policy.value("offline_allowed").toBool(); }
|
||||
bool PolicyHelper::isExpired() const {
|
||||
if (!isValid()) return true;
|
||||
const QDateTime expiry = QDateTime::fromString(m_policy.value("valid_until").toString(), Qt::ISODate);
|
||||
return !expiry.isValid() || QDateTime::currentDateTimeUtc() > expiry;
|
||||
}
|
||||
qint64 PolicyHelper::policySeq() const { return isValid() ? m_policy.value("policy_seq").toVariant().toLongLong() : 0; }
|
||||
QString PolicyHelper::message() const { return m_policy.value("message").toString(); }
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
|
||||
class PolicyHelper
|
||||
{
|
||||
public:
|
||||
explicit PolicyHelper(const QString& baseDir);
|
||||
|
||||
bool loadPolicy(const QString& relativePath = "config/version_policy.dat");
|
||||
bool loadPolicyObject(const QJsonObject& policy, const QString& signedText = QString());
|
||||
bool savePolicy(const QString& relativePath = "config/version_policy.dat") const;
|
||||
bool isValid() const;
|
||||
QString errorString() const;
|
||||
bool isVersionAllowed(const QString& currentVersion) const;
|
||||
bool allowRun() const;
|
||||
bool forceUpdate() const;
|
||||
bool isOfflineAllowed() const;
|
||||
bool isExpired() const;
|
||||
qint64 policySeq() const;
|
||||
QString message() const;
|
||||
|
||||
private:
|
||||
QByteArray canonicalPolicyBytes(const QJsonObject& obj) const;
|
||||
bool verifySignature(const QByteArray& payload, const QString& signatureBase64) const;
|
||||
|
||||
QString m_baseDir;
|
||||
QJsonObject m_policy;
|
||||
QString m_signedText;
|
||||
mutable QString m_error;
|
||||
bool m_loaded = false;
|
||||
};
|
||||
Reference in New Issue
Block a user