fix: 优化客户端错误提示和校验诊断信息
This commit is contained in:
+106
-36
@@ -5,10 +5,11 @@
|
||||
#include <QFile>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QMessageAuthenticationCode>
|
||||
#include <QSaveFile>
|
||||
#include <QStandardPaths>
|
||||
#include <QUuid>
|
||||
#include <QMessageAuthenticationCode>
|
||||
#include <QSaveFile>
|
||||
#include <QStandardPaths>
|
||||
#include <QStringList>
|
||||
#include <QUuid>
|
||||
|
||||
static QByteArray ticketMac(const QJsonObject& payload, const QString& secret)
|
||||
{
|
||||
@@ -22,10 +23,20 @@ bool TicketHelper::createTicket(const QString& appId, const QString& deviceId,
|
||||
{
|
||||
// Launcher 启动业务主程序前生成一次性 ticket。
|
||||
// ticket 只保存在临时目录、有效期 60 秒,并用 launch_token 做 HMAC,防止用户绕过 Launcher 直接启动主程序。
|
||||
if (appId.isEmpty() || version.isEmpty() || secret.isEmpty()) {
|
||||
if (errorMessage) *errorMessage = "ticket identity or secret is empty";
|
||||
return false;
|
||||
}
|
||||
if (appId.isEmpty() || deviceId.isEmpty() || version.isEmpty() || secret.isEmpty()) {
|
||||
if (errorMessage) {
|
||||
QStringList missing;
|
||||
if (appId.isEmpty()) missing.append(QStringLiteral("app_id"));
|
||||
if (deviceId.isEmpty()) missing.append(QStringLiteral("device_id"));
|
||||
if (version.isEmpty()) missing.append(QStringLiteral("current_version"));
|
||||
if (secret.isEmpty()) missing.append(QStringLiteral("launch_token"));
|
||||
*errorMessage = QCoreApplication::translate(
|
||||
"TicketHelper",
|
||||
"Cannot create launch ticket because required fields are empty: %1. Check app_config.json, registry-imported configuration and device authorization.")
|
||||
.arg(missing.join(QStringLiteral(", ")));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const QDateTime now = QDateTime::currentDateTimeUtc();
|
||||
QJsonObject payload{
|
||||
{"app_id", appId}, {"device_id", deviceId}, {"version", version},
|
||||
@@ -36,14 +47,27 @@ bool TicketHelper::createTicket(const QString& appId, const QString& deviceId,
|
||||
};
|
||||
QJsonObject wrapper{{"payload", payload}, {"signature", QString::fromLatin1(ticketMac(payload, secret))}};
|
||||
const QString dirPath = QDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation)).filePath("marsco_tickets");
|
||||
if (!QDir().mkpath(dirPath)) { if (errorMessage) *errorMessage = "cannot create ticket directory"; return false; }
|
||||
if (!QDir().mkpath(dirPath)) {
|
||||
if (errorMessage) {
|
||||
*errorMessage = QCoreApplication::translate(
|
||||
"TicketHelper",
|
||||
"Cannot create launch ticket directory: %1.")
|
||||
.arg(dirPath);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const QString path = QDir(dirPath).filePath("ticket_" + QUuid::createUuid().toString(QUuid::WithoutBraces) + ".json");
|
||||
QSaveFile file(path);
|
||||
const QByteArray bytes = QJsonDocument(wrapper).toJson(QJsonDocument::Compact);
|
||||
if (!file.open(QIODevice::WriteOnly) || file.write(bytes) != bytes.size() || !file.commit()) {
|
||||
if (errorMessage) *errorMessage = "cannot save ticket";
|
||||
return false;
|
||||
}
|
||||
QSaveFile file(path);
|
||||
const QByteArray bytes = QJsonDocument(wrapper).toJson(QJsonDocument::Compact);
|
||||
if (!file.open(QIODevice::WriteOnly) || file.write(bytes) != bytes.size() || !file.commit()) {
|
||||
if (errorMessage) {
|
||||
*errorMessage = QCoreApplication::translate(
|
||||
"TicketHelper",
|
||||
"Cannot save launch ticket file: %1. Error: %2.")
|
||||
.arg(path, file.errorString());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
QFile::setPermissions(path, QFileDevice::ReadOwner | QFileDevice::WriteOwner);
|
||||
if (ticketPath) *ticketPath = path;
|
||||
return true;
|
||||
@@ -56,24 +80,40 @@ bool TicketHelper::consumeAndVerify(const QString& ticketPath, const QString& ex
|
||||
// 主程序启动后第一时间把 ticket 改名成 .consuming,再读取并删除。
|
||||
// 这样同一张 ticket 即使校验失败也不能被重复使用,避免重放启动。
|
||||
const QString consumingPath = ticketPath + ".consuming."
|
||||
+ QString::number(QCoreApplication::applicationPid());
|
||||
if (!QFile::rename(ticketPath, consumingPath)) {
|
||||
if (errorMessage) *errorMessage = "ticket missing or already consumed";
|
||||
return false;
|
||||
}
|
||||
QFile file(consumingPath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
QFile::remove(consumingPath);
|
||||
if (errorMessage) *errorMessage = "cannot read claimed ticket";
|
||||
return false;
|
||||
}
|
||||
+ QString::number(QCoreApplication::applicationPid());
|
||||
if (!QFile::rename(ticketPath, consumingPath)) {
|
||||
if (errorMessage) {
|
||||
*errorMessage = QCoreApplication::translate(
|
||||
"TicketHelper",
|
||||
"Launch ticket is missing or has already been consumed. Ticket file: %1. Please start the application from Launcher.")
|
||||
.arg(ticketPath);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
QFile file(consumingPath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
QFile::remove(consumingPath);
|
||||
if (errorMessage) {
|
||||
*errorMessage = QCoreApplication::translate(
|
||||
"TicketHelper",
|
||||
"Cannot read claimed launch ticket: %1. Error: %2.")
|
||||
.arg(consumingPath, file.errorString());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const QByteArray raw = file.readAll(); file.close();
|
||||
QFile::remove(consumingPath); // Consume once; it must not be replayed regardless of success or failure.
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(raw, &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
|
||||
if (errorMessage) *errorMessage = "invalid ticket JSON"; return false;
|
||||
}
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(raw, &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
|
||||
if (errorMessage) {
|
||||
*errorMessage = QCoreApplication::translate(
|
||||
"TicketHelper",
|
||||
"Launch ticket is not valid JSON. JSON error: %1.")
|
||||
.arg(parseError.errorString());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const QJsonObject wrapper = doc.object();
|
||||
const QJsonObject payload = wrapper.value("payload").toObject();
|
||||
const QByteArray actual = wrapper.value("signature").toString().toLatin1();
|
||||
@@ -84,27 +124,57 @@ bool TicketHelper::consumeAndVerify(const QString& ticketPath, const QString& ex
|
||||
const bool timeOk = issued.isValid() && expires.isValid() && issued <= now.addSecs(5)
|
||||
&& expires >= now && issued.secsTo(expires) <= 65;
|
||||
if (actual.isEmpty() || actual != expected) {
|
||||
if (errorMessage) *errorMessage = "ticket signature invalid; check launch_token";
|
||||
if (errorMessage) {
|
||||
*errorMessage = QCoreApplication::translate(
|
||||
"TicketHelper",
|
||||
"Launch ticket signature is invalid. The launch_token used by Launcher and the main application is inconsistent, or the ticket content was changed.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (payload.value("app_id").toString() != expectedAppId) {
|
||||
if (errorMessage) *errorMessage = "ticket app_id mismatch";
|
||||
if (errorMessage) {
|
||||
*errorMessage = QCoreApplication::translate(
|
||||
"TicketHelper",
|
||||
"Launch ticket app_id does not match. Ticket app_id: %1. Expected app_id: %2.")
|
||||
.arg(payload.value("app_id").toString(), expectedAppId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (payload.value("device_id").toString() != expectedDeviceId) {
|
||||
if (errorMessage) *errorMessage = "ticket device_id mismatch";
|
||||
if (errorMessage) {
|
||||
*errorMessage = QCoreApplication::translate(
|
||||
"TicketHelper",
|
||||
"Launch ticket device_id does not match. Ticket device_id: %1. Expected device_id: %2. Reauthorize the device from Launcher if the configuration was regenerated.")
|
||||
.arg(payload.value("device_id").toString(), expectedDeviceId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (payload.value("version").toString() != expectedVersion) {
|
||||
if (errorMessage) *errorMessage = "ticket version mismatch";
|
||||
if (errorMessage) {
|
||||
*errorMessage = QCoreApplication::translate(
|
||||
"TicketHelper",
|
||||
"Launch ticket version does not match. Ticket version: %1. Expected version: %2.")
|
||||
.arg(payload.value("version").toString(), expectedVersion);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (!timeOk) {
|
||||
if (errorMessage) *errorMessage = "ticket time invalid or expired";
|
||||
if (errorMessage) {
|
||||
*errorMessage = QCoreApplication::translate(
|
||||
"TicketHelper",
|
||||
"Launch ticket time is invalid or expired. Issued at: %1. Expires at: %2. Current UTC time: %3.")
|
||||
.arg(payload.value("issued_at").toString(),
|
||||
payload.value("expires_at").toString(),
|
||||
now.toString(Qt::ISODate));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (payload.value("nonce").toString().isEmpty()) {
|
||||
if (errorMessage) *errorMessage = "ticket nonce missing";
|
||||
if (errorMessage) {
|
||||
*errorMessage = QCoreApplication::translate(
|
||||
"TicketHelper",
|
||||
"Launch ticket nonce is missing. The ticket is incomplete.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user