license login feature
This commit is contained in:
+207
-7
@@ -10,6 +10,7 @@
|
||||
#include "versioninstaller.h"
|
||||
#include "versionmanifestservice.h"
|
||||
#include "modloaderinstaller.h"
|
||||
#include "msaauthservice.h"
|
||||
#include "modloaderversionservice.h"
|
||||
#include "buildswitcher.h"
|
||||
#include "seasonalbuildservice.h"
|
||||
@@ -35,12 +36,27 @@ namespace {
|
||||
|
||||
const QString kOffline = QStringLiteral("offline");
|
||||
const QString kElyBy = QStringLiteral("elyby");
|
||||
const QString kMicrosoft = QStringLiteral("microsoft");
|
||||
|
||||
// Токен Minecraft живёт около суток, но обновляем его чуть раньше срока: игра
|
||||
// проверяет сессию не в первую секунду после старта.
|
||||
const int kTokenRenewMarginSecs = 120;
|
||||
|
||||
// Сервер готовых сборок по умолчанию. Переопределяется ключом seasonalBaseUrl
|
||||
// в settings.json — это нужно, чтобы прогонять установку против локального
|
||||
// http-сервера, не трогая боевой.
|
||||
const QString kSeasonalBaseUrl = QStringLiteral("https://launcher-builds.galeonworx.com");
|
||||
|
||||
// Тип авторизации профиля: всё, что лаунчер не знает, считается офлайном.
|
||||
QString normalizedAuthType(const QString &value)
|
||||
{
|
||||
if (value == kElyBy)
|
||||
return kElyBy;
|
||||
if (value == kMicrosoft)
|
||||
return kMicrosoft;
|
||||
return kOffline;
|
||||
}
|
||||
|
||||
// Дата сезона в таблице: год нужен, время — нет.
|
||||
QString formatSeasonDate(const QDate &date)
|
||||
{
|
||||
@@ -60,6 +76,7 @@ int settingInt(const QVariantMap &settings, const QString &key, int fallback)
|
||||
LauncherBackend::LauncherBackend(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_auth(new AuthService(this))
|
||||
, m_msa(new MsaAuthService(this))
|
||||
, m_launcher(new GameLauncher(this))
|
||||
, m_manifest(new VersionManifestService(this))
|
||||
, m_installer(new VersionInstaller(m_manifest, this))
|
||||
@@ -95,6 +112,7 @@ LauncherBackend::LauncherBackend(QObject *parent)
|
||||
}
|
||||
|
||||
connect(m_auth, &AuthService::progress, this, &LauncherBackend::launchProgress);
|
||||
connect(m_msa, &MsaAuthService::progress, this, &LauncherBackend::launchProgress);
|
||||
connect(m_launcher, &GameLauncher::progress, this, &LauncherBackend::launchProgress);
|
||||
connect(m_launcher, &GameLauncher::output, this, &LauncherBackend::gameOutput);
|
||||
connect(m_launcher, &GameLauncher::gameStarted, this, [this](const QString &commandLine) {
|
||||
@@ -396,7 +414,7 @@ void LauncherBackend::addProfile(const QString &name,
|
||||
profile.name = name;
|
||||
profile.login = login;
|
||||
profile.password = password;
|
||||
profile.authType = (authType == kElyBy) ? kElyBy : kOffline;
|
||||
profile.authType = normalizedAuthType(authType);
|
||||
profile.clientToken = AuthService::generateClientToken();
|
||||
m_profiles.append(profile);
|
||||
saveProfiles();
|
||||
@@ -428,16 +446,30 @@ void LauncherBackend::updateProfile(int index,
|
||||
return;
|
||||
|
||||
Profile &profile = m_profiles[index];
|
||||
// Смена логина или пароля обесценивает сохранённый токен Ely.by.
|
||||
if (profile.login != login || profile.password != password) {
|
||||
const QString type = normalizedAuthType(authType);
|
||||
|
||||
// Смена логина или пароля обесценивает сохранённый токен Ely.by. У аккаунта
|
||||
// Microsoft ни того ни другого нет, и переименование профиля не должно
|
||||
// выкидывать из аккаунта.
|
||||
if (profile.authType == kElyBy && (profile.login != login || profile.password != password)) {
|
||||
profile.accessToken.clear();
|
||||
profile.uuid.clear();
|
||||
profile.resolvedName.clear();
|
||||
}
|
||||
// Смена типа авторизации: чужие учётные данные к новому типу не подходят.
|
||||
if (profile.authType != type) {
|
||||
profile.accessToken.clear();
|
||||
profile.refreshToken.clear();
|
||||
profile.uuid.clear();
|
||||
profile.resolvedName.clear();
|
||||
profile.xuid.clear();
|
||||
profile.tokenExpiry.clear();
|
||||
profile.userType.clear();
|
||||
}
|
||||
profile.name = name;
|
||||
profile.login = login;
|
||||
profile.password = password;
|
||||
profile.authType = (authType == kElyBy) ? kElyBy : kOffline;
|
||||
profile.authType = type;
|
||||
if (profile.clientToken.isEmpty())
|
||||
profile.clientToken = AuthService::generateClientToken();
|
||||
|
||||
@@ -495,7 +527,9 @@ QVariantMap LauncherBackend::profileAt(int index) const
|
||||
return {{"name", p.name},
|
||||
{"login", p.login},
|
||||
{"password", p.password},
|
||||
{"authType", p.authType}};
|
||||
{"authType", p.authType},
|
||||
{"resolvedName", p.resolvedName},
|
||||
{"hasMicrosoftSession", !p.refreshToken.isEmpty()}};
|
||||
}
|
||||
|
||||
QVariantMap LauncherBackend::customBuildAt(int index) const
|
||||
@@ -1149,10 +1183,157 @@ void LauncherBackend::launchGame(int profileIndex, int buildIndex)
|
||||
beginAuthentication();
|
||||
}
|
||||
|
||||
void LauncherBackend::startMicrosoftLogin(int profileIndex)
|
||||
{
|
||||
if (!microsoftAvailable()) {
|
||||
emit microsoftLoginFailed(tr("Эта сборка лаунчера собрана без Qt WebEngine — "
|
||||
"показать окно входа Microsoft нечем"));
|
||||
return;
|
||||
}
|
||||
// -1 из QML означает «профиля ещё нет»; внутри держим -2, чтобы отличать
|
||||
// это состояние от закрытого окна.
|
||||
if (profileIndex >= m_profiles.size()) {
|
||||
emit microsoftLoginFailed(tr("Профиль не найден"));
|
||||
return;
|
||||
}
|
||||
m_msaLoginProfile = profileIndex < 0 ? -2 : profileIndex;
|
||||
emit microsoftLoginUrlReady(MsaAuthService::authorizationUrl().toString());
|
||||
}
|
||||
|
||||
void LauncherBackend::cancelMicrosoftLogin()
|
||||
{
|
||||
m_msaLoginProfile = -1;
|
||||
}
|
||||
|
||||
QVariantMap LauncherBackend::inspectMicrosoftRedirect(const QString &url) const
|
||||
{
|
||||
QString code;
|
||||
QString error;
|
||||
const bool matched = MsaAuthService::matchRedirect(QUrl(url), &code, &error);
|
||||
return {{QStringLiteral("matched"), matched},
|
||||
{QStringLiteral("code"), code},
|
||||
{QStringLiteral("error"), error}};
|
||||
}
|
||||
|
||||
void LauncherBackend::finishMicrosoftLogin(const QString &code)
|
||||
{
|
||||
if (m_msaLoginProfile == -1) {
|
||||
emit microsoftLoginFailed(tr("Вход в Microsoft уже отменён"));
|
||||
return;
|
||||
}
|
||||
|
||||
setBusy(true);
|
||||
m_msa->loginWithCode(code, [this](const AuthResult &result) {
|
||||
setBusy(false);
|
||||
const int target = m_msaLoginProfile;
|
||||
m_msaLoginProfile = -1;
|
||||
if (target == -1)
|
||||
return; // окно закрыли, пока шла авторизация
|
||||
|
||||
if (!result.ok) {
|
||||
emit microsoftLoginFailed(result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (target == -2) {
|
||||
// Профиля ещё нет — заводим его на ник из аккаунта.
|
||||
Profile profile;
|
||||
profile.id = m_nextProfileId++;
|
||||
profile.name = result.playerName;
|
||||
profile.authType = kMicrosoft;
|
||||
profile.clientToken = AuthService::generateClientToken();
|
||||
applyMicrosoftResult(profile, result);
|
||||
m_profiles.append(profile);
|
||||
} else if (target < m_profiles.size()) {
|
||||
Profile &profile = m_profiles[target];
|
||||
profile.authType = kMicrosoft;
|
||||
applyMicrosoftResult(profile, result);
|
||||
} else {
|
||||
emit microsoftLoginFailed(tr("Профиль был удалён, пока шёл вход"));
|
||||
return;
|
||||
}
|
||||
|
||||
saveProfiles();
|
||||
emit profilesChanged();
|
||||
emit microsoftLoginSucceeded(result.playerName);
|
||||
});
|
||||
}
|
||||
|
||||
void LauncherBackend::applyMicrosoftResult(Profile &profile, const AuthResult &auth)
|
||||
{
|
||||
profile.accessToken = auth.accessToken;
|
||||
// Microsoft выдаёт новый refresh-токен и при продлении, но если в ответе
|
||||
// его не было — прежний остаётся рабочим.
|
||||
if (!auth.refreshToken.isEmpty())
|
||||
profile.refreshToken = auth.refreshToken;
|
||||
if (!auth.uuid.isEmpty())
|
||||
profile.uuid = auth.uuid;
|
||||
if (!auth.playerName.isEmpty())
|
||||
profile.resolvedName = auth.playerName;
|
||||
profile.xuid = auth.xuid;
|
||||
profile.userType = auth.userType;
|
||||
profile.tokenExpiry = auth.expiresAt.isValid()
|
||||
? auth.expiresAt.toUTC().toString(Qt::ISODate)
|
||||
: QString();
|
||||
}
|
||||
|
||||
void LauncherBackend::beginMicrosoftAuthentication()
|
||||
{
|
||||
const Profile &profile = m_profiles[m_pendingProfile];
|
||||
|
||||
if (profile.refreshToken.isEmpty()) {
|
||||
const int index = m_pendingProfile;
|
||||
failLaunch(tr("Профиль «%1» не подключён к аккаунту Microsoft — войдите в него")
|
||||
.arg(profile.name));
|
||||
emit microsoftReloginRequired(index);
|
||||
return;
|
||||
}
|
||||
|
||||
// Сохранённый токен ещё жив — идём в игру, не дёргая Microsoft.
|
||||
const QDateTime expiry = QDateTime::fromString(profile.tokenExpiry, Qt::ISODate);
|
||||
if (!profile.accessToken.isEmpty() && expiry.isValid()
|
||||
&& QDateTime::currentDateTimeUtc().addSecs(kTokenRenewMarginSecs) < expiry) {
|
||||
AuthResult stored;
|
||||
stored.ok = true;
|
||||
stored.playerName = profile.resolvedName;
|
||||
stored.uuid = profile.uuid;
|
||||
stored.accessToken = profile.accessToken;
|
||||
stored.refreshToken = profile.refreshToken;
|
||||
stored.clientToken = MsaAuthService::clientId();
|
||||
stored.xuid = profile.xuid;
|
||||
stored.userType = profile.userType.isEmpty() ? QStringLiteral("msa") : profile.userType;
|
||||
stored.expiresAt = expiry;
|
||||
continueLaunch(stored);
|
||||
return;
|
||||
}
|
||||
|
||||
m_msa->loginWithRefreshToken(profile.refreshToken, [this](const AuthResult &result) {
|
||||
if (m_pendingProfile < 0)
|
||||
return; // запуск отменили, пока шло продление
|
||||
if (result.licenseMissing) {
|
||||
failLaunch(result.error);
|
||||
return;
|
||||
}
|
||||
if (!result.ok) {
|
||||
const int index = m_pendingProfile;
|
||||
failLaunch(tr("Сессия Microsoft истекла — войдите в аккаунт заново (%1)")
|
||||
.arg(result.error));
|
||||
emit microsoftReloginRequired(index);
|
||||
return;
|
||||
}
|
||||
continueLaunch(result);
|
||||
});
|
||||
}
|
||||
|
||||
void LauncherBackend::beginAuthentication()
|
||||
{
|
||||
const Profile &profile = m_profiles[m_pendingProfile];
|
||||
|
||||
if (profile.authType == kMicrosoft) {
|
||||
beginMicrosoftAuthentication();
|
||||
return;
|
||||
}
|
||||
|
||||
if (profile.authType != kElyBy) {
|
||||
const QString nickname = profile.login.isEmpty() ? profile.name : profile.login;
|
||||
const AuthResult result = AuthService::offline(nickname);
|
||||
@@ -1252,7 +1433,10 @@ void LauncherBackend::continueLaunch(const AuthResult &auth)
|
||||
|
||||
// Ely.by возвращает игровой ник и UUID аккаунта — их и запоминаем,
|
||||
// чтобы в следующий раз обойтись без пароля.
|
||||
if (!auth.accessToken.isEmpty() && profile.authType == kElyBy) {
|
||||
if (profile.authType == kMicrosoft && auth.ok) {
|
||||
applyMicrosoftResult(profile, auth);
|
||||
saveProfiles();
|
||||
} else if (!auth.accessToken.isEmpty() && profile.authType == kElyBy) {
|
||||
profile.accessToken = auth.accessToken;
|
||||
profile.clientToken = auth.clientToken;
|
||||
if (!auth.uuid.isEmpty())
|
||||
@@ -1273,6 +1457,7 @@ void LauncherBackend::continueLaunch(const AuthResult &auth)
|
||||
options.accessToken = auth.accessToken;
|
||||
options.clientToken = auth.clientToken;
|
||||
options.userType = auth.userType;
|
||||
options.xuid = auth.xuid;
|
||||
|
||||
// Скачанная лаунчером сборка важнее пути из настроек: её выбрали явно в
|
||||
// окне выбора версии Java.
|
||||
@@ -1354,6 +1539,15 @@ bool LauncherBackend::busy() const
|
||||
|| m_packDownloader->isRunning();
|
||||
}
|
||||
|
||||
bool LauncherBackend::microsoftAvailable() const
|
||||
{
|
||||
#ifdef LAUNCHER_HAS_WEBENGINE
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void LauncherBackend::setBusy(bool busy)
|
||||
{
|
||||
if (m_busy == busy)
|
||||
@@ -2195,6 +2389,9 @@ void LauncherBackend::loadData()
|
||||
profile.uuid = o.value("uuid").toString();
|
||||
profile.resolvedName = o.value("resolvedName").toString();
|
||||
profile.userType = o.value("userType").toString();
|
||||
profile.refreshToken = o.value("refreshToken").toString();
|
||||
profile.xuid = o.value("xuid").toString();
|
||||
profile.tokenExpiry = o.value("tokenExpiry").toString();
|
||||
if (profile.clientToken.isEmpty())
|
||||
profile.clientToken = AuthService::generateClientToken();
|
||||
m_profiles.append(profile);
|
||||
@@ -2308,7 +2505,10 @@ void LauncherBackend::saveProfiles()
|
||||
{"accessToken", p.accessToken},
|
||||
{"uuid", p.uuid},
|
||||
{"resolvedName", p.resolvedName},
|
||||
{"userType", p.userType}});
|
||||
{"userType", p.userType},
|
||||
{"refreshToken", p.refreshToken},
|
||||
{"xuid", p.xuid},
|
||||
{"tokenExpiry", p.tokenExpiry}});
|
||||
|
||||
writeJsonFile(LauncherPaths::profilesFile(), QJsonDocument(arr));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user