chore: prepare repository for submodule split
This commit is contained in:
@@ -28,6 +28,40 @@ QString ConfigHelper::configPath() const
|
||||
return m_configPath;
|
||||
}
|
||||
|
||||
QString ConfigHelper::installRoot() const
|
||||
{
|
||||
QString relativeRoot = getValue("Runtime", "install_root").trimmed();
|
||||
if (relativeRoot.isEmpty())
|
||||
relativeRoot = ".";
|
||||
return QDir::cleanPath(QDir(runtimeRoot()).filePath(relativeRoot));
|
||||
}
|
||||
|
||||
QString ConfigHelper::runtimeRoot() const
|
||||
{
|
||||
return QDir::cleanPath(QApplication::applicationDirPath());
|
||||
}
|
||||
|
||||
QString ConfigHelper::updateRoot() const
|
||||
{
|
||||
return QDir(runtimeRoot()).filePath("update");
|
||||
}
|
||||
|
||||
QString ConfigHelper::runtimeRelativePath() const
|
||||
{
|
||||
QString relative = QDir::fromNativeSeparators(QDir(installRoot()).relativeFilePath(runtimeRoot()));
|
||||
relative = QDir::cleanPath(relative);
|
||||
if (relative == ".")
|
||||
return QString();
|
||||
while (relative.startsWith("./"))
|
||||
relative = relative.mid(2);
|
||||
return relative.trimmed();
|
||||
}
|
||||
|
||||
QString ConfigHelper::lastError() const
|
||||
{
|
||||
return m_error;
|
||||
}
|
||||
|
||||
QString ConfigHelper::getValue(const QString& section, const QString& key) const
|
||||
{
|
||||
Q_UNUSED(section);
|
||||
@@ -46,33 +80,56 @@ QString ConfigHelper::getValue(const QString& section, const QString& key) const
|
||||
bool ConfigHelper::setValue(const QString& section, const QString& key, const QString& value)
|
||||
{
|
||||
Q_UNUSED(section);
|
||||
m_error.clear();
|
||||
|
||||
QFile input(m_configPath);
|
||||
QJsonObject config;
|
||||
if (input.exists())
|
||||
{
|
||||
if (!input.open(QIODevice::ReadOnly))
|
||||
{
|
||||
m_error = QString("Cannot open config for reading: %1").arg(input.errorString());
|
||||
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())
|
||||
{
|
||||
m_error = QString("Invalid config JSON: %1").arg(parseError.errorString());
|
||||
return false;
|
||||
}
|
||||
config = document.object();
|
||||
}
|
||||
|
||||
config.insert(key, value);
|
||||
QDir().mkpath(QFileInfo(m_configPath).path());
|
||||
QDir dir(QFileInfo(m_configPath).path());
|
||||
if (!dir.exists() && !dir.mkpath("."))
|
||||
{
|
||||
m_error = QString("Cannot create config directory: %1").arg(dir.path());
|
||||
return false;
|
||||
}
|
||||
|
||||
QSaveFile output(m_configPath);
|
||||
if (!output.open(QIODevice::WriteOnly))
|
||||
{
|
||||
m_error = QString("Cannot open config for writing: %1").arg(output.errorString());
|
||||
return false;
|
||||
}
|
||||
const QByteArray payload = QJsonDocument(config).toJson(QJsonDocument::Indented);
|
||||
if (output.write(payload) != payload.size())
|
||||
{
|
||||
m_error = QString("Cannot write full config file: %1").arg(output.errorString());
|
||||
output.cancelWriting();
|
||||
return false;
|
||||
}
|
||||
return output.commit();
|
||||
if (!output.commit())
|
||||
{
|
||||
m_error = QString("Cannot commit config file: %1").arg(output.errorString());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConfigHelper::migrateLegacyIniIfNeeded()
|
||||
@@ -103,6 +160,7 @@ bool ConfigHelper::migrateLegacyIniIfNeeded()
|
||||
copyText("Update", "request_timeout_ms", "5000");
|
||||
copyText("Update", "temp_folder", "update_temp");
|
||||
copyText("Update", "device_id");
|
||||
copyText("Runtime", "install_root", ".");
|
||||
copyText("Runtime", "main_executable", "MainApp.exe");
|
||||
copyText("Runtime", "launcher_executable", "Launcher.exe");
|
||||
copyText("Runtime", "updater_executable", "Updater.exe");
|
||||
|
||||
@@ -8,9 +8,15 @@ public:
|
||||
QString getValue(const QString& section, const QString& key) const;
|
||||
bool setValue(const QString& section, const QString& key, const QString& value);
|
||||
QString configPath() const;
|
||||
QString installRoot() const;
|
||||
QString runtimeRoot() const;
|
||||
QString updateRoot() const;
|
||||
QString runtimeRelativePath() const;
|
||||
QString lastError() const;
|
||||
|
||||
private:
|
||||
ConfigHelper();
|
||||
bool migrateLegacyIniIfNeeded();
|
||||
QString m_configPath;
|
||||
QString m_error;
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <QSaveFile>
|
||||
#include <QSysInfo>
|
||||
#include <QUuid>
|
||||
#include <QTimer>
|
||||
#ifdef HAVE_OPENSSL
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/pem.h>
|
||||
@@ -35,7 +36,14 @@ bool DeviceIdentityHelper::loadAndVerify(const QString& appId,const QString& cha
|
||||
bool DeviceIdentityHelper::verifyLocal(const QString& appId,const QString& channel){m_error.clear();return loadAndVerify(appId,channel);}
|
||||
bool DeviceIdentityHelper::ensureIssued(const QString& base,const QString& token,const QString& appId,const QString& channel,const QString& licenseKey){
|
||||
m_error.clear();if(loadAndVerify(appId,channel)){ConfigHelper::instance().setValue("Update","device_id",m_deviceId);return true;}
|
||||
QString installation=ConfigHelper::instance().getValue("Device","installation_id");if(installation.isEmpty()){installation=QUuid::createUuid().toString(QUuid::WithoutBraces);if(!ConfigHelper::instance().setValue("Device","installation_id",installation)){m_error="cannot save installation id";return false;}}
|
||||
const QString trimmedBase=base.trimmed();
|
||||
if(appId.trimmed().isEmpty()){m_error="app_id is empty in config/app_config.json";return false;}
|
||||
if(channel.trimmed().isEmpty()){m_error="channel is empty in config/app_config.json";return false;}
|
||||
if(trimmedBase.isEmpty()||trimmedBase.contains("YOUR_SERVER_IP",Qt::CaseInsensitive)){m_error="api_base_url is not configured. Set it to the update server address, for example http://192.168.229.128:8000";return false;}
|
||||
if(token.trimmed().isEmpty()){m_error="client_token is empty in config/app_config.json";return false;}
|
||||
if(licenseKey.trimmed().isEmpty()){m_error="license_key is empty. Create a License in the admin page and paste the generated key into config/app_config.json";return false;}
|
||||
ConfigHelper& config=ConfigHelper::instance();
|
||||
QString installation=config.getValue("Device","installation_id");if(installation.isEmpty()){installation=QUuid::createUuid().toString(QUuid::WithoutBraces);if(!config.setValue("Device","installation_id",installation)){m_error=QString("cannot save installation id to %1: %2").arg(config.configPath(),config.lastError());return false;}}
|
||||
QByteArray machine=QSysInfo::machineUniqueId()+installation.toUtf8();QString mh=QString::fromLatin1(QCryptographicHash::hash(machine,QCryptographicHash::Sha256).toHex());QJsonObject body{{"app_id",appId},{"channel",channel},{"license_key",licenseKey},{"installation_id",installation},{"machine_hash",mh}};
|
||||
QNetworkAccessManager manager;QNetworkRequest req{QUrl(base+"/api/v1/device/issue")};req.setHeader(QNetworkRequest::ContentTypeHeader,"application/json");req.setRawHeader("X-Client-Token",token.toUtf8());QNetworkReply* reply=manager.post(req,QJsonDocument(body).toJson(QJsonDocument::Compact));QEventLoop loop;QObject::connect(reply,&QNetworkReply::finished,&loop,&QEventLoop::quit);loop.exec();int status=reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();QByteArray raw=reply->readAll();reply->deleteLater();if(status!=200){m_error=QString("device issue failed (HTTP %1): %2").arg(status).arg(QString::fromUtf8(raw));return false;}auto response=QJsonDocument::fromJson(raw).object();QJsonObject wrapper{{"identity_text",response.value("identity_text")},{"signature",response.value("signature")}};QString path=QDir(m_installDir).filePath("config/client_identity.dat");QSaveFile out(path);QByteArray bytes=QJsonDocument(wrapper).toJson(QJsonDocument::Compact);if(!out.open(QIODevice::WriteOnly)||out.write(bytes)!=bytes.size()||!out.commit()){m_error="cannot save device credential";return false;}if(!loadAndVerify(appId,channel))return false;if(!ConfigHelper::instance().setValue("Update","device_id",m_deviceId)){m_error="cannot save server device id";return false;}return true;
|
||||
QNetworkAccessManager manager;QNetworkRequest req{QUrl(trimmedBase+"/api/v1/device/issue")};req.setHeader(QNetworkRequest::ContentTypeHeader,"application/json");req.setRawHeader("X-Client-Token",token.toUtf8());QNetworkReply* reply=manager.post(req,QJsonDocument(body).toJson(QJsonDocument::Compact));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())reply->abort();});QObject::connect(reply,&QNetworkReply::finished,&loop,&QEventLoop::quit);timer.start(timeoutMs);loop.exec();timer.stop();int status=reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();QByteArray raw=reply->readAll();reply->deleteLater();if(status!=200){m_error=QString("device issue failed (HTTP %1): %2").arg(status).arg(QString::fromUtf8(raw));return false;}auto response=QJsonDocument::fromJson(raw).object();QJsonObject wrapper{{"identity_text",response.value("identity_text")},{"signature",response.value("signature")}};QString path=QDir(m_installDir).filePath("config/client_identity.dat");QSaveFile out(path);QByteArray bytes=QJsonDocument(wrapper).toJson(QJsonDocument::Compact);if(!out.open(QIODevice::WriteOnly)||out.write(bytes)!=bytes.size()||!out.commit()){m_error=QString("cannot save device credential to %1: %2").arg(path,out.errorString());return false;}if(!loadAndVerify(appId,channel))return false;if(!config.setValue("Update","device_id",m_deviceId)){m_error=QString("cannot save server device id to %1: %2").arg(config.configPath(),config.lastError());return false;}return true;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#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)
|
||||
@@ -27,9 +28,22 @@ void HttpHelper::postRequest(const QString& url, const QJsonObject& jsonBody,
|
||||
|
||||
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;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "IntegrityHelper.h"
|
||||
#include "ConfigHelper.h"
|
||||
#include <QCryptographicHash>
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
@@ -28,10 +29,19 @@ bool IntegrityHelper::safeRelativePath(const QString& path) const
|
||||
bool IntegrityHelper::runtimeProtectedPath(const QString& path) const
|
||||
{
|
||||
const QString p = QDir::fromNativeSeparators(path).toCaseFolded();
|
||||
static const QSet<QString> protectedPaths{
|
||||
QSet<QString> protectedPaths{
|
||||
"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.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);
|
||||
}
|
||||
|
||||
@@ -49,7 +59,10 @@ bool IntegrityHelper::verifySignature(const QByteArray& payload, const QString&
|
||||
#ifndef HAVE_OPENSSL
|
||||
Q_UNUSED(payload); Q_UNUSED(signatureBase64); m_error = "OpenSSL unavailable"; return false;
|
||||
#else
|
||||
QFile keyFile(QDir(m_installDir).filePath("config/manifest_public_key.pem"));
|
||||
QString keyPath = QDir(m_installDir).filePath("config/manifest_public_key.pem");
|
||||
if (!QFile::exists(keyPath))
|
||||
keyPath = QFileInfo(ConfigHelper::instance().configPath()).dir().filePath("manifest_public_key.pem");
|
||||
QFile keyFile(keyPath);
|
||||
if (!keyFile.open(QIODevice::ReadOnly)) { m_error = "manifest public key missing"; return false; }
|
||||
const QByteArray keyData = keyFile.readAll();
|
||||
BIO* bio = BIO_new_mem_buf(keyData.constData(), keyData.size());
|
||||
@@ -72,8 +85,12 @@ bool IntegrityHelper::verifyInstalledVersion(const QString& appId, const QString
|
||||
const QString& version)
|
||||
{
|
||||
m_error.clear();
|
||||
const QString cachePath = QDir(m_installDir).filePath(
|
||||
QString cachePath = QDir(ConfigHelper::instance().updateRoot()).filePath(
|
||||
"manifest_cache/manifest_" + version + ".json");
|
||||
const QString legacyCachePath = QDir(m_installDir).filePath(
|
||||
"update/manifest_cache/manifest_" + version + ".json");
|
||||
if (!QFile::exists(cachePath))
|
||||
cachePath = legacyCachePath;
|
||||
QFile cache(cachePath);
|
||||
if (!cache.open(QIODevice::ReadOnly)) { m_error = "signed manifest cache missing for " + version; return false; }
|
||||
QJsonParseError wrapperError;
|
||||
@@ -121,8 +138,12 @@ bool IntegrityHelper::verifyInstalledVersion(const QString& appId, const QString
|
||||
const QString fullPath = it.next();
|
||||
const QString relative = QDir::fromNativeSeparators(root.relativeFilePath(fullPath));
|
||||
const QString folded = relative.toCaseFolded();
|
||||
const QString runtimePrefix = ConfigHelper::instance().runtimeRelativePath().toCaseFolded();
|
||||
const bool runtimeWorkDir = !runtimePrefix.isEmpty()
|
||||
&& (folded.startsWith(runtimePrefix + "/update/")
|
||||
|| folded.startsWith(runtimePrefix + "/update_temp/"));
|
||||
if (folded.startsWith("update/") || folded.startsWith("update_temp/")
|
||||
|| runtimeProtectedPath(relative)) continue;
|
||||
|| runtimeWorkDir || runtimeProtectedPath(relative)) continue;
|
||||
const QString suffix = QFileInfo(relative).suffix().toCaseFolded();
|
||||
if ((suffix == "exe" || suffix == "dll") && !declaredExecutables.contains(folded)) {
|
||||
m_error = "undeclared executable or plugin: " + relative; return false;
|
||||
|
||||
Reference in New Issue
Block a user