Files

461 lines
20 KiB
C++

#include <QApplication>
#include <QDebug>
#include <QMessageBox>
#include <QProcess>
#include <QProgressDialog>
#include <QInputDialog>
#include <QLineEdit>
#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 <QFile>
#include <QFileDialog>
#include <QDir>
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;
}