#include #include #include #include #include #include #include #include "UpdateLogic.h" #include "../Common/ConfigHelper.h" #include "../Common/PolicyHelper.h" #include "../Common/LocalStateHelper.h" #include "../Common/TicketHelper.h" #include "../Common/DeviceIdentityHelper.h" #include "../Common/IntegrityHelper.h" #include "../Common/ManifestBootstrapPolicy.h" #include "../Common/TranslationHelper.h" #include "../Updater/UpdaterLogic.h" #include #include #include int main(int argc, char *argv[]) { QApplication app(argc, argv); QApplication::setApplicationName("Marsco Launcher"); TranslationHelper translation; translation.installForSystemLocale(); QProgressDialog progress(QCoreApplication::translate("Launcher", "Checking for software updates..."), QString(), 0, 0); progress.setWindowTitle(QCoreApplication::translate("Launcher", "Marsco Launcher")); progress.setCancelButton(nullptr); progress.setWindowModality(Qt::ApplicationModal); progress.setMinimumDuration(0); progress.setAutoClose(false); progress.show(); QApplication::processEvents(); const QString appDir = QApplication::applicationDirPath(); ConfigHelper &config = ConfigHelper::instance(); const auto isLicenseError = [&translation](const QString &errorText) { const QString text = errorText.toLower(); QStringList markers{QStringLiteral("license")}; const char *const markerSources[]{ QT_TRANSLATE_NOOP("LauncherErrorMarker", "authorization"), QT_TRANSLATE_NOOP("LauncherErrorMarker", "expired"), QT_TRANSLATE_NOOP("LauncherErrorMarker", "device limit reached"), QT_TRANSLATE_NOOP("LauncherErrorMarker", "bound to another license")}; for (const char *source : markerSources) { markers.append(QCoreApplication::translate("LauncherErrorMarker", source)); markers.append(translation.translate("LauncherErrorMarker", source)); } for (const QString &marker : markers) { if (!marker.isEmpty() && text.contains(marker.toLower())) return true; } return false; }; const auto clearDeviceCredential = [&]() { QFile::remove(QDir(appDir).filePath("config/client_identity.dat")); config.setValue("Update", "device_id", QString()); }; QString licenseKey = config.getValue("License", "license_key").trimmed(); auto promptAndSaveLicense = [&](const QString &reason) -> QString { QString promptReason = reason; while (true) { progress.close(); bool accepted = false; const QString appName = config.getValue("App", "app_name").trimmed(); const QString displayName = appName.isEmpty() ? QCoreApplication::translate("Launcher", "application") : appName; const QString prompt = promptReason.trimmed().isEmpty() ? QCoreApplication::translate("Launcher", "Enter the license for %1:").arg(displayName) : QCoreApplication::translate("Launcher", "%1\n\nPlease re-enter the license for %2:").arg(promptReason, displayName); licenseKey = QInputDialog::getText( nullptr, QCoreApplication::translate("Launcher", "Enter License"), prompt, QLineEdit::Normal, QString(), &accepted) .trimmed(); if (!accepted) { QMessageBox::information( nullptr, QCoreApplication::translate("Launcher", "License Required"), QCoreApplication::translate("Launcher", "A license provided by an administrator is required for the first launch.")); return QString(); } if (licenseKey.isEmpty()) { QMessageBox::warning( nullptr, QCoreApplication::translate("Launcher", "License Cannot Be Empty"), QCoreApplication::translate("Launcher", "Paste the license created by an administrator.")); promptReason.clear(); continue; } if (!config.setValue("License", "license_key", licenseKey)) { QMessageBox::critical( nullptr, QCoreApplication::translate("Launcher", "Failed to Save License"), QCoreApplication::translate("Launcher", "Cannot save system state at %1:\n%2") .arg(config.configPath(), config.lastError())); return QString(); } progress.show(); progress.setLabelText(QCoreApplication::translate("Launcher", "Verifying license...")); QApplication::processEvents(); return licenseKey; } }; while (true) { if (licenseKey.isEmpty()) { licenseKey = promptAndSaveLicense(QString()); if (licenseKey.isEmpty()) return 0; } DeviceIdentityHelper identity(appDir); if (identity.ensureIssued(config.getValue("Server", "api_base_url"), config.getValue("Server", "client_token"), config.getValue("App", "app_id"), config.getValue("App", "channel"), licenseKey)) { break; } const QString error = identity.errorString(); if (!isLicenseError(error)) { progress.close(); QMessageBox::critical(nullptr, QCoreApplication::translate("Launcher", "Device Authentication Failed"), error); return -1; } clearDeviceCredential(); licenseKey = promptAndSaveLicense(QCoreApplication::translate("Launcher", "The current license cannot be used: %1").arg(error)); if (licenseKey.isEmpty()) return 0; } UpdateLogic logic; logic.checkUpdate(); const bool needUpdate = logic.getNeedUpdate(); const bool networkOk = logic.isNetworkOk(); const QString latestVer = logic.getLatestVersion(); const QJsonObject response = logic.getCheckResult(); const int targetVersionId = response.value("version_id").toInt(); const QString appId = logic.getAppId(); const QString channel = logic.getChannel(); if (logic.lastStatusCode() == 401 || logic.lastStatusCode() == 403) { progress.close(); const QJsonValue detail = response.value("detail"); const QString message = detail.isObject() ? detail.toObject().value("msg").toString() : detail.toString(); const QString displayMessage = message.isEmpty() ? QCoreApplication::translate("Launcher", "The device or license authorization is invalid.") : message; if (isLicenseError(displayMessage)) { clearDeviceCredential(); const QString newLicense = promptAndSaveLicense( QCoreApplication::translate("Launcher", "The server rejected the current license: %1").arg(displayMessage)); if (!newLicense.isEmpty()) { progress.close(); QMessageBox::information( nullptr, QCoreApplication::translate("Launcher", "License Saved"), QCoreApplication::translate("Launcher", "Restart Launcher to complete device authorization and check for updates.")); } return 0; } QMessageBox::critical(nullptr, QCoreApplication::translate("Launcher", "Authorization Denied"), displayMessage); return -1; } const QString launchToken = config.getValue("App", "launch_token"); const QString currentVersion = config.getValue("App", "current_version"); const auto configuredName = [&](const QString &key, const QString &fallback) { const QString value = config.getValue("Runtime", key).trimmed(); return value.isEmpty() ? fallback : value; }; const QString mainExecutable = configuredName("main_executable", "MainApp.exe"); const QString updaterExecutable = configuredName("updater_executable", "Updater.exe"); const QString mainAppPath = QDir(appDir).filePath(mainExecutable); const QString updaterPath = QDir(appDir).filePath(updaterExecutable); IntegrityHelper integrity(config.installRoot()); bool integrityVerified = integrity.verifyInstalledVersion( appId, channel, currentVersion); QString integrityError = integrity.errorString(); const UpdateClient::ManifestBootstrapAction bootstrapAction = UpdateClient::manifestBootstrapAction( integrity.isManifestCacheMissing(), networkOk, needUpdate, targetVersionId); if (!integrityVerified && bootstrapAction == UpdateClient::ManifestBootstrapAction::FetchCurrentManifest) { UpdaterLogic manifestLogic; manifestLogic.getManifest( appId, channel, currentVersion, targetVersionId); const QString cacheDirectory = QDir(config.updateRoot()).filePath( QStringLiteral("manifest_cache")); if (manifestLogic.verifyManifestSignature() && manifestLogic.saveManifestCache(cacheDirectory)) { integrityVerified = integrity.verifyInstalledVersion( appId, channel, currentVersion); integrityError = integrity.errorString(); } else { integrityError = QCoreApplication::translate( "Launcher", "The signed manifest for version %1 could not be fetched or cached.") .arg(currentVersion); } } else if (!integrityVerified && bootstrapAction == UpdateClient::ManifestBootstrapAction::CurrentVersionNotPublished) { integrityError = QCoreApplication::translate( "Launcher", "Version %1 is not published on the update server. Publish this exact installed build before distribution so its signed manifest can be verified.") .arg(currentVersion); } if (!integrityVerified) { progress.close(); QMessageBox::critical( nullptr, QCoreApplication::translate("Launcher", "Integrity Check Failed"), QCoreApplication::translate( "Launcher", "Application files failed signed manifest verification:\n%1") .arg(integrityError)); return -1; } const auto importOfflinePackage = [&]() { const QString package = QFileDialog::getOpenFileName( nullptr, QCoreApplication::translate("Launcher", "Select Offline Update Package"), QString(), QCoreApplication::translate("Launcher", "Marsco Offline Update Package (*.upd)")); return package.isEmpty() ? false : QProcess::startDetached(updaterPath, QStringList{QString("--offline-package=%1").arg(package)}); }; const QString deviceId = config.getValue("Update", "device_id"); if (QCoreApplication::arguments().contains("--import-offline")) { progress.close(); if (!importOfflinePackage()) QMessageBox::information( nullptr, QCoreApplication::translate("Launcher", "Offline Update"), QCoreApplication::translate("Launcher", "No offline update package was selected.")); return 0; } const auto startMainApp = [&]() { QString ticketPath; QString ticketError; if (!TicketHelper::createTicket(logic.getAppId(), deviceId, currentVersion, launchToken, &ticketPath, &ticketError)) { qDebug() << "Cannot create launch ticket:" << ticketError; return false; } const bool started = QProcess::startDetached(mainAppPath, QStringList{QString("--ticket-file=%1").arg(ticketPath)}); if (!started) QFile::remove(ticketPath); return started; }; progress.setLabelText(QCoreApplication::translate("Launcher", "Validating the local run policy...")); QApplication::processEvents(); PolicyHelper policy(appDir); if (!policy.loadPolicy("config/version_policy.dat") || !policy.isValid()) { progress.close(); QMessageBox::critical( nullptr, QCoreApplication::translate("Launcher", "Unable to Start"), QCoreApplication::translate("Launcher", "The local version policy is invalid: %1").arg(policy.errorString())); return -1; } if (!policy.isApplicable(appId, channel, currentVersion)) { progress.close(); QMessageBox::critical( nullptr, QCoreApplication::translate("Launcher", "Unable to Start"), QCoreApplication::translate("Launcher", "The local version policy is invalid: %1").arg(policy.errorString())); return -1; } if (policy.isExpired()) { progress.close(); QMessageBox::critical( nullptr, QCoreApplication::translate("Launcher", "Unable to Start"), QCoreApplication::translate("Launcher", "The local version policy has expired. Connect to the network or contact an administrator.")); return -1; } const bool currentVersionAllowed = policy.allowRun() && policy.isVersionAllowed(currentVersion); if (!currentVersionAllowed && !(networkOk && needUpdate)) { progress.close(); QMessageBox::critical( nullptr, QCoreApplication::translate("Launcher", "Current Version Cannot Run"), policy.message().isEmpty() ? QCoreApplication::translate("Launcher", "Version %1 has been disabled by an administrator.").arg(currentVersion) : policy.message()); return -1; } LocalStateHelper state(appDir); if (!state.loadState()) { progress.close(); QMessageBox::critical( nullptr, QCoreApplication::translate("Launcher", "Unable to Start"), QCoreApplication::translate("Launcher", "Cannot read the local state: %1").arg(state.errorString())); return -1; } if (state.isPolicySeqRolledBack(policy.policySeq())) { progress.close(); QMessageBox::critical( nullptr, QCoreApplication::translate("Launcher", "Security Check Failed"), QCoreApplication::translate("Launcher", "A version policy sequence rollback was detected, so startup was blocked.")); return -1; } if (state.isSystemTimeRewound()) { progress.close(); QMessageBox::critical( nullptr, QCoreApplication::translate("Launcher", "Security Check Failed"), QCoreApplication::translate("Launcher", "A possible system clock rollback was detected, so startup was blocked.")); return -1; } if (networkOk && needUpdate) { progress.close(); const bool rollbackOperation = response.value("action").toString() == "rollback_allowed"; const bool updateRequired = policy.forceUpdate() || !currentVersionAllowed; const QString dialogTitle = rollbackOperation ? QCoreApplication::translate("Launcher", "Version Rollback") : (updateRequired ? QCoreApplication::translate("Launcher", "Update Required") : QCoreApplication::translate("Launcher", "New Version Available")); const QString prompt = policy.message().isEmpty() ? (rollbackOperation ? QCoreApplication::translate("Launcher", "An administrator selected version %1 as the rollback target. Downgrade now?") : QCoreApplication::translate("Launcher", "Version %1 is available. Update now?")) .arg(latestVer) : policy.message() + QCoreApplication::translate("Launcher", "\nTarget version: %1").arg(latestVer); bool accepted = true; if (updateRequired) { QMessageBox::information(nullptr, dialogTitle, prompt); } else { accepted = QMessageBox::question(nullptr, dialogTitle, prompt, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes; } if (!accepted) { if (!startMainApp()) { QMessageBox::critical( nullptr, QCoreApplication::translate("Launcher", "Startup Failed"), QCoreApplication::translate("Launcher", "Cannot start the main application: %1").arg(mainAppPath)); return -1; } return 0; } const QStringList updaterArgs{appId, channel, latestVer, QString::number(targetVersionId)}; if (!QProcess::startDetached(updaterPath, updaterArgs)) { QMessageBox::critical( nullptr, QCoreApplication::translate("Launcher", "Updater Startup Failed"), QCoreApplication::translate("Launcher", "Cannot start the updater: %1").arg(updaterPath)); return -1; } return 0; } if (!networkOk) { progress.close(); if (QMessageBox::question(nullptr, QCoreApplication::translate("Launcher", "Server Unavailable"), QCoreApplication::translate("Launcher", "The update server is currently unreachable. Import an offline update package?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes) { if (!importOfflinePackage()) QMessageBox::information( nullptr, QCoreApplication::translate("Launcher", "Offline Update"), QCoreApplication::translate("Launcher", "No offline update package was selected, or the updater could not be started.")); return 0; } progress.show(); } if (!networkOk && !policy.isOfflineAllowed()) { progress.close(); QMessageBox::critical( nullptr, QCoreApplication::translate("Launcher", "Network Unavailable"), QCoreApplication::translate("Launcher", "The update server is unreachable and the current policy does not allow offline startup.")); return -1; } progress.setLabelText( networkOk ? QCoreApplication::translate("Launcher", "The current version is up to date. Starting...") : QCoreApplication::translate("Launcher", "Offline mode is active. Starting...")); QApplication::processEvents(); if (!startMainApp()) { progress.close(); QMessageBox::critical( nullptr, QCoreApplication::translate("Launcher", "Startup Failed"), QCoreApplication::translate("Launcher", "Cannot start the main application: %1").arg(mainAppPath)); return -1; } progress.close(); return 0; }