GH-3392 dirty initial MSA support that shares logic with Mojang flows
Both act as the first step of AuthContext.
This commit is contained in:
parent
fca2e9e44c
commit
3a53349e33
@ -90,6 +90,9 @@ set(MultiMC_DISCORD_URL "" CACHE STRING "URL for the Discord guild.")
|
|||||||
# Subreddit URL
|
# Subreddit URL
|
||||||
set(MultiMC_SUBREDDIT_URL "" CACHE STRING "URL for the subreddit.")
|
set(MultiMC_SUBREDDIT_URL "" CACHE STRING "URL for the subreddit.")
|
||||||
|
|
||||||
|
# MSA Client ID
|
||||||
|
set(MultiMC_MSA_CLIENT_ID "" CACHE STRING "Client ID used for MSA authentication")
|
||||||
|
|
||||||
#### Check the current Git commit and branch
|
#### Check the current Git commit and branch
|
||||||
include(GetGitRevisionDescription)
|
include(GetGitRevisionDescription)
|
||||||
get_git_head_revision(MultiMC_GIT_REFSPEC MultiMC_GIT_COMMIT)
|
get_git_head_revision(MultiMC_GIT_REFSPEC MultiMC_GIT_COMMIT)
|
||||||
|
@ -35,6 +35,7 @@ Config::Config()
|
|||||||
PASTE_EE_KEY = "@MultiMC_PASTE_EE_API_KEY@";
|
PASTE_EE_KEY = "@MultiMC_PASTE_EE_API_KEY@";
|
||||||
IMGUR_CLIENT_ID = "@MultiMC_IMGUR_CLIENT_ID@";
|
IMGUR_CLIENT_ID = "@MultiMC_IMGUR_CLIENT_ID@";
|
||||||
META_URL = "@MultiMC_META_URL@";
|
META_URL = "@MultiMC_META_URL@";
|
||||||
|
MSA_CLIENT_ID = "@MultiMC_MSA_CLIENT_ID@";
|
||||||
|
|
||||||
BUG_TRACKER_URL = "@MultiMC_BUG_TRACKER_URL@";
|
BUG_TRACKER_URL = "@MultiMC_BUG_TRACKER_URL@";
|
||||||
DISCORD_URL = "@MultiMC_DISCORD_URL@";
|
DISCORD_URL = "@MultiMC_DISCORD_URL@";
|
||||||
|
@ -75,13 +75,17 @@ public:
|
|||||||
*/
|
*/
|
||||||
QString META_URL;
|
QString META_URL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MSA client ID - registered with Azure / Microsoft, needs correct setup on MS side.
|
||||||
|
*/
|
||||||
|
QString MSA_CLIENT_ID;
|
||||||
|
|
||||||
QString BUG_TRACKER_URL;
|
QString BUG_TRACKER_URL;
|
||||||
QString DISCORD_URL;
|
QString DISCORD_URL;
|
||||||
QString SUBREDDIT_URL;
|
QString SUBREDDIT_URL;
|
||||||
|
|
||||||
QString RESOURCE_BASE = "https://resources.download.minecraft.net/";
|
QString RESOURCE_BASE = "https://resources.download.minecraft.net/";
|
||||||
QString LIBRARY_BASE = "https://libraries.minecraft.net/";
|
QString LIBRARY_BASE = "https://libraries.minecraft.net/";
|
||||||
QString SKINS_BASE = "https://crafatar.com/skins/";
|
|
||||||
QString AUTH_BASE = "https://authserver.mojang.com/";
|
QString AUTH_BASE = "https://authserver.mojang.com/";
|
||||||
QString MOJANG_STATUS_URL = "https://status.mojang.com/check";
|
QString MOJANG_STATUS_URL = "https://status.mojang.com/check";
|
||||||
QString IMGUR_BASE_URL = "https://api.imgur.com/3/";
|
QString IMGUR_BASE_URL = "https://api.imgur.com/3/";
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
#include "settings/INIFile.h"
|
#include "settings/INIFile.h"
|
||||||
#include "BaseVersionList.h"
|
#include "BaseVersionList.h"
|
||||||
#include "minecraft/auth/MojangAccount.h"
|
#include "minecraft/auth/MinecraftAccount.h"
|
||||||
#include "MessageLevel.h"
|
#include "MessageLevel.h"
|
||||||
#include "pathmatcher/IPathMatcher.h"
|
#include "pathmatcher/IPathMatcher.h"
|
||||||
|
|
||||||
|
@ -203,20 +203,31 @@ set(STATUS_SOURCES
|
|||||||
# Support for Minecraft instances and launch
|
# Support for Minecraft instances and launch
|
||||||
set(MINECRAFT_SOURCES
|
set(MINECRAFT_SOURCES
|
||||||
# Minecraft support
|
# Minecraft support
|
||||||
|
minecraft/auth/AccountData.h
|
||||||
|
minecraft/auth/AccountData.cpp
|
||||||
|
minecraft/auth/AccountTask.h
|
||||||
|
minecraft/auth/AccountTask.cpp
|
||||||
minecraft/auth/AuthSession.h
|
minecraft/auth/AuthSession.h
|
||||||
minecraft/auth/AuthSession.cpp
|
minecraft/auth/AuthSession.cpp
|
||||||
minecraft/auth/MojangAccountList.h
|
minecraft/auth/AccountList.h
|
||||||
minecraft/auth/MojangAccountList.cpp
|
minecraft/auth/AccountList.cpp
|
||||||
minecraft/auth/MojangAccount.h
|
minecraft/auth/MinecraftAccount.h
|
||||||
minecraft/auth/MojangAccount.cpp
|
minecraft/auth/MinecraftAccount.cpp
|
||||||
minecraft/auth/YggdrasilTask.h
|
minecraft/auth/flows/AuthContext.h
|
||||||
minecraft/auth/YggdrasilTask.cpp
|
minecraft/auth/flows/AuthContext.cpp
|
||||||
minecraft/auth/flows/AuthenticateTask.h
|
|
||||||
minecraft/auth/flows/AuthenticateTask.cpp
|
minecraft/auth/flows/MSAInteractive.h
|
||||||
minecraft/auth/flows/RefreshTask.cpp
|
minecraft/auth/flows/MSAInteractive.cpp
|
||||||
minecraft/auth/flows/RefreshTask.cpp
|
minecraft/auth/flows/MSASilent.h
|
||||||
minecraft/auth/flows/ValidateTask.h
|
minecraft/auth/flows/MSASilent.cpp
|
||||||
minecraft/auth/flows/ValidateTask.cpp
|
|
||||||
|
minecraft/auth/flows/MojangLogin.h
|
||||||
|
minecraft/auth/flows/MojangLogin.cpp
|
||||||
|
minecraft/auth/flows/MojangRefresh.h
|
||||||
|
minecraft/auth/flows/MojangRefresh.cpp
|
||||||
|
|
||||||
|
minecraft/auth/flows/Yggdrasil.h
|
||||||
|
minecraft/auth/flows/Yggdrasil.cpp
|
||||||
|
|
||||||
minecraft/gameoptions/GameOptions.h
|
minecraft/gameoptions/GameOptions.h
|
||||||
minecraft/gameoptions/GameOptions.cpp
|
minecraft/gameoptions/GameOptions.cpp
|
||||||
@ -732,6 +743,8 @@ SET(MULTIMC_SOURCES
|
|||||||
dialogs/IconPickerDialog.h
|
dialogs/IconPickerDialog.h
|
||||||
dialogs/LoginDialog.cpp
|
dialogs/LoginDialog.cpp
|
||||||
dialogs/LoginDialog.h
|
dialogs/LoginDialog.h
|
||||||
|
dialogs/MSALoginDialog.cpp
|
||||||
|
dialogs/MSALoginDialog.h
|
||||||
dialogs/NewComponentDialog.cpp
|
dialogs/NewComponentDialog.cpp
|
||||||
dialogs/NewComponentDialog.h
|
dialogs/NewComponentDialog.h
|
||||||
dialogs/NewInstanceDialog.cpp
|
dialogs/NewInstanceDialog.cpp
|
||||||
@ -850,6 +863,7 @@ SET(MULTIMC_UIS
|
|||||||
dialogs/EditAccountDialog.ui
|
dialogs/EditAccountDialog.ui
|
||||||
dialogs/ExportInstanceDialog.ui
|
dialogs/ExportInstanceDialog.ui
|
||||||
dialogs/LoginDialog.ui
|
dialogs/LoginDialog.ui
|
||||||
|
dialogs/MSALoginDialog.ui
|
||||||
dialogs/UpdateDialog.ui
|
dialogs/UpdateDialog.ui
|
||||||
dialogs/NotificationDialog.ui
|
dialogs/NotificationDialog.ui
|
||||||
dialogs/SkinUploadDialog.ui
|
dialogs/SkinUploadDialog.ui
|
||||||
@ -892,6 +906,7 @@ target_link_libraries(MultiMC_logic
|
|||||||
optional-bare
|
optional-bare
|
||||||
tomlc99
|
tomlc99
|
||||||
BuildConfig
|
BuildConfig
|
||||||
|
Katabasis
|
||||||
)
|
)
|
||||||
target_link_libraries(MultiMC_logic
|
target_link_libraries(MultiMC_logic
|
||||||
Qt5::Core
|
Qt5::Core
|
||||||
|
@ -101,7 +101,6 @@ void Env::initHttpMetaCache()
|
|||||||
m_metacache->addBase("ModpacksCHPacks", QDir("cache/ModpacksCHPacks").absolutePath());
|
m_metacache->addBase("ModpacksCHPacks", QDir("cache/ModpacksCHPacks").absolutePath());
|
||||||
m_metacache->addBase("TechnicPacks", QDir("cache/TechnicPacks").absolutePath());
|
m_metacache->addBase("TechnicPacks", QDir("cache/TechnicPacks").absolutePath());
|
||||||
m_metacache->addBase("FlamePacks", QDir("cache/FlamePacks").absolutePath());
|
m_metacache->addBase("FlamePacks", QDir("cache/FlamePacks").absolutePath());
|
||||||
m_metacache->addBase("skins", QDir("accounts/skins").absolutePath());
|
|
||||||
m_metacache->addBase("root", QDir::currentPath());
|
m_metacache->addBase("root", QDir::currentPath());
|
||||||
m_metacache->addBase("translations", QDir("translations").absolutePath());
|
m_metacache->addBase("translations", QDir("translations").absolutePath());
|
||||||
m_metacache->addBase("icons", QDir("cache/icons").absolutePath());
|
m_metacache->addBase("icons", QDir("cache/icons").absolutePath());
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
#include "LaunchController.h"
|
#include "LaunchController.h"
|
||||||
#include "MainWindow.h"
|
#include "MainWindow.h"
|
||||||
#include <minecraft/auth/MojangAccountList.h>
|
#include <minecraft/auth/AccountList.h>
|
||||||
#include "MultiMC.h"
|
#include "MultiMC.h"
|
||||||
#include "dialogs/CustomMessageBox.h"
|
#include "dialogs/CustomMessageBox.h"
|
||||||
#include "dialogs/ProfileSelectDialog.h"
|
#include "dialogs/ProfileSelectDialog.h"
|
||||||
@ -12,7 +12,7 @@
|
|||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
#include <QInputDialog>
|
#include <QInputDialog>
|
||||||
#include <tasks/Task.h>
|
#include <tasks/Task.h>
|
||||||
#include <minecraft/auth/YggdrasilTask.h>
|
#include <minecraft/auth/AccountTask.h>
|
||||||
#include <launch/steps/TextPrint.h>
|
#include <launch/steps/TextPrint.h>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
#include <QHostInfo>
|
#include <QHostInfo>
|
||||||
@ -35,22 +35,23 @@ void LaunchController::executeTask()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: minecraft specific
|
// FIXME: minecraft specific
|
||||||
void LaunchController::login()
|
void LaunchController::login() {
|
||||||
{
|
|
||||||
JavaCommon::checkJVMArgs(m_instance->settings()->get("JvmArgs").toString(), m_parentWidget);
|
JavaCommon::checkJVMArgs(m_instance->settings()->get("JvmArgs").toString(), m_parentWidget);
|
||||||
|
|
||||||
// Find an account to use.
|
// Find an account to use.
|
||||||
std::shared_ptr<MojangAccountList> accounts = MMC->accounts();
|
std::shared_ptr<AccountList> accounts = MMC->accounts();
|
||||||
MojangAccountPtr account = accounts->activeAccount();
|
|
||||||
if (accounts->count() <= 0)
|
if (accounts->count() <= 0)
|
||||||
{
|
{
|
||||||
// Tell the user they need to log in at least one account in order to play.
|
// Tell the user they need to log in at least one account in order to play.
|
||||||
auto reply = CustomMessageBox::selectable(
|
auto reply = CustomMessageBox::selectable(
|
||||||
m_parentWidget, tr("No Accounts"),
|
m_parentWidget,
|
||||||
|
tr("No Accounts"),
|
||||||
tr("In order to play Minecraft, you must have at least one Mojang or Minecraft "
|
tr("In order to play Minecraft, you must have at least one Mojang or Minecraft "
|
||||||
"account logged in to MultiMC."
|
"account logged in to MultiMC."
|
||||||
"Would you like to open the account manager to add an account now?"),
|
"Would you like to open the account manager to add an account now?"),
|
||||||
QMessageBox::Information, QMessageBox::Yes | QMessageBox::No)->exec();
|
QMessageBox::Information,
|
||||||
|
QMessageBox::Yes | QMessageBox::No
|
||||||
|
)->exec();
|
||||||
|
|
||||||
if (reply == QMessageBox::Yes)
|
if (reply == QMessageBox::Yes)
|
||||||
{
|
{
|
||||||
@ -58,11 +59,16 @@ void LaunchController::login()
|
|||||||
MMC->ShowGlobalSettings(m_parentWidget, "accounts");
|
MMC->ShowGlobalSettings(m_parentWidget, "accounts");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (account.get() == nullptr)
|
|
||||||
|
MinecraftAccountPtr account = accounts->activeAccount();
|
||||||
|
if (account.get() == nullptr)
|
||||||
{
|
{
|
||||||
// If no default account is set, ask the user which one to use.
|
// If no default account is set, ask the user which one to use.
|
||||||
ProfileSelectDialog selectDialog(tr("Which profile would you like to use?"),
|
ProfileSelectDialog selectDialog(
|
||||||
ProfileSelectDialog::GlobalDefaultCheckbox, m_parentWidget);
|
tr("Which account would you like to use?"),
|
||||||
|
ProfileSelectDialog::GlobalDefaultCheckbox,
|
||||||
|
m_parentWidget
|
||||||
|
);
|
||||||
|
|
||||||
selectDialog.exec();
|
selectDialog.exec();
|
||||||
|
|
||||||
@ -70,8 +76,9 @@ void LaunchController::login()
|
|||||||
account = selectDialog.selectedAccount();
|
account = selectDialog.selectedAccount();
|
||||||
|
|
||||||
// If the user said to use the account as default, do that.
|
// If the user said to use the account as default, do that.
|
||||||
if (selectDialog.useAsGlobalDefault() && account.get() != nullptr)
|
if (selectDialog.useAsGlobalDefault() && account.get() != nullptr) {
|
||||||
accounts->setActiveAccount(account->username());
|
accounts->setActiveAccount(account->profileId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if no account is selected, we bail
|
// if no account is selected, we bail
|
||||||
@ -93,7 +100,13 @@ void LaunchController::login()
|
|||||||
{
|
{
|
||||||
m_session = std::make_shared<AuthSession>();
|
m_session = std::make_shared<AuthSession>();
|
||||||
m_session->wants_online = m_online;
|
m_session->wants_online = m_online;
|
||||||
auto task = account->login(m_session, password);
|
std::shared_ptr<AccountTask> task;
|
||||||
|
if(!password.isNull()) {
|
||||||
|
task = account->login(m_session, password);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
task = account->refresh(m_session);
|
||||||
|
}
|
||||||
if (task)
|
if (task)
|
||||||
{
|
{
|
||||||
// We'll need to validate the access token to make sure the account
|
// We'll need to validate the access token to make sure the account
|
||||||
@ -107,9 +120,9 @@ void LaunchController::login()
|
|||||||
if (!task->wasSuccessful())
|
if (!task->wasSuccessful())
|
||||||
{
|
{
|
||||||
auto failReasonNew = task->failReason();
|
auto failReasonNew = task->failReason();
|
||||||
if(failReasonNew == "Invalid token.")
|
if(failReasonNew == "Invalid token." || failReasonNew == "Invalid Signature")
|
||||||
{
|
{
|
||||||
account->invalidateClientToken();
|
// account->invalidateClientToken();
|
||||||
failReason = needLoginAgain;
|
failReason = needLoginAgain;
|
||||||
}
|
}
|
||||||
else failReason = failReasonNew;
|
else failReason = failReasonNew;
|
||||||
@ -117,15 +130,14 @@ void LaunchController::login()
|
|||||||
}
|
}
|
||||||
switch (m_session->status)
|
switch (m_session->status)
|
||||||
{
|
{
|
||||||
case AuthSession::Undetermined:
|
case AuthSession::Undetermined: {
|
||||||
{
|
|
||||||
qCritical() << "Received undetermined session status during login. Bye.";
|
qCritical() << "Received undetermined session status during login. Bye.";
|
||||||
tryagain = false;
|
tryagain = false;
|
||||||
emitFailed(tr("Received undetermined session status during login."));
|
emitFailed(tr("Received undetermined session status during login."));
|
||||||
break;
|
return;
|
||||||
}
|
}
|
||||||
case AuthSession::RequiresPassword:
|
case AuthSession::RequiresPassword: {
|
||||||
{
|
// FIXME: this needs to understand MSA
|
||||||
EditAccountDialog passDialog(failReason, m_parentWidget, EditAccountDialog::PasswordField);
|
EditAccountDialog passDialog(failReason, m_parentWidget, EditAccountDialog::PasswordField);
|
||||||
auto username = m_session->username;
|
auto username = m_session->username;
|
||||||
auto chopN = [](QString toChop, int N) -> QString
|
auto chopN = [](QString toChop, int N) -> QString
|
||||||
@ -154,17 +166,28 @@ void LaunchController::login()
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
tryagain = false;
|
tryagain = false;
|
||||||
|
emitFailed(tr("Received undetermined session status during login."));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case AuthSession::PlayableOffline:
|
case AuthSession::RequiresOAuth: {
|
||||||
{
|
// FIXME: add UI for expired / broken MS accounts
|
||||||
|
tryagain = false;
|
||||||
|
emitFailed(tr("Microsoft account has expired and needs to be logged into again."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case AuthSession::PlayableOffline: {
|
||||||
// we ask the user for a player name
|
// we ask the user for a player name
|
||||||
bool ok = false;
|
bool ok = false;
|
||||||
QString usedname = m_session->player_name;
|
QString usedname = m_session->player_name;
|
||||||
QString name = QInputDialog::getText(m_parentWidget, tr("Player name"),
|
QString name = QInputDialog::getText(
|
||||||
|
m_parentWidget,
|
||||||
|
tr("Player name"),
|
||||||
tr("Choose your offline mode player name."),
|
tr("Choose your offline mode player name."),
|
||||||
QLineEdit::Normal, m_session->player_name, &ok);
|
QLineEdit::Normal,
|
||||||
|
m_session->player_name,
|
||||||
|
&ok
|
||||||
|
);
|
||||||
if (!ok)
|
if (!ok)
|
||||||
{
|
{
|
||||||
tryagain = false;
|
tryagain = false;
|
||||||
|
@ -54,7 +54,7 @@
|
|||||||
#include <java/JavaUtils.h>
|
#include <java/JavaUtils.h>
|
||||||
#include <java/JavaInstallList.h>
|
#include <java/JavaInstallList.h>
|
||||||
#include <launch/LaunchTask.h>
|
#include <launch/LaunchTask.h>
|
||||||
#include <minecraft/auth/MojangAccountList.h>
|
#include <minecraft/auth/AccountList.h>
|
||||||
#include <SkinUtils.h>
|
#include <SkinUtils.h>
|
||||||
#include <BuildConfig.h>
|
#include <BuildConfig.h>
|
||||||
#include <net/NetJob.h>
|
#include <net/NetJob.h>
|
||||||
@ -90,6 +90,20 @@
|
|||||||
#include "KonamiCode.h"
|
#include "KonamiCode.h"
|
||||||
#include <InstanceCopyTask.h>
|
#include <InstanceCopyTask.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
QString profileInUseFilter(const QString & profile, bool used)
|
||||||
|
{
|
||||||
|
if(used)
|
||||||
|
{
|
||||||
|
return QObject::tr("%1 (in use)").arg(profile);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WHY: to hold the pre-translation strings together with the T pointer, so it can be retranslated without a lot of ugly code
|
// WHY: to hold the pre-translation strings together with the T pointer, so it can be retranslated without a lot of ugly code
|
||||||
template <typename T>
|
template <typename T>
|
||||||
class Translated
|
class Translated
|
||||||
@ -753,49 +767,27 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow
|
|||||||
// Update the menu when the active account changes.
|
// Update the menu when the active account changes.
|
||||||
// Shouldn't have to use lambdas here like this, but if I don't, the compiler throws a fit.
|
// Shouldn't have to use lambdas here like this, but if I don't, the compiler throws a fit.
|
||||||
// Template hell sucks...
|
// Template hell sucks...
|
||||||
connect(MMC->accounts().get(), &MojangAccountList::activeAccountChanged, [this]
|
connect(
|
||||||
{
|
MMC->accounts().get(),
|
||||||
|
&AccountList::activeAccountChanged,
|
||||||
|
[this] {
|
||||||
activeAccountChanged();
|
activeAccountChanged();
|
||||||
});
|
}
|
||||||
connect(MMC->accounts().get(), &MojangAccountList::listChanged, [this]
|
);
|
||||||
|
connect(
|
||||||
|
MMC->accounts().get(),
|
||||||
|
&AccountList::listChanged,
|
||||||
|
[this]
|
||||||
{
|
{
|
||||||
repopulateAccountsMenu();
|
repopulateAccountsMenu();
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Show initial account
|
// Show initial account
|
||||||
activeAccountChanged();
|
activeAccountChanged();
|
||||||
|
|
||||||
auto accounts = MMC->accounts();
|
// TODO: refresh accounts here?
|
||||||
|
// auto accounts = MMC->accounts();
|
||||||
QList<Net::Download::Ptr> skin_dls;
|
|
||||||
for (int i = 0; i < accounts->count(); i++)
|
|
||||||
{
|
|
||||||
auto account = accounts->at(i);
|
|
||||||
if (!account)
|
|
||||||
{
|
|
||||||
qWarning() << "Null account at index" << i;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (auto profile : account->profiles())
|
|
||||||
{
|
|
||||||
auto meta = Env::getInstance().metacache()->resolveEntry("skins", profile.id + ".png");
|
|
||||||
auto action = Net::Download::makeCached(QUrl(BuildConfig.SKINS_BASE + profile.id + ".png"), meta);
|
|
||||||
skin_dls.append(action);
|
|
||||||
meta->setStale(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!skin_dls.isEmpty())
|
|
||||||
{
|
|
||||||
auto job = new NetJob("Startup player skins download");
|
|
||||||
connect(job, &NetJob::succeeded, this, &MainWindow::skinJobFinished);
|
|
||||||
connect(job, &NetJob::failed, this, &MainWindow::skinJobFinished);
|
|
||||||
for (auto action : skin_dls)
|
|
||||||
{
|
|
||||||
job->addNetAction(action);
|
|
||||||
}
|
|
||||||
skin_download_job.reset(job);
|
|
||||||
job->start();
|
|
||||||
}
|
|
||||||
|
|
||||||
// load the news
|
// load the news
|
||||||
{
|
{
|
||||||
@ -844,7 +836,15 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow
|
|||||||
|
|
||||||
void MainWindow::retranslateUi()
|
void MainWindow::retranslateUi()
|
||||||
{
|
{
|
||||||
|
std::shared_ptr<AccountList> accounts = MMC->accounts();
|
||||||
|
MinecraftAccountPtr active_account = accounts->activeAccount();
|
||||||
|
if(active_account) {
|
||||||
|
auto profileLabel = profileInUseFilter(active_account->profileName(), active_account->isInUse());
|
||||||
|
accountMenuButton->setText(profileLabel);
|
||||||
|
}
|
||||||
|
else {
|
||||||
accountMenuButton->setText(tr("Profiles"));
|
accountMenuButton->setText(tr("Profiles"));
|
||||||
|
}
|
||||||
|
|
||||||
if (m_selectedInstance) {
|
if (m_selectedInstance) {
|
||||||
m_statusLeft->setText(m_selectedInstance->getStatusbarDescription());
|
m_statusLeft->setText(m_selectedInstance->getStatusbarDescription());
|
||||||
@ -872,12 +872,6 @@ void MainWindow::konamiTriggered()
|
|||||||
qDebug() << "Super Secret Mode ACTIVATED!";
|
qDebug() << "Super Secret Mode ACTIVATED!";
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::skinJobFinished()
|
|
||||||
{
|
|
||||||
activeAccountChanged();
|
|
||||||
skin_download_job.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::showInstanceContextMenu(const QPoint &pos)
|
void MainWindow::showInstanceContextMenu(const QPoint &pos)
|
||||||
{
|
{
|
||||||
QList<QAction *> actions;
|
QList<QAction *> actions;
|
||||||
@ -1018,34 +1012,21 @@ void MainWindow::updateToolsMenu()
|
|||||||
ui->actionLaunchInstanceOffline->setMenu(launchOfflineMenu);
|
ui->actionLaunchInstanceOffline->setMenu(launchOfflineMenu);
|
||||||
}
|
}
|
||||||
|
|
||||||
QString profileInUseFilter(const QString & profile, bool used)
|
|
||||||
{
|
|
||||||
if(used)
|
|
||||||
{
|
|
||||||
return profile + QObject::tr(" (in use)");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return profile;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::repopulateAccountsMenu()
|
void MainWindow::repopulateAccountsMenu()
|
||||||
{
|
{
|
||||||
accountMenu->clear();
|
accountMenu->clear();
|
||||||
|
|
||||||
std::shared_ptr<MojangAccountList> accounts = MMC->accounts();
|
std::shared_ptr<AccountList> accounts = MMC->accounts();
|
||||||
MojangAccountPtr active_account = accounts->activeAccount();
|
MinecraftAccountPtr active_account = accounts->activeAccount();
|
||||||
|
|
||||||
QString active_username = "";
|
QString active_profileId = "";
|
||||||
if (active_account != nullptr)
|
if (active_account != nullptr)
|
||||||
{
|
{
|
||||||
active_username = active_account->username();
|
active_profileId = active_account->profileId();
|
||||||
const AccountProfile *profile = active_account->currentProfile();
|
|
||||||
// this can be called before accountMenuButton exists
|
// this can be called before accountMenuButton exists
|
||||||
if (profile != nullptr && accountMenuButton)
|
if (accountMenuButton)
|
||||||
{
|
{
|
||||||
auto profileLabel = profileInUseFilter(profile->name, active_account->isInUse());
|
auto profileLabel = profileInUseFilter(active_account->profileName(), active_account->isInUse());
|
||||||
accountMenuButton->setText(profileLabel);
|
accountMenuButton->setText(profileLabel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1061,24 +1042,21 @@ void MainWindow::repopulateAccountsMenu()
|
|||||||
// TODO: Nicer way to iterate?
|
// TODO: Nicer way to iterate?
|
||||||
for (int i = 0; i < accounts->count(); i++)
|
for (int i = 0; i < accounts->count(); i++)
|
||||||
{
|
{
|
||||||
MojangAccountPtr account = accounts->at(i);
|
MinecraftAccountPtr account = accounts->at(i);
|
||||||
for (auto profile : account->profiles())
|
auto profileLabel = profileInUseFilter(account->profileName(), account->isInUse());
|
||||||
{
|
|
||||||
auto profileLabel = profileInUseFilter(profile.name, account->isInUse());
|
|
||||||
QAction *action = new QAction(profileLabel, this);
|
QAction *action = new QAction(profileLabel, this);
|
||||||
action->setData(account->username());
|
action->setData(account->profileId());
|
||||||
action->setCheckable(true);
|
action->setCheckable(true);
|
||||||
if (active_username == account->username())
|
if (active_profileId == account->profileId())
|
||||||
{
|
{
|
||||||
action->setChecked(true);
|
action->setChecked(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
action->setIcon(SkinUtils::getFaceFromCache(profile.id));
|
action->setIcon(account->getFace());
|
||||||
accountMenu->addAction(action);
|
accountMenu->addAction(action);
|
||||||
connect(action, SIGNAL(triggered(bool)), SLOT(changeActiveAccount()));
|
connect(action, SIGNAL(triggered(bool)), SLOT(changeActiveAccount()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
accountMenu->addSeparator();
|
accountMenu->addSeparator();
|
||||||
|
|
||||||
@ -1086,8 +1064,7 @@ void MainWindow::repopulateAccountsMenu()
|
|||||||
action->setCheckable(true);
|
action->setCheckable(true);
|
||||||
action->setIcon(MMC->getThemedIcon("noaccount"));
|
action->setIcon(MMC->getThemedIcon("noaccount"));
|
||||||
action->setData("");
|
action->setData("");
|
||||||
if (active_username.isEmpty())
|
if (active_profileId.isEmpty()) {
|
||||||
{
|
|
||||||
action->setChecked(true);
|
action->setChecked(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1134,19 +1111,16 @@ void MainWindow::activeAccountChanged()
|
|||||||
{
|
{
|
||||||
repopulateAccountsMenu();
|
repopulateAccountsMenu();
|
||||||
|
|
||||||
MojangAccountPtr account = MMC->accounts()->activeAccount();
|
MinecraftAccountPtr account = MMC->accounts()->activeAccount();
|
||||||
|
|
||||||
if (account != nullptr && account->username() != "")
|
// FIXME: this needs adjustment for MSA
|
||||||
|
if (account != nullptr && account->profileName() != "")
|
||||||
{
|
{
|
||||||
const AccountProfile *profile = account->currentProfile();
|
auto profileLabel = profileInUseFilter(account->profileName(), account->isInUse());
|
||||||
if (profile != nullptr)
|
|
||||||
{
|
|
||||||
auto profileLabel = profileInUseFilter(profile->name, account->isInUse());
|
|
||||||
accountMenuButton->setIcon(SkinUtils::getFaceFromCache(profile->id));
|
|
||||||
accountMenuButton->setText(profileLabel);
|
accountMenuButton->setText(profileLabel);
|
||||||
|
accountMenuButton->setIcon(account->getFace());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Set the icon to the "no account" icon.
|
// Set the icon to the "no account" icon.
|
||||||
accountMenuButton->setIcon(MMC->getThemedIcon("noaccount"));
|
accountMenuButton->setIcon(MMC->getThemedIcon("noaccount"));
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
|
||||||
#include "BaseInstance.h"
|
#include "BaseInstance.h"
|
||||||
#include "minecraft/auth/MojangAccount.h"
|
#include "minecraft/auth/MinecraftAccount.h"
|
||||||
#include "net/NetJob.h"
|
#include "net/NetJob.h"
|
||||||
#include "updater/GoUpdate.h"
|
#include "updater/GoUpdate.h"
|
||||||
|
|
||||||
@ -149,8 +149,6 @@ private slots:
|
|||||||
|
|
||||||
void updateToolsMenu();
|
void updateToolsMenu();
|
||||||
|
|
||||||
void skinJobFinished();
|
|
||||||
|
|
||||||
void instanceActivated(QModelIndex);
|
void instanceActivated(QModelIndex);
|
||||||
|
|
||||||
void instanceChanged(const QModelIndex ¤t, const QModelIndex &previous);
|
void instanceChanged(const QModelIndex ¤t, const QModelIndex &previous);
|
||||||
@ -214,7 +212,6 @@ private:
|
|||||||
QToolButton *accountMenuButton = nullptr;
|
QToolButton *accountMenuButton = nullptr;
|
||||||
KonamiCode * secretEventFilter = nullptr;
|
KonamiCode * secretEventFilter = nullptr;
|
||||||
|
|
||||||
unique_qobject_ptr<NetJob> skin_download_job;
|
|
||||||
unique_qobject_ptr<NewsChecker> m_newsChecker;
|
unique_qobject_ptr<NewsChecker> m_newsChecker;
|
||||||
unique_qobject_ptr<NotificationChecker> m_notificationChecker;
|
unique_qobject_ptr<NotificationChecker> m_notificationChecker;
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@
|
|||||||
#include "dialogs/CustomMessageBox.h"
|
#include "dialogs/CustomMessageBox.h"
|
||||||
#include "InstanceList.h"
|
#include "InstanceList.h"
|
||||||
|
|
||||||
#include <minecraft/auth/MojangAccountList.h>
|
#include <minecraft/auth/AccountList.h>
|
||||||
#include "icons/IconList.h"
|
#include "icons/IconList.h"
|
||||||
#include "net/HttpMetaCache.h"
|
#include "net/HttpMetaCache.h"
|
||||||
#include "Env.h"
|
#include "Env.h"
|
||||||
@ -745,7 +745,7 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv)
|
|||||||
|
|
||||||
// and accounts
|
// and accounts
|
||||||
{
|
{
|
||||||
m_accounts.reset(new MojangAccountList(this));
|
m_accounts.reset(new AccountList(this));
|
||||||
qDebug() << "Loading accounts...";
|
qDebug() << "Loading accounts...";
|
||||||
m_accounts->setListFilePath("accounts.json", true);
|
m_accounts->setListFilePath("accounts.json", true);
|
||||||
m_accounts->loadList();
|
m_accounts->loadList();
|
||||||
|
@ -24,7 +24,7 @@ class QFile;
|
|||||||
class HttpMetaCache;
|
class HttpMetaCache;
|
||||||
class SettingsObject;
|
class SettingsObject;
|
||||||
class InstanceList;
|
class InstanceList;
|
||||||
class MojangAccountList;
|
class AccountList;
|
||||||
class IconList;
|
class IconList;
|
||||||
class QNetworkAccessManager;
|
class QNetworkAccessManager;
|
||||||
class JavaInstallList;
|
class JavaInstallList;
|
||||||
@ -111,7 +111,7 @@ public:
|
|||||||
return m_mcedit.get();
|
return m_mcedit.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<MojangAccountList> accounts() const
|
std::shared_ptr<AccountList> accounts() const
|
||||||
{
|
{
|
||||||
return m_accounts;
|
return m_accounts;
|
||||||
}
|
}
|
||||||
@ -188,7 +188,7 @@ private:
|
|||||||
FolderInstanceProvider * m_instanceFolder = nullptr;
|
FolderInstanceProvider * m_instanceFolder = nullptr;
|
||||||
std::shared_ptr<IconList> m_icons;
|
std::shared_ptr<IconList> m_icons;
|
||||||
std::shared_ptr<UpdateChecker> m_updateChecker;
|
std::shared_ptr<UpdateChecker> m_updateChecker;
|
||||||
std::shared_ptr<MojangAccountList> m_accounts;
|
std::shared_ptr<AccountList> m_accounts;
|
||||||
std::shared_ptr<JavaInstallList> m_javalist;
|
std::shared_ptr<JavaInstallList> m_javalist;
|
||||||
std::shared_ptr<TranslationsModel> m_translations;
|
std::shared_ptr<TranslationsModel> m_translations;
|
||||||
std::shared_ptr<GenericPageProvider> m_globalSettingsProvider;
|
std::shared_ptr<GenericPageProvider> m_globalSettingsProvider;
|
||||||
|
@ -30,9 +30,7 @@ namespace SkinUtils
|
|||||||
*/
|
*/
|
||||||
QPixmap getFaceFromCache(QString username, int height, int width)
|
QPixmap getFaceFromCache(QString username, int height, int width)
|
||||||
{
|
{
|
||||||
QFile fskin(ENV.metacache()
|
QFile fskin(ENV.metacache()->resolveEntry("skins", username + ".png")->getFullPath());
|
||||||
->resolveEntry("skins", username + ".png")
|
|
||||||
->getFullPath());
|
|
||||||
|
|
||||||
if (fskin.exists())
|
if (fskin.exists())
|
||||||
{
|
{
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
#include "LoginDialog.h"
|
#include "LoginDialog.h"
|
||||||
#include "ui_LoginDialog.h"
|
#include "ui_LoginDialog.h"
|
||||||
|
|
||||||
#include "minecraft/auth/YggdrasilTask.h"
|
#include "minecraft/auth/AccountTask.h"
|
||||||
|
|
||||||
#include <QtWidgets/QPushButton>
|
#include <QtWidgets/QPushButton>
|
||||||
|
|
||||||
@ -42,11 +42,10 @@ void LoginDialog::accept()
|
|||||||
ui->progressBar->setVisible(true);
|
ui->progressBar->setVisible(true);
|
||||||
|
|
||||||
// Setup the login task and start it
|
// Setup the login task and start it
|
||||||
m_account = MojangAccount::createFromUsername(ui->userTextBox->text());
|
m_account = MinecraftAccount::createFromUsername(ui->userTextBox->text());
|
||||||
m_loginTask = m_account->login(nullptr, ui->passTextBox->text());
|
m_loginTask = m_account->login(nullptr, ui->passTextBox->text());
|
||||||
connect(m_loginTask.get(), &Task::failed, this, &LoginDialog::onTaskFailed);
|
connect(m_loginTask.get(), &Task::failed, this, &LoginDialog::onTaskFailed);
|
||||||
connect(m_loginTask.get(), &Task::succeeded, this,
|
connect(m_loginTask.get(), &Task::succeeded, this, &LoginDialog::onTaskSucceeded);
|
||||||
&LoginDialog::onTaskSucceeded);
|
|
||||||
connect(m_loginTask.get(), &Task::status, this, &LoginDialog::onTaskStatus);
|
connect(m_loginTask.get(), &Task::status, this, &LoginDialog::onTaskStatus);
|
||||||
connect(m_loginTask.get(), &Task::progress, this, &LoginDialog::onTaskProgress);
|
connect(m_loginTask.get(), &Task::progress, this, &LoginDialog::onTaskProgress);
|
||||||
m_loginTask->start();
|
m_loginTask->start();
|
||||||
@ -98,7 +97,7 @@ void LoginDialog::onTaskProgress(qint64 current, qint64 total)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Public interface
|
// Public interface
|
||||||
MojangAccountPtr LoginDialog::newAccount(QWidget *parent, QString msg)
|
MinecraftAccountPtr LoginDialog::newAccount(QWidget *parent, QString msg)
|
||||||
{
|
{
|
||||||
LoginDialog dlg(parent);
|
LoginDialog dlg(parent);
|
||||||
dlg.ui->label->setText(msg);
|
dlg.ui->label->setText(msg);
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
#include <QtWidgets/QDialog>
|
#include <QtWidgets/QDialog>
|
||||||
#include <QtCore/QEventLoop>
|
#include <QtCore/QEventLoop>
|
||||||
|
|
||||||
#include "minecraft/auth/MojangAccount.h"
|
#include "minecraft/auth/MinecraftAccount.h"
|
||||||
|
|
||||||
namespace Ui
|
namespace Ui
|
||||||
{
|
{
|
||||||
@ -32,7 +32,7 @@ class LoginDialog : public QDialog
|
|||||||
public:
|
public:
|
||||||
~LoginDialog();
|
~LoginDialog();
|
||||||
|
|
||||||
static MojangAccountPtr newAccount(QWidget *parent, QString message);
|
static MinecraftAccountPtr newAccount(QWidget *parent, QString message);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
explicit LoginDialog(QWidget *parent = 0);
|
explicit LoginDialog(QWidget *parent = 0);
|
||||||
@ -53,6 +53,6 @@ slots:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
Ui::LoginDialog *ui;
|
Ui::LoginDialog *ui;
|
||||||
MojangAccountPtr m_account;
|
MinecraftAccountPtr m_account;
|
||||||
std::shared_ptr<Task> m_loginTask;
|
std::shared_ptr<Task> m_loginTask;
|
||||||
};
|
};
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>421</width>
|
<width>421</width>
|
||||||
<height>238</height>
|
<height>198</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
@ -20,16 +20,6 @@
|
|||||||
<string>Add Account</string>
|
<string>Add Account</string>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout">
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="microsoftAccountsNoticeLabel">
|
|
||||||
<property name="text">
|
|
||||||
<string>NOTICE: MultiMC does not currently support Microsoft accounts. This means that accounts created from December 2020 onwards cannot be used.</string>
|
|
||||||
</property>
|
|
||||||
<property name="wordWrap">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="label">
|
<widget class="QLabel" name="label">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
96
launcher/dialogs/MSALoginDialog.cpp
Normal file
96
launcher/dialogs/MSALoginDialog.cpp
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
/* Copyright 2013-2021 MultiMC Contributors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "MSALoginDialog.h"
|
||||||
|
#include "ui_MSALoginDialog.h"
|
||||||
|
|
||||||
|
#include "minecraft/auth/AccountTask.h"
|
||||||
|
|
||||||
|
#include <QtWidgets/QPushButton>
|
||||||
|
|
||||||
|
MSALoginDialog::MSALoginDialog(QWidget *parent) : QDialog(parent), ui(new Ui::MSALoginDialog)
|
||||||
|
{
|
||||||
|
ui->setupUi(this);
|
||||||
|
ui->progressBar->setVisible(false);
|
||||||
|
// ui->buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false);
|
||||||
|
|
||||||
|
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||||
|
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||||
|
}
|
||||||
|
|
||||||
|
int MSALoginDialog::exec() {
|
||||||
|
setUserInputsEnabled(false);
|
||||||
|
ui->progressBar->setVisible(true);
|
||||||
|
|
||||||
|
// Setup the login task and start it
|
||||||
|
m_account = MinecraftAccount::createBlankMSA();
|
||||||
|
m_loginTask = m_account->loginMSA(nullptr);
|
||||||
|
connect(m_loginTask.get(), &Task::failed, this, &MSALoginDialog::onTaskFailed);
|
||||||
|
connect(m_loginTask.get(), &Task::succeeded, this, &MSALoginDialog::onTaskSucceeded);
|
||||||
|
connect(m_loginTask.get(), &Task::status, this, &MSALoginDialog::onTaskStatus);
|
||||||
|
connect(m_loginTask.get(), &Task::progress, this, &MSALoginDialog::onTaskProgress);
|
||||||
|
m_loginTask->start();
|
||||||
|
|
||||||
|
return QDialog::exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
MSALoginDialog::~MSALoginDialog()
|
||||||
|
{
|
||||||
|
delete ui;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MSALoginDialog::setUserInputsEnabled(bool enable)
|
||||||
|
{
|
||||||
|
ui->buttonBox->setEnabled(enable);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MSALoginDialog::onTaskFailed(const QString &reason)
|
||||||
|
{
|
||||||
|
// Set message
|
||||||
|
ui->label->setText("<span style='color:red'>" + reason + "</span>");
|
||||||
|
|
||||||
|
// Re-enable user-interaction
|
||||||
|
setUserInputsEnabled(true);
|
||||||
|
ui->progressBar->setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MSALoginDialog::onTaskSucceeded()
|
||||||
|
{
|
||||||
|
QDialog::accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MSALoginDialog::onTaskStatus(const QString &status)
|
||||||
|
{
|
||||||
|
ui->label->setText(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MSALoginDialog::onTaskProgress(qint64 current, qint64 total)
|
||||||
|
{
|
||||||
|
ui->progressBar->setMaximum(total);
|
||||||
|
ui->progressBar->setValue(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public interface
|
||||||
|
MinecraftAccountPtr MSALoginDialog::newAccount(QWidget *parent, QString msg)
|
||||||
|
{
|
||||||
|
MSALoginDialog dlg(parent);
|
||||||
|
dlg.ui->label->setText(msg);
|
||||||
|
if (dlg.exec() == QDialog::Accepted)
|
||||||
|
{
|
||||||
|
return dlg.m_account;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
55
launcher/dialogs/MSALoginDialog.h
Normal file
55
launcher/dialogs/MSALoginDialog.h
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
/* Copyright 2013-2021 MultiMC Contributors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QtWidgets/QDialog>
|
||||||
|
#include <QtCore/QEventLoop>
|
||||||
|
|
||||||
|
#include "minecraft/auth/MinecraftAccount.h"
|
||||||
|
|
||||||
|
namespace Ui
|
||||||
|
{
|
||||||
|
class MSALoginDialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MSALoginDialog : public QDialog
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
~MSALoginDialog();
|
||||||
|
|
||||||
|
static MinecraftAccountPtr newAccount(QWidget *parent, QString message);
|
||||||
|
int exec() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit MSALoginDialog(QWidget *parent = 0);
|
||||||
|
|
||||||
|
void setUserInputsEnabled(bool enable);
|
||||||
|
|
||||||
|
protected
|
||||||
|
slots:
|
||||||
|
void onTaskFailed(const QString &reason);
|
||||||
|
void onTaskSucceeded();
|
||||||
|
void onTaskStatus(const QString &status);
|
||||||
|
void onTaskProgress(qint64 current, qint64 total);
|
||||||
|
|
||||||
|
private:
|
||||||
|
Ui::MSALoginDialog *ui;
|
||||||
|
MinecraftAccountPtr m_account;
|
||||||
|
std::shared_ptr<Task> m_loginTask;
|
||||||
|
};
|
||||||
|
|
60
launcher/dialogs/MSALoginDialog.ui
Normal file
60
launcher/dialogs/MSALoginDialog.ui
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>MSALoginDialog</class>
|
||||||
|
<widget class="QDialog" name="MSALoginDialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>421</width>
|
||||||
|
<height>114</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Add Microsoft Account</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="text">
|
||||||
|
<string notr="true">Message label placeholder.</string>
|
||||||
|
</property>
|
||||||
|
<property name="textFormat">
|
||||||
|
<enum>Qt::RichText</enum>
|
||||||
|
</property>
|
||||||
|
<property name="textInteractionFlags">
|
||||||
|
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QProgressBar" name="progressBar">
|
||||||
|
<property name="value">
|
||||||
|
<number>24</number>
|
||||||
|
</property>
|
||||||
|
<property name="textVisible">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="standardButtons">
|
||||||
|
<set>QDialogButtonBox::Cancel</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
@ -33,9 +33,10 @@ ProfileSelectDialog::ProfileSelectDialog(const QString &message, int flags, QWid
|
|||||||
m_accounts = MMC->accounts();
|
m_accounts = MMC->accounts();
|
||||||
auto view = ui->listView;
|
auto view = ui->listView;
|
||||||
//view->setModel(m_accounts.get());
|
//view->setModel(m_accounts.get());
|
||||||
//view->hideColumn(MojangAccountList::ActiveColumn);
|
//view->hideColumn(AccountList::ActiveColumn);
|
||||||
view->setColumnCount(1);
|
view->setColumnCount(1);
|
||||||
view->setRootIsDecorated(false);
|
view->setRootIsDecorated(false);
|
||||||
|
// FIXME: use a real model, not this
|
||||||
if(QTreeWidgetItem* header = view->headerItem())
|
if(QTreeWidgetItem* header = view->headerItem())
|
||||||
{
|
{
|
||||||
header->setText(0, tr("Name"));
|
header->setText(0, tr("Name"));
|
||||||
@ -47,21 +48,20 @@ ProfileSelectDialog::ProfileSelectDialog(const QString &message, int flags, QWid
|
|||||||
QList <QTreeWidgetItem *> items;
|
QList <QTreeWidgetItem *> items;
|
||||||
for (int i = 0; i < m_accounts->count(); i++)
|
for (int i = 0; i < m_accounts->count(); i++)
|
||||||
{
|
{
|
||||||
MojangAccountPtr account = m_accounts->at(i);
|
MinecraftAccountPtr account = m_accounts->at(i);
|
||||||
for (auto profile : account->profiles())
|
QString profileLabel;
|
||||||
{
|
if(account->isInUse()) {
|
||||||
auto profileLabel = profile.name;
|
profileLabel = tr("%1 (in use)").arg(account->profileName());
|
||||||
if(account->isInUse())
|
}
|
||||||
{
|
else {
|
||||||
profileLabel += tr(" (in use)");
|
profileLabel = account->profileName();
|
||||||
}
|
}
|
||||||
auto item = new QTreeWidgetItem(view);
|
auto item = new QTreeWidgetItem(view);
|
||||||
item->setText(0, profileLabel);
|
item->setText(0, profileLabel);
|
||||||
item->setIcon(0, SkinUtils::getFaceFromCache(profile.id));
|
item->setIcon(0, account->getFace());
|
||||||
item->setData(0, MojangAccountList::PointerRole, QVariant::fromValue(account));
|
item->setData(0, AccountList::PointerRole, QVariant::fromValue(account));
|
||||||
items.append(item);
|
items.append(item);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
view->addTopLevelItems(items);
|
view->addTopLevelItems(items);
|
||||||
|
|
||||||
// Set the message label.
|
// Set the message label.
|
||||||
@ -84,7 +84,7 @@ ProfileSelectDialog::~ProfileSelectDialog()
|
|||||||
delete ui;
|
delete ui;
|
||||||
}
|
}
|
||||||
|
|
||||||
MojangAccountPtr ProfileSelectDialog::selectedAccount() const
|
MinecraftAccountPtr ProfileSelectDialog::selectedAccount() const
|
||||||
{
|
{
|
||||||
return m_selected;
|
return m_selected;
|
||||||
}
|
}
|
||||||
@ -105,7 +105,7 @@ void ProfileSelectDialog::on_buttonBox_accepted()
|
|||||||
if (selection.size() > 0)
|
if (selection.size() > 0)
|
||||||
{
|
{
|
||||||
QModelIndex selected = selection.first();
|
QModelIndex selected = selection.first();
|
||||||
m_selected = selected.data(MojangAccountList::PointerRole).value<MojangAccountPtr>();
|
m_selected = selected.data(AccountList::PointerRole).value<MinecraftAccountPtr>();
|
||||||
}
|
}
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
#include "minecraft/auth/MojangAccountList.h"
|
#include "minecraft/auth/AccountList.h"
|
||||||
|
|
||||||
namespace Ui
|
namespace Ui
|
||||||
{
|
{
|
||||||
@ -59,7 +59,7 @@ public:
|
|||||||
* Gets a pointer to the account that the user selected.
|
* Gets a pointer to the account that the user selected.
|
||||||
* This is null if the user clicked cancel or hasn't clicked OK yet.
|
* This is null if the user clicked cancel or hasn't clicked OK yet.
|
||||||
*/
|
*/
|
||||||
MojangAccountPtr selectedAccount() const;
|
MinecraftAccountPtr selectedAccount() const;
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* Returns true if the user checked the "use as global default" checkbox.
|
* Returns true if the user checked the "use as global default" checkbox.
|
||||||
@ -80,10 +80,10 @@ slots:
|
|||||||
void on_buttonBox_rejected();
|
void on_buttonBox_rejected();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
std::shared_ptr<MojangAccountList> m_accounts;
|
std::shared_ptr<AccountList> m_accounts;
|
||||||
|
|
||||||
//! The account that was selected when the user clicked OK.
|
//! The account that was selected when the user clicked OK.
|
||||||
MojangAccountPtr m_selected;
|
MinecraftAccountPtr m_selected;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Ui::ProfileSelectDialog *ui;
|
Ui::ProfileSelectDialog *ui;
|
||||||
|
@ -107,7 +107,7 @@ void SkinUploadDialog::on_skinBrowseBtn_clicked()
|
|||||||
ui->skinPathTextBox->setText(cooked_path);
|
ui->skinPathTextBox->setText(cooked_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
SkinUploadDialog::SkinUploadDialog(MojangAccountPtr acct, QWidget *parent)
|
SkinUploadDialog::SkinUploadDialog(MinecraftAccountPtr acct, QWidget *parent)
|
||||||
:QDialog(parent), m_acct(acct), ui(new Ui::SkinUploadDialog)
|
:QDialog(parent), m_acct(acct), ui(new Ui::SkinUploadDialog)
|
||||||
{
|
{
|
||||||
ui->setupUi(this);
|
ui->setupUi(this);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QDialog>
|
#include <QDialog>
|
||||||
#include <minecraft/auth/MojangAccount.h>
|
#include <minecraft/auth/MinecraftAccount.h>
|
||||||
|
|
||||||
namespace Ui
|
namespace Ui
|
||||||
{
|
{
|
||||||
@ -11,7 +11,7 @@ namespace Ui
|
|||||||
class SkinUploadDialog : public QDialog {
|
class SkinUploadDialog : public QDialog {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit SkinUploadDialog(MojangAccountPtr acct, QWidget *parent = 0);
|
explicit SkinUploadDialog(MinecraftAccountPtr acct, QWidget *parent = 0);
|
||||||
virtual ~SkinUploadDialog() {};
|
virtual ~SkinUploadDialog() {};
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
@ -22,7 +22,7 @@ public slots:
|
|||||||
void on_skinBrowseBtn_clicked();
|
void on_skinBrowseBtn_clicked();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
MojangAccountPtr m_acct;
|
MinecraftAccountPtr m_acct;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Ui::SkinUploadDialog *ui;
|
Ui::SkinUploadDialog *ui;
|
||||||
|
@ -423,7 +423,7 @@ QStringList MinecraftInstance::processMinecraftArgs(
|
|||||||
// yggdrasil!
|
// yggdrasil!
|
||||||
if(session)
|
if(session)
|
||||||
{
|
{
|
||||||
token_mapping["auth_username"] = session->username;
|
// token_mapping["auth_username"] = session->username;
|
||||||
token_mapping["auth_session"] = session->session;
|
token_mapping["auth_session"] = session->session;
|
||||||
token_mapping["auth_access_token"] = session->access_token;
|
token_mapping["auth_access_token"] = session->access_token;
|
||||||
token_mapping["auth_player_name"] = session->player_name;
|
token_mapping["auth_player_name"] = session->player_name;
|
||||||
@ -691,19 +691,11 @@ QMap<QString, QString> MinecraftInstance::createCensorFilterFromSession(AuthSess
|
|||||||
addToFilter(sessionRef.session, tr("<SESSION ID>"));
|
addToFilter(sessionRef.session, tr("<SESSION ID>"));
|
||||||
}
|
}
|
||||||
addToFilter(sessionRef.access_token, tr("<ACCESS TOKEN>"));
|
addToFilter(sessionRef.access_token, tr("<ACCESS TOKEN>"));
|
||||||
|
if(sessionRef.client_token.size()) {
|
||||||
addToFilter(sessionRef.client_token, tr("<CLIENT TOKEN>"));
|
addToFilter(sessionRef.client_token, tr("<CLIENT TOKEN>"));
|
||||||
|
}
|
||||||
addToFilter(sessionRef.uuid, tr("<PROFILE ID>"));
|
addToFilter(sessionRef.uuid, tr("<PROFILE ID>"));
|
||||||
|
|
||||||
auto i = sessionRef.u.properties.begin();
|
|
||||||
while (i != sessionRef.u.properties.end())
|
|
||||||
{
|
|
||||||
if(i.value().length() <= 3) {
|
|
||||||
++i;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
addToFilter(i.value(), "<" + i.key().toUpper() + ">");
|
|
||||||
++i;
|
|
||||||
}
|
|
||||||
return filter;
|
return filter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
#include "BuildConfig.h"
|
|
||||||
#include <QObject>
|
|
||||||
|
|
||||||
const Config BuildConfig;
|
|
||||||
|
|
||||||
Config::Config()
|
|
||||||
{
|
|
||||||
CLIENT_ID = "@MOJANGDEMO_CLIENT_ID@";
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include <QString>
|
|
||||||
|
|
||||||
class Config
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
Config();
|
|
||||||
QString CLIENT_ID;
|
|
||||||
};
|
|
||||||
|
|
||||||
extern const Config BuildConfig;
|
|
@ -1,28 +0,0 @@
|
|||||||
find_package(Qt5 COMPONENTS Core Gui Network Widgets REQUIRED)
|
|
||||||
|
|
||||||
set(CMAKE_AUTOMOC ON)
|
|
||||||
set(CMAKE_AUTOUIC ON)
|
|
||||||
set(CMAKE_INCLUDE_CURRENT_DIR ON)
|
|
||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")
|
|
||||||
|
|
||||||
|
|
||||||
set(MOJANGDEMO_CLIENT_ID "" CACHE STRING "Client ID used for OAuth2 in mojangdemo")
|
|
||||||
|
|
||||||
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/BuildConfig.cpp.in" "${CMAKE_CURRENT_BINARY_DIR}/BuildConfig.cpp")
|
|
||||||
|
|
||||||
set(mojang_SRCS
|
|
||||||
main.cpp
|
|
||||||
context.cpp
|
|
||||||
context.h
|
|
||||||
|
|
||||||
mainwindow.cpp
|
|
||||||
mainwindow.h
|
|
||||||
mainwindow.ui
|
|
||||||
|
|
||||||
${CMAKE_CURRENT_BINARY_DIR}/BuildConfig.cpp
|
|
||||||
BuildConfig.h
|
|
||||||
)
|
|
||||||
|
|
||||||
add_executable( mojangdemo ${mojang_SRCS} )
|
|
||||||
target_link_libraries( mojangdemo Katabasis Qt5::Gui Qt5::Widgets )
|
|
||||||
target_include_directories(mojangdemo PRIVATE logic)
|
|
@ -1,100 +0,0 @@
|
|||||||
#include <QApplication>
|
|
||||||
#include <QStringList>
|
|
||||||
#include <QTimer>
|
|
||||||
#include <QDebug>
|
|
||||||
#include <QFile>
|
|
||||||
#include <QSaveFile>
|
|
||||||
|
|
||||||
#include "context.h"
|
|
||||||
#include "mainwindow.h"
|
|
||||||
|
|
||||||
void myMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
|
|
||||||
{
|
|
||||||
QByteArray localMsg = msg.toLocal8Bit();
|
|
||||||
const char *file = context.file ? context.file : "";
|
|
||||||
const char *function = context.function ? context.function : "";
|
|
||||||
switch (type) {
|
|
||||||
case QtDebugMsg:
|
|
||||||
fprintf(stderr, "Debug: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);
|
|
||||||
break;
|
|
||||||
case QtInfoMsg:
|
|
||||||
fprintf(stderr, "Info: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);
|
|
||||||
break;
|
|
||||||
case QtWarningMsg:
|
|
||||||
fprintf(stderr, "Warning: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);
|
|
||||||
break;
|
|
||||||
case QtCriticalMsg:
|
|
||||||
fprintf(stderr, "Critical: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);
|
|
||||||
break;
|
|
||||||
case QtFatalMsg:
|
|
||||||
fprintf(stderr, "Fatal: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Helper : public QObject {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
Helper(Context * context) : QObject(), context_(context), msg_(QString()) {
|
|
||||||
QFile tokenCache("usercache.dat");
|
|
||||||
if(tokenCache.open(QIODevice::ReadOnly)) {
|
|
||||||
context_->resumeFromState(tokenCache.readAll());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public slots:
|
|
||||||
void run() {
|
|
||||||
connect(context_, &Context::activityChanged, this, &Helper::onActivityChanged);
|
|
||||||
context_->silentSignIn();
|
|
||||||
}
|
|
||||||
|
|
||||||
void onFailed() {
|
|
||||||
qDebug() << "Login failed";
|
|
||||||
}
|
|
||||||
|
|
||||||
void onActivityChanged(Katabasis::Activity activity) {
|
|
||||||
if(activity == Katabasis::Activity::Idle) {
|
|
||||||
switch(context_->validity()) {
|
|
||||||
case Katabasis::Validity::None: {
|
|
||||||
// account is gone, remove it.
|
|
||||||
QFile::remove("usercache.dat");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Katabasis::Validity::Assumed: {
|
|
||||||
// this is basically a soft-failed refresh. do nothing.
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Katabasis::Validity::Certain: {
|
|
||||||
// stuff got refreshed / signed in. Save.
|
|
||||||
auto data = context_->saveState();
|
|
||||||
QSaveFile tokenCache("usercache.dat");
|
|
||||||
if(tokenCache.open(QIODevice::WriteOnly)) {
|
|
||||||
tokenCache.write(context_->saveState());
|
|
||||||
tokenCache.commit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
Context *context_;
|
|
||||||
QString msg_;
|
|
||||||
};
|
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
|
||||||
qInstallMessageHandler(myMessageOutput);
|
|
||||||
QApplication a(argc, argv);
|
|
||||||
QCoreApplication::setOrganizationName("MultiMC");
|
|
||||||
QCoreApplication::setApplicationName("MultiMC");
|
|
||||||
Context c;
|
|
||||||
Helper helper(&c);
|
|
||||||
MainWindow window(&c);
|
|
||||||
window.show();
|
|
||||||
QTimer::singleShot(0, &helper, &Helper::run);
|
|
||||||
return a.exec();
|
|
||||||
}
|
|
||||||
|
|
||||||
#include "main.moc"
|
|
@ -1,97 +0,0 @@
|
|||||||
#include "mainwindow.h"
|
|
||||||
#include "ui_mainwindow.h"
|
|
||||||
#include <QDebug>
|
|
||||||
|
|
||||||
#include <QDesktopServices>
|
|
||||||
|
|
||||||
#include "BuildConfig.h"
|
|
||||||
|
|
||||||
MainWindow::MainWindow(Context * context, QWidget *parent) :
|
|
||||||
QMainWindow(parent),
|
|
||||||
m_context(context),
|
|
||||||
m_ui(new Ui::MainWindow)
|
|
||||||
{
|
|
||||||
m_ui->setupUi(this);
|
|
||||||
connect(m_ui->signInButton_MSA, &QPushButton::clicked, this, &MainWindow::SignInMSAClicked);
|
|
||||||
connect(m_ui->signInButton_Mojang, &QPushButton::clicked, this, &MainWindow::SignInMojangClicked);
|
|
||||||
connect(m_ui->signOutButton, &QPushButton::clicked, this, &MainWindow::SignOutClicked);
|
|
||||||
connect(m_ui->refreshButton, &QPushButton::clicked, this, &MainWindow::RefreshClicked);
|
|
||||||
|
|
||||||
// connect(m_context, &Context::linkingSucceeded, this, &MainWindow::SignInSucceeded);
|
|
||||||
// connect(m_context, &Context::linkingFailed, this, &MainWindow::SignInFailed);
|
|
||||||
connect(m_context, &Context::activityChanged, this, &MainWindow::ActivityChanged);
|
|
||||||
ActivityChanged(Katabasis::Activity::Idle);
|
|
||||||
}
|
|
||||||
|
|
||||||
MainWindow::~MainWindow() = default;
|
|
||||||
|
|
||||||
void MainWindow::ActivityChanged(Katabasis::Activity activity) {
|
|
||||||
switch(activity) {
|
|
||||||
case Katabasis::Activity::Idle: {
|
|
||||||
if(m_context->validity() != Katabasis::Validity::None) {
|
|
||||||
m_ui->signInButton_Mojang->setEnabled(false);
|
|
||||||
m_ui->signInButton_MSA->setEnabled(false);
|
|
||||||
m_ui->signOutButton->setEnabled(true);
|
|
||||||
m_ui->refreshButton->setEnabled(true);
|
|
||||||
m_ui->statusBar->showMessage(QString("Hello %1!").arg(m_context->userName()));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
m_ui->signInButton_Mojang->setEnabled(true);
|
|
||||||
m_ui->signInButton_MSA->setEnabled(true);
|
|
||||||
m_ui->signOutButton->setEnabled(false);
|
|
||||||
m_ui->refreshButton->setEnabled(false);
|
|
||||||
m_ui->statusBar->showMessage("Press the login button to start.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Katabasis::Activity::LoggingIn: {
|
|
||||||
m_ui->signInButton_Mojang->setEnabled(false);
|
|
||||||
m_ui->signInButton_MSA->setEnabled(false);
|
|
||||||
m_ui->signOutButton->setEnabled(false);
|
|
||||||
m_ui->refreshButton->setEnabled(false);
|
|
||||||
m_ui->statusBar->showMessage("Logging in...");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Katabasis::Activity::LoggingOut: {
|
|
||||||
m_ui->signInButton_Mojang->setEnabled(false);
|
|
||||||
m_ui->signInButton_MSA->setEnabled(false);
|
|
||||||
m_ui->signOutButton->setEnabled(false);
|
|
||||||
m_ui->refreshButton->setEnabled(false);
|
|
||||||
m_ui->statusBar->showMessage("Logging out...");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Katabasis::Activity::Refreshing: {
|
|
||||||
m_ui->signInButton_Mojang->setEnabled(false);
|
|
||||||
m_ui->signInButton_MSA->setEnabled(false);
|
|
||||||
m_ui->signOutButton->setEnabled(false);
|
|
||||||
m_ui->refreshButton->setEnabled(false);
|
|
||||||
m_ui->statusBar->showMessage("Refreshing login...");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::SignInMSAClicked() {
|
|
||||||
qDebug() << "Sign In MSA";
|
|
||||||
// signIn({{"prompt", "select_account"}})
|
|
||||||
// FIXME: wrong. very wrong. this should not be operating on the current context
|
|
||||||
m_context->signIn();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::SignInMojangClicked() {
|
|
||||||
qDebug() << "Sign In Mojang";
|
|
||||||
// signIn({{"prompt", "select_account"}})
|
|
||||||
// FIXME: wrong. very wrong. this should not be operating on the current context
|
|
||||||
m_context->signIn();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void MainWindow::SignOutClicked() {
|
|
||||||
qDebug() << "Sign Out";
|
|
||||||
m_context->signOut();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::RefreshClicked() {
|
|
||||||
qDebug() << "Refresh";
|
|
||||||
m_context->silentSignIn();
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QMainWindow>
|
|
||||||
#include <QScopedPointer>
|
|
||||||
#include <QtNetwork>
|
|
||||||
#include <katabasis/Bits.h>
|
|
||||||
|
|
||||||
#include "context.h"
|
|
||||||
|
|
||||||
namespace Ui {
|
|
||||||
class MainWindow;
|
|
||||||
}
|
|
||||||
|
|
||||||
class MainWindow : public QMainWindow {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit MainWindow(Context * context, QWidget *parent = nullptr);
|
|
||||||
~MainWindow() override;
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void SignInMojangClicked();
|
|
||||||
void SignInMSAClicked();
|
|
||||||
|
|
||||||
void SignOutClicked();
|
|
||||||
void RefreshClicked();
|
|
||||||
|
|
||||||
void ActivityChanged(Katabasis::Activity activity);
|
|
||||||
|
|
||||||
private:
|
|
||||||
Context* m_context;
|
|
||||||
QScopedPointer<Ui::MainWindow> m_ui;
|
|
||||||
};
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<ui version="4.0">
|
|
||||||
<class>MainWindow</class>
|
|
||||||
<widget class="QMainWindow" name="MainWindow">
|
|
||||||
<property name="geometry">
|
|
||||||
<rect>
|
|
||||||
<x>0</x>
|
|
||||||
<y>0</y>
|
|
||||||
<width>1037</width>
|
|
||||||
<height>511</height>
|
|
||||||
</rect>
|
|
||||||
</property>
|
|
||||||
<property name="windowTitle">
|
|
||||||
<string>SmartMapsClient</string>
|
|
||||||
</property>
|
|
||||||
<property name="dockNestingEnabled">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
<widget class="QWidget" name="centralWidget">
|
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
|
||||||
<item row="1" column="3">
|
|
||||||
<widget class="QPushButton" name="signInButton_Mojang">
|
|
||||||
<property name="text">
|
|
||||||
<string>SignIn Mojang</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="3" column="3">
|
|
||||||
<widget class="Line" name="line">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="0" rowspan="7" colspan="3">
|
|
||||||
<widget class="QTreeView" name="accountView"/>
|
|
||||||
</item>
|
|
||||||
<item row="5" column="3">
|
|
||||||
<widget class="QPushButton" name="refreshButton">
|
|
||||||
<property name="text">
|
|
||||||
<string>Refresh</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="3">
|
|
||||||
<widget class="QPushButton" name="signInButton_MSA">
|
|
||||||
<property name="text">
|
|
||||||
<string>SignIn MSA</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="6" column="3">
|
|
||||||
<widget class="QPushButton" name="signOutButton">
|
|
||||||
<property name="text">
|
|
||||||
<string>SignOut</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="4" column="3">
|
|
||||||
<widget class="QPushButton" name="makeActiveButton">
|
|
||||||
<property name="text">
|
|
||||||
<string>Make Active</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
<widget class="QStatusBar" name="statusBar"/>
|
|
||||||
</widget>
|
|
||||||
<resources/>
|
|
||||||
<connections/>
|
|
||||||
</ui>
|
|
387
launcher/minecraft/auth/AccountData.cpp
Normal file
387
launcher/minecraft/auth/AccountData.cpp
Normal file
@ -0,0 +1,387 @@
|
|||||||
|
#include "AccountData.h"
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QUuid>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
void tokenToJSONV3(QJsonObject &parent, Katabasis::Token t, const char * tokenName) {
|
||||||
|
if(!t.persistent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QJsonObject out;
|
||||||
|
if(t.issueInstant.isValid()) {
|
||||||
|
out["iat"] = QJsonValue(t.issueInstant.toMSecsSinceEpoch() / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(t.notAfter.isValid()) {
|
||||||
|
out["exp"] = QJsonValue(t.notAfter.toMSecsSinceEpoch() / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool save = false;
|
||||||
|
if(!t.token.isEmpty()) {
|
||||||
|
out["token"] = QJsonValue(t.token);
|
||||||
|
save = true;
|
||||||
|
}
|
||||||
|
if(!t.refresh_token.isEmpty()) {
|
||||||
|
out["refresh_token"] = QJsonValue(t.refresh_token);
|
||||||
|
save = true;
|
||||||
|
}
|
||||||
|
if(t.extra.size()) {
|
||||||
|
out["extra"] = QJsonObject::fromVariantMap(t.extra);
|
||||||
|
save = true;
|
||||||
|
}
|
||||||
|
if(save) {
|
||||||
|
parent[tokenName] = out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Katabasis::Token tokenFromJSONV3(const QJsonObject &parent, const char * tokenName) {
|
||||||
|
Katabasis::Token out;
|
||||||
|
auto tokenObject = parent.value(tokenName).toObject();
|
||||||
|
if(tokenObject.isEmpty()) {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
auto issueInstant = tokenObject.value("iat");
|
||||||
|
if(issueInstant.isDouble()) {
|
||||||
|
out.issueInstant = QDateTime::fromMSecsSinceEpoch(((int64_t) issueInstant.toDouble()) * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto notAfter = tokenObject.value("exp");
|
||||||
|
if(notAfter.isDouble()) {
|
||||||
|
out.notAfter = QDateTime::fromMSecsSinceEpoch(((int64_t) notAfter.toDouble()) * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto token = tokenObject.value("token");
|
||||||
|
if(token.isString()) {
|
||||||
|
out.token = token.toString();
|
||||||
|
out.validity = Katabasis::Validity::Assumed;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto refresh_token = tokenObject.value("refresh_token");
|
||||||
|
if(refresh_token.isString()) {
|
||||||
|
out.refresh_token = refresh_token.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto extra = tokenObject.value("extra");
|
||||||
|
if(extra.isObject()) {
|
||||||
|
out.extra = extra.toObject().toVariantMap();
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
void profileToJSONV3(QJsonObject &parent, MinecraftProfile p, const char * tokenName) {
|
||||||
|
if(p.id.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QJsonObject out;
|
||||||
|
out["id"] = QJsonValue(p.id);
|
||||||
|
out["name"] = QJsonValue(p.name);
|
||||||
|
if(p.currentCape != -1) {
|
||||||
|
out["cape"] = p.capes[p.currentCape].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
QJsonObject skinObj;
|
||||||
|
skinObj["id"] = p.skin.id;
|
||||||
|
skinObj["url"] = p.skin.url;
|
||||||
|
skinObj["variant"] = p.skin.variant;
|
||||||
|
if(p.skin.data.size()) {
|
||||||
|
skinObj["data"] = QString::fromLatin1(p.skin.data.toBase64());
|
||||||
|
}
|
||||||
|
out["skin"] = skinObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonArray capesArray;
|
||||||
|
for(auto & cape: p.capes) {
|
||||||
|
QJsonObject capeObj;
|
||||||
|
capeObj["id"] = cape.id;
|
||||||
|
capeObj["url"] = cape.url;
|
||||||
|
capeObj["alias"] = cape.alias;
|
||||||
|
if(cape.data.size()) {
|
||||||
|
capeObj["data"] = QString::fromLatin1(cape.data.toBase64());
|
||||||
|
}
|
||||||
|
capesArray.push_back(capeObj);
|
||||||
|
}
|
||||||
|
out["capes"] = capesArray;
|
||||||
|
parent[tokenName] = out;
|
||||||
|
}
|
||||||
|
|
||||||
|
MinecraftProfile profileFromJSONV3(const QJsonObject &parent, const char * tokenName) {
|
||||||
|
MinecraftProfile out;
|
||||||
|
auto tokenObject = parent.value(tokenName).toObject();
|
||||||
|
if(tokenObject.isEmpty()) {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto idV = tokenObject.value("id");
|
||||||
|
auto nameV = tokenObject.value("name");
|
||||||
|
if(!idV.isString() || !nameV.isString()) {
|
||||||
|
qWarning() << "mandatory profile attributes are missing or of unexpected type";
|
||||||
|
return MinecraftProfile();
|
||||||
|
}
|
||||||
|
out.name = nameV.toString();
|
||||||
|
out.id = idV.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto skinV = tokenObject.value("skin");
|
||||||
|
if(!skinV.isObject()) {
|
||||||
|
qWarning() << "skin is missing";
|
||||||
|
return MinecraftProfile();
|
||||||
|
}
|
||||||
|
auto skinObj = skinV.toObject();
|
||||||
|
auto idV = skinObj.value("id");
|
||||||
|
auto urlV = skinObj.value("url");
|
||||||
|
auto variantV = skinObj.value("variant");
|
||||||
|
if(!idV.isString() || !urlV.isString() || !variantV.isString()) {
|
||||||
|
qWarning() << "mandatory skin attributes are missing or of unexpected type";
|
||||||
|
return MinecraftProfile();
|
||||||
|
}
|
||||||
|
out.skin.id = idV.toString();
|
||||||
|
out.skin.url = urlV.toString();
|
||||||
|
out.skin.variant = variantV.toString();
|
||||||
|
|
||||||
|
// data for skin is optional
|
||||||
|
auto dataV = skinObj.value("data");
|
||||||
|
if(dataV.isString()) {
|
||||||
|
// TODO: validate base64
|
||||||
|
out.skin.data = QByteArray::fromBase64(dataV.toString().toLatin1());
|
||||||
|
}
|
||||||
|
else if (!dataV.isUndefined()) {
|
||||||
|
qWarning() << "skin data is something unexpected";
|
||||||
|
return MinecraftProfile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto capesV = tokenObject.value("capes");
|
||||||
|
if(!capesV.isArray()) {
|
||||||
|
qWarning() << "capes is not an array!";
|
||||||
|
return MinecraftProfile();
|
||||||
|
}
|
||||||
|
auto capesArray = capesV.toArray();
|
||||||
|
for(auto capeV: capesArray) {
|
||||||
|
if(!capeV.isObject()) {
|
||||||
|
qWarning() << "cape is not an object!";
|
||||||
|
return MinecraftProfile();
|
||||||
|
}
|
||||||
|
auto capeObj = capeV.toObject();
|
||||||
|
auto idV = capeObj.value("id");
|
||||||
|
auto urlV = capeObj.value("url");
|
||||||
|
auto aliasV = capeObj.value("alias");
|
||||||
|
if(!idV.isString() || !urlV.isString() || !aliasV.isString()) {
|
||||||
|
qWarning() << "mandatory skin attributes are missing or of unexpected type";
|
||||||
|
return MinecraftProfile();
|
||||||
|
}
|
||||||
|
Cape cape;
|
||||||
|
cape.id = idV.toString();
|
||||||
|
cape.url = urlV.toString();
|
||||||
|
cape.alias = aliasV.toString();
|
||||||
|
|
||||||
|
// data for cape is optional.
|
||||||
|
auto dataV = capeObj.value("data");
|
||||||
|
if(dataV.isString()) {
|
||||||
|
// TODO: validate base64
|
||||||
|
cape.data = QByteArray::fromBase64(dataV.toString().toLatin1());
|
||||||
|
}
|
||||||
|
else if (!dataV.isUndefined()) {
|
||||||
|
qWarning() << "cape data is something unexpected";
|
||||||
|
return MinecraftProfile();
|
||||||
|
}
|
||||||
|
out.capes.push_back(cape);
|
||||||
|
}
|
||||||
|
out.validity = Katabasis::Validity::Assumed;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AccountData::resumeStateFromV2(QJsonObject data) {
|
||||||
|
// The JSON object must at least have a username for it to be valid.
|
||||||
|
if (!data.value("username").isString())
|
||||||
|
{
|
||||||
|
qCritical() << "Can't load Mojang account info from JSON object. Username field is missing or of the wrong type.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString userName = data.value("username").toString("");
|
||||||
|
QString clientToken = data.value("clientToken").toString("");
|
||||||
|
QString accessToken = data.value("accessToken").toString("");
|
||||||
|
|
||||||
|
QJsonArray profileArray = data.value("profiles").toArray();
|
||||||
|
if (profileArray.size() < 1)
|
||||||
|
{
|
||||||
|
qCritical() << "Can't load Mojang account with username \"" << userName << "\". No profiles found.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AccountProfile
|
||||||
|
{
|
||||||
|
QString id;
|
||||||
|
QString name;
|
||||||
|
bool legacy;
|
||||||
|
};
|
||||||
|
|
||||||
|
QList<AccountProfile> profiles;
|
||||||
|
int currentProfileIndex = 0;
|
||||||
|
int index = -1;
|
||||||
|
QString currentProfile = data.value("activeProfile").toString("");
|
||||||
|
for (QJsonValue profileVal : profileArray)
|
||||||
|
{
|
||||||
|
index++;
|
||||||
|
QJsonObject profileObject = profileVal.toObject();
|
||||||
|
QString id = profileObject.value("id").toString("");
|
||||||
|
QString name = profileObject.value("name").toString("");
|
||||||
|
bool legacy = profileObject.value("legacy").toBool(false);
|
||||||
|
if (id.isEmpty() || name.isEmpty())
|
||||||
|
{
|
||||||
|
qWarning() << "Unable to load a profile" << name << "because it was missing an ID or a name.";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if(id == currentProfile) {
|
||||||
|
currentProfileIndex = index;
|
||||||
|
}
|
||||||
|
profiles.append({id, name, legacy});
|
||||||
|
}
|
||||||
|
auto & profile = profiles[currentProfileIndex];
|
||||||
|
|
||||||
|
type = AccountType::Mojang;
|
||||||
|
legacy = profile.legacy;
|
||||||
|
|
||||||
|
minecraftProfile.id = profile.id;
|
||||||
|
minecraftProfile.name = profile.name;
|
||||||
|
minecraftProfile.validity = Katabasis::Validity::Assumed;
|
||||||
|
|
||||||
|
yggdrasilToken.token = accessToken;
|
||||||
|
yggdrasilToken.extra["clientToken"] = clientToken;
|
||||||
|
yggdrasilToken.extra["userName"] = userName;
|
||||||
|
yggdrasilToken.validity = Katabasis::Validity::Assumed;
|
||||||
|
|
||||||
|
validity_ = minecraftProfile.validity;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AccountData::resumeStateFromV3(QJsonObject data) {
|
||||||
|
auto typeV = data.value("type");
|
||||||
|
if(!typeV.isString()) {
|
||||||
|
qWarning() << "Failed to parse account data: type is missing.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto typeS = typeV.toString();
|
||||||
|
if(typeS == "MSA") {
|
||||||
|
type = AccountType::MSA;
|
||||||
|
} else if (typeS == "Mojang") {
|
||||||
|
type = AccountType::Mojang;
|
||||||
|
} else {
|
||||||
|
qWarning() << "Failed to parse account data: type is not recognized.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(type == AccountType::Mojang) {
|
||||||
|
legacy = data.value("legacy").toBool(false);
|
||||||
|
canMigrateToMSA = data.value("canMigrateToMSA").toBool(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(type == AccountType::MSA) {
|
||||||
|
msaToken = tokenFromJSONV3(data, "msa");
|
||||||
|
userToken = tokenFromJSONV3(data, "utoken");
|
||||||
|
xboxApiToken = tokenFromJSONV3(data, "xrp-main");
|
||||||
|
mojangservicesToken = tokenFromJSONV3(data, "xrp-mc");
|
||||||
|
}
|
||||||
|
|
||||||
|
yggdrasilToken = tokenFromJSONV3(data, "ygg");
|
||||||
|
minecraftProfile = profileFromJSONV3(data, "profile");
|
||||||
|
|
||||||
|
validity_ = minecraftProfile.validity;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject AccountData::saveState() const {
|
||||||
|
QJsonObject output;
|
||||||
|
if(type == AccountType::Mojang) {
|
||||||
|
output["type"] = "Mojang";
|
||||||
|
if(legacy) {
|
||||||
|
output["legacy"] = true;
|
||||||
|
}
|
||||||
|
if(canMigrateToMSA) {
|
||||||
|
output["canMigrateToMSA"] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (type == AccountType::MSA) {
|
||||||
|
output["type"] = "MSA";
|
||||||
|
tokenToJSONV3(output, msaToken, "msa");
|
||||||
|
tokenToJSONV3(output, userToken, "utoken");
|
||||||
|
tokenToJSONV3(output, xboxApiToken, "xrp-main");
|
||||||
|
tokenToJSONV3(output, mojangservicesToken, "xrp-mc");
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenToJSONV3(output, yggdrasilToken, "ygg");
|
||||||
|
profileToJSONV3(output, minecraftProfile, "profile");
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString AccountData::userName() const {
|
||||||
|
if(type != AccountType::Mojang) {
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
return yggdrasilToken.extra["userName"].toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString AccountData::accessToken() const {
|
||||||
|
return yggdrasilToken.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString AccountData::clientToken() const {
|
||||||
|
if(type != AccountType::Mojang) {
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
return yggdrasilToken.extra["clientToken"].toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AccountData::setClientToken(QString clientToken) {
|
||||||
|
if(type != AccountType::Mojang) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
yggdrasilToken.extra["clientToken"] = clientToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AccountData::generateClientTokenIfMissing() {
|
||||||
|
if(yggdrasilToken.extra.contains("clientToken")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
invalidateClientToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AccountData::invalidateClientToken() {
|
||||||
|
if(type != AccountType::Mojang) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegExp("[{-}]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
QString AccountData::profileId() const {
|
||||||
|
return minecraftProfile.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString AccountData::profileName() const {
|
||||||
|
return minecraftProfile.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString AccountData::accountDisplayString() const {
|
||||||
|
switch(type) {
|
||||||
|
case AccountType::Mojang: {
|
||||||
|
return userName();
|
||||||
|
}
|
||||||
|
case AccountType::MSA: {
|
||||||
|
if(xboxApiToken.extra.contains("gtg")) {
|
||||||
|
return xboxApiToken.extra["gtg"].toString();
|
||||||
|
}
|
||||||
|
return "Xbox profile missing";
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return "Invalid Account";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
73
launcher/minecraft/auth/AccountData.h
Normal file
73
launcher/minecraft/auth/AccountData.h
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <QString>
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QVector>
|
||||||
|
#include <katabasis/Bits.h>
|
||||||
|
#include <QJsonObject>
|
||||||
|
|
||||||
|
struct Skin {
|
||||||
|
QString id;
|
||||||
|
QString url;
|
||||||
|
QString variant;
|
||||||
|
|
||||||
|
QByteArray data;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Cape {
|
||||||
|
QString id;
|
||||||
|
QString url;
|
||||||
|
QString alias;
|
||||||
|
|
||||||
|
QByteArray data;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MinecraftProfile {
|
||||||
|
QString id;
|
||||||
|
QString name;
|
||||||
|
Skin skin;
|
||||||
|
int currentCape = -1;
|
||||||
|
QVector<Cape> capes;
|
||||||
|
Katabasis::Validity validity = Katabasis::Validity::None;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class AccountType {
|
||||||
|
MSA,
|
||||||
|
Mojang
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AccountData {
|
||||||
|
QJsonObject saveState() const;
|
||||||
|
bool resumeStateFromV2(QJsonObject data);
|
||||||
|
bool resumeStateFromV3(QJsonObject data);
|
||||||
|
|
||||||
|
//! userName for Mojang accounts, gamertag for MSA
|
||||||
|
QString accountDisplayString() const;
|
||||||
|
|
||||||
|
//! Only valid for Mojang accounts. MSA does not preserve this information
|
||||||
|
QString userName() const;
|
||||||
|
|
||||||
|
//! Only valid for Mojang accounts.
|
||||||
|
QString clientToken() const;
|
||||||
|
void setClientToken(QString clientToken);
|
||||||
|
void invalidateClientToken();
|
||||||
|
void generateClientTokenIfMissing();
|
||||||
|
|
||||||
|
//! Yggdrasil access token, as passed to the game.
|
||||||
|
QString accessToken() const;
|
||||||
|
|
||||||
|
QString profileId() const;
|
||||||
|
QString profileName() const;
|
||||||
|
|
||||||
|
AccountType type = AccountType::MSA;
|
||||||
|
bool legacy = false;
|
||||||
|
bool canMigrateToMSA = false;
|
||||||
|
|
||||||
|
Katabasis::Token msaToken;
|
||||||
|
Katabasis::Token userToken;
|
||||||
|
Katabasis::Token xboxApiToken;
|
||||||
|
Katabasis::Token mojangservicesToken;
|
||||||
|
|
||||||
|
Katabasis::Token yggdrasilToken;
|
||||||
|
MinecraftProfile minecraftProfile;
|
||||||
|
Katabasis::Validity validity_ = Katabasis::Validity::None;
|
||||||
|
};
|
@ -13,8 +13,8 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "MojangAccountList.h"
|
#include "AccountList.h"
|
||||||
#include "MojangAccount.h"
|
#include "AccountData.h"
|
||||||
|
|
||||||
#include <QIODevice>
|
#include <QIODevice>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
@ -28,31 +28,49 @@
|
|||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
|
||||||
#include <FileSystem.h>
|
#include <FileSystem.h>
|
||||||
|
#include <QSaveFile>
|
||||||
|
|
||||||
#define ACCOUNT_LIST_FORMAT_VERSION 2
|
enum AccountListVersion {
|
||||||
|
MojangOnly = 2,
|
||||||
|
MojangMSA = 3
|
||||||
|
};
|
||||||
|
|
||||||
MojangAccountList::MojangAccountList(QObject *parent) : QAbstractListModel(parent)
|
AccountList::AccountList(QObject *parent) : QAbstractListModel(parent) { }
|
||||||
{
|
|
||||||
|
int AccountList::findAccountByProfileId(const QString& profileId) const {
|
||||||
|
for (int i = 0; i < count(); i++) {
|
||||||
|
MinecraftAccountPtr account = at(i);
|
||||||
|
if (account->profileId() == profileId) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
MojangAccountPtr MojangAccountList::findAccount(const QString &username) const
|
const MinecraftAccountPtr AccountList::at(int i) const
|
||||||
{
|
{
|
||||||
for (int i = 0; i < count(); i++)
|
return MinecraftAccountPtr(m_accounts.at(i));
|
||||||
{
|
|
||||||
MojangAccountPtr account = at(i);
|
|
||||||
if (account->username() == username)
|
|
||||||
return account;
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const MojangAccountPtr MojangAccountList::at(int i) const
|
void AccountList::addAccount(const MinecraftAccountPtr account)
|
||||||
{
|
{
|
||||||
return MojangAccountPtr(m_accounts.at(i));
|
// We only ever want accounts with valid profiles.
|
||||||
|
// Keeping profile-less accounts is pointless and serves no purpose.
|
||||||
|
auto profileId = account->profileId();
|
||||||
|
if(!profileId.size()) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MojangAccountList::addAccount(const MojangAccountPtr account)
|
// override/replace existing account with the same profileId
|
||||||
{
|
auto existingAccount = findAccountByProfileId(profileId);
|
||||||
|
if(existingAccount != -1) {
|
||||||
|
m_accounts[existingAccount] = account;
|
||||||
|
emit dataChanged(index(existingAccount), index(existingAccount, columnCount(QModelIndex()) - 1));
|
||||||
|
onListChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we don't have this porfileId yet, add the account to the end
|
||||||
int row = m_accounts.count();
|
int row = m_accounts.count();
|
||||||
beginInsertRows(QModelIndex(), row, row);
|
beginInsertRows(QModelIndex(), row, row);
|
||||||
connect(account.get(), SIGNAL(changed()), SLOT(accountChanged()));
|
connect(account.get(), SIGNAL(changed()), SLOT(accountChanged()));
|
||||||
@ -61,24 +79,7 @@ void MojangAccountList::addAccount(const MojangAccountPtr account)
|
|||||||
onListChanged();
|
onListChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MojangAccountList::removeAccount(const QString &username)
|
void AccountList::removeAccount(QModelIndex index)
|
||||||
{
|
|
||||||
int idx = 0;
|
|
||||||
for (auto account : m_accounts)
|
|
||||||
{
|
|
||||||
if (account->username() == username)
|
|
||||||
{
|
|
||||||
beginRemoveRows(QModelIndex(), idx, idx);
|
|
||||||
m_accounts.removeOne(account);
|
|
||||||
endRemoveRows();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
idx++;
|
|
||||||
}
|
|
||||||
onListChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MojangAccountList::removeAccount(QModelIndex index)
|
|
||||||
{
|
{
|
||||||
int row = index.row();
|
int row = index.row();
|
||||||
if(index.isValid() && row >= 0 && row < m_accounts.size())
|
if(index.isValid() && row >= 0 && row < m_accounts.size())
|
||||||
@ -96,19 +97,19 @@ void MojangAccountList::removeAccount(QModelIndex index)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MojangAccountPtr MojangAccountList::activeAccount() const
|
MinecraftAccountPtr AccountList::activeAccount() const
|
||||||
{
|
{
|
||||||
return m_activeAccount;
|
return m_activeAccount;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MojangAccountList::setActiveAccount(const QString &username)
|
void AccountList::setActiveAccount(const QString &profileId)
|
||||||
{
|
{
|
||||||
if (username.isEmpty() && m_activeAccount)
|
if (profileId.isEmpty() && m_activeAccount)
|
||||||
{
|
{
|
||||||
int idx = 0;
|
int idx = 0;
|
||||||
auto prevActiveAcc = m_activeAccount;
|
auto prevActiveAcc = m_activeAccount;
|
||||||
m_activeAccount = nullptr;
|
m_activeAccount = nullptr;
|
||||||
for (MojangAccountPtr account : m_accounts)
|
for (MinecraftAccountPtr account : m_accounts)
|
||||||
{
|
{
|
||||||
if (account == prevActiveAcc)
|
if (account == prevActiveAcc)
|
||||||
{
|
{
|
||||||
@ -125,9 +126,9 @@ void MojangAccountList::setActiveAccount(const QString &username)
|
|||||||
auto newActiveAccount = m_activeAccount;
|
auto newActiveAccount = m_activeAccount;
|
||||||
int newActiveAccountIdx = -1;
|
int newActiveAccountIdx = -1;
|
||||||
int idx = 0;
|
int idx = 0;
|
||||||
for (MojangAccountPtr account : m_accounts)
|
for (MinecraftAccountPtr account : m_accounts)
|
||||||
{
|
{
|
||||||
if (account->username() == username)
|
if (account->profileId() == profileId)
|
||||||
{
|
{
|
||||||
newActiveAccount = account;
|
newActiveAccount = account;
|
||||||
newActiveAccountIdx = idx;
|
newActiveAccountIdx = idx;
|
||||||
@ -148,13 +149,13 @@ void MojangAccountList::setActiveAccount(const QString &username)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MojangAccountList::accountChanged()
|
void AccountList::accountChanged()
|
||||||
{
|
{
|
||||||
// the list changed. there is no doubt.
|
// the list changed. there is no doubt.
|
||||||
onListChanged();
|
onListChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MojangAccountList::onListChanged()
|
void AccountList::onListChanged()
|
||||||
{
|
{
|
||||||
if (m_autosave)
|
if (m_autosave)
|
||||||
// TODO: Alert the user if this fails.
|
// TODO: Alert the user if this fails.
|
||||||
@ -163,7 +164,7 @@ void MojangAccountList::onListChanged()
|
|||||||
emit listChanged();
|
emit listChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MojangAccountList::onActiveChanged()
|
void AccountList::onActiveChanged()
|
||||||
{
|
{
|
||||||
if (m_autosave)
|
if (m_autosave)
|
||||||
saveList();
|
saveList();
|
||||||
@ -171,12 +172,12 @@ void MojangAccountList::onActiveChanged()
|
|||||||
emit activeAccountChanged();
|
emit activeAccountChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
int MojangAccountList::count() const
|
int AccountList::count() const
|
||||||
{
|
{
|
||||||
return m_accounts.count();
|
return m_accounts.count();
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariant MojangAccountList::data(const QModelIndex &index, int role) const
|
QVariant AccountList::data(const QModelIndex &index, int role) const
|
||||||
{
|
{
|
||||||
if (!index.isValid())
|
if (!index.isValid())
|
||||||
return QVariant();
|
return QVariant();
|
||||||
@ -184,7 +185,7 @@ QVariant MojangAccountList::data(const QModelIndex &index, int role) const
|
|||||||
if (index.row() > count())
|
if (index.row() > count())
|
||||||
return QVariant();
|
return QVariant();
|
||||||
|
|
||||||
MojangAccountPtr account = at(index.row());
|
MinecraftAccountPtr account = at(index.row());
|
||||||
|
|
||||||
switch (role)
|
switch (role)
|
||||||
{
|
{
|
||||||
@ -192,14 +193,24 @@ QVariant MojangAccountList::data(const QModelIndex &index, int role) const
|
|||||||
switch (index.column())
|
switch (index.column())
|
||||||
{
|
{
|
||||||
case NameColumn:
|
case NameColumn:
|
||||||
return account->username();
|
return account->accountDisplayString();
|
||||||
|
|
||||||
|
case TypeColumn: {
|
||||||
|
auto typeStr = account->typeString();
|
||||||
|
typeStr[0] = typeStr[0].toUpper();
|
||||||
|
return typeStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
case ProfileNameColumn: {
|
||||||
|
return account->profileName();
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
|
|
||||||
case Qt::ToolTipRole:
|
case Qt::ToolTipRole:
|
||||||
return account->username();
|
return account->accountDisplayString();
|
||||||
|
|
||||||
case PointerRole:
|
case PointerRole:
|
||||||
return qVariantFromValue(account);
|
return qVariantFromValue(account);
|
||||||
@ -207,7 +218,7 @@ QVariant MojangAccountList::data(const QModelIndex &index, int role) const
|
|||||||
case Qt::CheckStateRole:
|
case Qt::CheckStateRole:
|
||||||
switch (index.column())
|
switch (index.column())
|
||||||
{
|
{
|
||||||
case ActiveColumn:
|
case NameColumn:
|
||||||
return account == m_activeAccount ? Qt::Checked : Qt::Unchecked;
|
return account == m_activeAccount ? Qt::Checked : Qt::Unchecked;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,19 +227,19 @@ QVariant MojangAccountList::data(const QModelIndex &index, int role) const
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariant MojangAccountList::headerData(int section, Qt::Orientation orientation, int role) const
|
QVariant AccountList::headerData(int section, Qt::Orientation orientation, int role) const
|
||||||
{
|
{
|
||||||
switch (role)
|
switch (role)
|
||||||
{
|
{
|
||||||
case Qt::DisplayRole:
|
case Qt::DisplayRole:
|
||||||
switch (section)
|
switch (section)
|
||||||
{
|
{
|
||||||
case ActiveColumn:
|
|
||||||
return tr("Active?");
|
|
||||||
|
|
||||||
case NameColumn:
|
case NameColumn:
|
||||||
return tr("Name");
|
return tr("Account");
|
||||||
|
case TypeColumn:
|
||||||
|
return tr("Type");
|
||||||
|
case ProfileNameColumn:
|
||||||
|
return tr("Profile");
|
||||||
default:
|
default:
|
||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
@ -237,8 +248,11 @@ QVariant MojangAccountList::headerData(int section, Qt::Orientation orientation,
|
|||||||
switch (section)
|
switch (section)
|
||||||
{
|
{
|
||||||
case NameColumn:
|
case NameColumn:
|
||||||
return tr("The name of the version.");
|
return tr("User name of the account.");
|
||||||
|
case TypeColumn:
|
||||||
|
return tr("Type of the account - Mojang or MSA.");
|
||||||
|
case ProfileNameColumn:
|
||||||
|
return tr("Name of the Minecraft profile associated with the account.");
|
||||||
default:
|
default:
|
||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
@ -248,18 +262,18 @@ QVariant MojangAccountList::headerData(int section, Qt::Orientation orientation,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int MojangAccountList::rowCount(const QModelIndex &) const
|
int AccountList::rowCount(const QModelIndex &) const
|
||||||
{
|
{
|
||||||
// Return count
|
// Return count
|
||||||
return count();
|
return count();
|
||||||
}
|
}
|
||||||
|
|
||||||
int MojangAccountList::columnCount(const QModelIndex &) const
|
int AccountList::columnCount(const QModelIndex &) const
|
||||||
{
|
{
|
||||||
return 2;
|
return NUM_COLUMNS;
|
||||||
}
|
}
|
||||||
|
|
||||||
Qt::ItemFlags MojangAccountList::flags(const QModelIndex &index) const
|
Qt::ItemFlags AccountList::flags(const QModelIndex &index) const
|
||||||
{
|
{
|
||||||
if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid())
|
if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid())
|
||||||
{
|
{
|
||||||
@ -269,7 +283,7 @@ Qt::ItemFlags MojangAccountList::flags(const QModelIndex &index) const
|
|||||||
return Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable;
|
return Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MojangAccountList::setData(const QModelIndex &index, const QVariant &value, int role)
|
bool AccountList::setData(const QModelIndex &index, const QVariant &value, int role)
|
||||||
{
|
{
|
||||||
if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid())
|
if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid())
|
||||||
{
|
{
|
||||||
@ -280,8 +294,8 @@ bool MojangAccountList::setData(const QModelIndex &index, const QVariant &value,
|
|||||||
{
|
{
|
||||||
if(value == Qt::Checked)
|
if(value == Qt::Checked)
|
||||||
{
|
{
|
||||||
MojangAccountPtr account = this->at(index.row());
|
MinecraftAccountPtr account = at(index.row());
|
||||||
this->setActiveAccount(account->username());
|
setActiveAccount(account->profileId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -289,31 +303,21 @@ bool MojangAccountList::setData(const QModelIndex &index, const QVariant &value,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MojangAccountList::updateListData(QList<MojangAccountPtr> versions)
|
bool AccountList::loadList()
|
||||||
{
|
{
|
||||||
beginResetModel();
|
if (m_listFilePath.isEmpty())
|
||||||
m_accounts = versions;
|
|
||||||
endResetModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool MojangAccountList::loadList(const QString &filePath)
|
|
||||||
{
|
|
||||||
QString path = filePath;
|
|
||||||
if (path.isEmpty())
|
|
||||||
path = m_listFilePath;
|
|
||||||
if (path.isEmpty())
|
|
||||||
{
|
{
|
||||||
qCritical() << "Can't load Mojang account list. No file path given and no default set.";
|
qCritical() << "Can't load Mojang account list. No file path given and no default set.";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
QFile file(path);
|
QFile file(m_listFilePath);
|
||||||
|
|
||||||
// Try to open the file and fail if we can't.
|
// Try to open the file and fail if we can't.
|
||||||
// TODO: We should probably report this error to the user.
|
// TODO: We should probably report this error to the user.
|
||||||
if (!file.open(QIODevice::ReadOnly))
|
if (!file.open(QIODevice::ReadOnly))
|
||||||
{
|
{
|
||||||
qCritical() << QString("Failed to read the account list file (%1).").arg(path).toUtf8();
|
qCritical() << QString("Failed to read the account list file (%1).").arg(m_listFilePath).toUtf8();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -343,121 +347,168 @@ bool MojangAccountList::loadList(const QString &filePath)
|
|||||||
QJsonObject root = jsonDoc.object();
|
QJsonObject root = jsonDoc.object();
|
||||||
|
|
||||||
// Make sure the format version matches.
|
// Make sure the format version matches.
|
||||||
if (root.value("formatVersion").toVariant().toInt() != ACCOUNT_LIST_FORMAT_VERSION)
|
auto listVersion = root.value("formatVersion").toVariant().toInt();
|
||||||
{
|
switch(listVersion) {
|
||||||
|
case AccountListVersion::MojangOnly: {
|
||||||
|
return loadV2(root);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case AccountListVersion::MojangMSA: {
|
||||||
|
return loadV3(root);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default: {
|
||||||
QString newName = "accounts-old.json";
|
QString newName = "accounts-old.json";
|
||||||
qWarning() << "Format version mismatch when loading account list. Existing one will be renamed to"
|
qWarning() << "Unknown format version when loading account list. Existing one will be renamed to" << newName;
|
||||||
<< newName;
|
|
||||||
|
|
||||||
// Attempt to rename the old version.
|
// Attempt to rename the old version.
|
||||||
file.rename(newName);
|
file.rename(newName);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Now, load the accounts array.
|
bool AccountList::loadV2(QJsonObject& root) {
|
||||||
beginResetModel();
|
beginResetModel();
|
||||||
|
auto activeUserName = root.value("activeAccount").toString("");
|
||||||
QJsonArray accounts = root.value("accounts").toArray();
|
QJsonArray accounts = root.value("accounts").toArray();
|
||||||
for (QJsonValue accountVal : accounts)
|
for (QJsonValue accountVal : accounts)
|
||||||
{
|
{
|
||||||
QJsonObject accountObj = accountVal.toObject();
|
QJsonObject accountObj = accountVal.toObject();
|
||||||
MojangAccountPtr account = MojangAccount::loadFromJson(accountObj);
|
MinecraftAccountPtr account = MinecraftAccount::loadFromJsonV2(accountObj);
|
||||||
if (account.get() != nullptr)
|
if (account.get() != nullptr)
|
||||||
{
|
{
|
||||||
connect(account.get(), SIGNAL(changed()), SLOT(accountChanged()));
|
auto profileId = account->profileId();
|
||||||
|
if(!profileId.size()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if(findAccountByProfileId(profileId) != -1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
connect(account.get(), &MinecraftAccount::changed, this, &AccountList::accountChanged);
|
||||||
m_accounts.append(account);
|
m_accounts.append(account);
|
||||||
|
if (activeUserName.size() && account->mojangUserName() == activeUserName) {
|
||||||
|
m_activeAccount = account;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
qWarning() << "Failed to load an account.";
|
qWarning() << "Failed to load an account.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Load the active account.
|
|
||||||
m_activeAccount = findAccount(root.value("activeAccount").toString(""));
|
|
||||||
endResetModel();
|
endResetModel();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MojangAccountList::saveList(const QString &filePath)
|
bool AccountList::loadV3(QJsonObject& root) {
|
||||||
|
beginResetModel();
|
||||||
|
QJsonArray accounts = root.value("accounts").toArray();
|
||||||
|
for (QJsonValue accountVal : accounts)
|
||||||
{
|
{
|
||||||
QString path(filePath);
|
QJsonObject accountObj = accountVal.toObject();
|
||||||
if (path.isEmpty())
|
MinecraftAccountPtr account = MinecraftAccount::loadFromJsonV3(accountObj);
|
||||||
path = m_listFilePath;
|
if (account.get() != nullptr)
|
||||||
if (path.isEmpty())
|
{
|
||||||
|
auto profileId = account->profileId();
|
||||||
|
if(!profileId.size()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if(findAccountByProfileId(profileId) != -1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
connect(account.get(), &MinecraftAccount::changed, this, &AccountList::accountChanged);
|
||||||
|
m_accounts.append(account);
|
||||||
|
if(accountObj.value("active").toBool(false)) {
|
||||||
|
m_activeAccount = account;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qWarning() << "Failed to load an account.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endResetModel();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool AccountList::saveList()
|
||||||
|
{
|
||||||
|
if (m_listFilePath.isEmpty())
|
||||||
{
|
{
|
||||||
qCritical() << "Can't save Mojang account list. No file path given and no default set.";
|
qCritical() << "Can't save Mojang account list. No file path given and no default set.";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure the parent folder exists
|
// make sure the parent folder exists
|
||||||
if(!FS::ensureFilePathExists(path))
|
if(!FS::ensureFilePathExists(m_listFilePath))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// make sure the file wasn't overwritten with a folder before (fixes a bug)
|
// make sure the file wasn't overwritten with a folder before (fixes a bug)
|
||||||
QFileInfo finfo(path);
|
QFileInfo finfo(m_listFilePath);
|
||||||
if(finfo.isDir())
|
if(finfo.isDir())
|
||||||
{
|
{
|
||||||
QDir badDir(path);
|
QDir badDir(m_listFilePath);
|
||||||
badDir.removeRecursively();
|
badDir.removeRecursively();
|
||||||
}
|
}
|
||||||
|
|
||||||
qDebug() << "Writing account list to" << path;
|
qDebug() << "Writing account list to" << m_listFilePath;
|
||||||
|
|
||||||
qDebug() << "Building JSON data structure.";
|
qDebug() << "Building JSON data structure.";
|
||||||
// Build the JSON document to write to the list file.
|
// Build the JSON document to write to the list file.
|
||||||
QJsonObject root;
|
QJsonObject root;
|
||||||
|
|
||||||
root.insert("formatVersion", ACCOUNT_LIST_FORMAT_VERSION);
|
root.insert("formatVersion", AccountListVersion::MojangMSA);
|
||||||
|
|
||||||
// Build a list of accounts.
|
// Build a list of accounts.
|
||||||
qDebug() << "Building account array.";
|
qDebug() << "Building account array.";
|
||||||
QJsonArray accounts;
|
QJsonArray accounts;
|
||||||
for (MojangAccountPtr account : m_accounts)
|
for (MinecraftAccountPtr account : m_accounts)
|
||||||
{
|
{
|
||||||
QJsonObject accountObj = account->saveToJson();
|
QJsonObject accountObj = account->saveToJson();
|
||||||
|
if(m_activeAccount == account) {
|
||||||
|
accountObj["active"] = true;
|
||||||
|
}
|
||||||
accounts.append(accountObj);
|
accounts.append(accountObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert the account list into the root object.
|
// Insert the account list into the root object.
|
||||||
root.insert("accounts", accounts);
|
root.insert("accounts", accounts);
|
||||||
|
|
||||||
if(m_activeAccount)
|
|
||||||
{
|
|
||||||
// Save the active account.
|
|
||||||
root.insert("activeAccount", m_activeAccount->username());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a JSON document object to convert our JSON to bytes.
|
// Create a JSON document object to convert our JSON to bytes.
|
||||||
QJsonDocument doc(root);
|
QJsonDocument doc(root);
|
||||||
|
|
||||||
// Now that we're done building the JSON object, we can write it to the file.
|
// Now that we're done building the JSON object, we can write it to the file.
|
||||||
qDebug() << "Writing account list to file.";
|
qDebug() << "Writing account list to file.";
|
||||||
QFile file(path);
|
QSaveFile file(m_listFilePath);
|
||||||
|
|
||||||
// Try to open the file and fail if we can't.
|
// Try to open the file and fail if we can't.
|
||||||
// TODO: We should probably report this error to the user.
|
// TODO: We should probably report this error to the user.
|
||||||
if (!file.open(QIODevice::WriteOnly))
|
if (!file.open(QIODevice::WriteOnly))
|
||||||
{
|
{
|
||||||
qCritical() << QString("Failed to read the account list file (%1).").arg(path).toUtf8();
|
qCritical() << QString("Failed to read the account list file (%1).").arg(m_listFilePath).toUtf8();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the JSON to the file.
|
// Write the JSON to the file.
|
||||||
file.write(doc.toJson());
|
file.write(doc.toJson());
|
||||||
file.setPermissions(QFile::ReadOwner|QFile::WriteOwner|QFile::ReadUser|QFile::WriteUser);
|
file.setPermissions(QFile::ReadOwner|QFile::WriteOwner|QFile::ReadUser|QFile::WriteUser);
|
||||||
file.close();
|
if(file.commit()) {
|
||||||
|
qDebug() << "Saved account list to" << m_listFilePath;
|
||||||
qDebug() << "Saved account list to" << path;
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
qDebug() << "Failed to save accounts to" << m_listFilePath;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void MojangAccountList::setListFilePath(QString path, bool autosave)
|
void AccountList::setListFilePath(QString path, bool autosave)
|
||||||
{
|
{
|
||||||
m_listFilePath = path;
|
m_listFilePath = path;
|
||||||
m_autosave = autosave;
|
m_autosave = autosave;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MojangAccountList::anyAccountIsValid()
|
bool AccountList::anyAccountIsValid()
|
||||||
{
|
{
|
||||||
for(auto account:m_accounts)
|
for(auto account:m_accounts)
|
||||||
{
|
{
|
118
launcher/minecraft/auth/AccountList.h
Normal file
118
launcher/minecraft/auth/AccountList.h
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
/* Copyright 2013-2021 MultiMC Contributors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "MinecraftAccount.h"
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QVariant>
|
||||||
|
#include <QAbstractListModel>
|
||||||
|
#include <QSharedPointer>
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* List of available Mojang accounts.
|
||||||
|
* This should be loaded in the background by MultiMC on startup.
|
||||||
|
*/
|
||||||
|
class AccountList : public QAbstractListModel
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
enum ModelRoles
|
||||||
|
{
|
||||||
|
PointerRole = 0x34B1CB48
|
||||||
|
};
|
||||||
|
|
||||||
|
enum VListColumns
|
||||||
|
{
|
||||||
|
// TODO: Add icon column.
|
||||||
|
NameColumn = 0,
|
||||||
|
ProfileNameColumn,
|
||||||
|
TypeColumn,
|
||||||
|
|
||||||
|
NUM_COLUMNS
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit AccountList(QObject *parent = 0);
|
||||||
|
|
||||||
|
const MinecraftAccountPtr at(int i) const;
|
||||||
|
int count() const;
|
||||||
|
|
||||||
|
//////// List Model Functions ////////
|
||||||
|
QVariant data(const QModelIndex &index, int role) const override;
|
||||||
|
virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
|
||||||
|
virtual int rowCount(const QModelIndex &parent) const override;
|
||||||
|
virtual int columnCount(const QModelIndex &parent) const override;
|
||||||
|
virtual Qt::ItemFlags flags(const QModelIndex &index) const override;
|
||||||
|
virtual bool setData(const QModelIndex &index, const QVariant &value, int role) override;
|
||||||
|
|
||||||
|
void addAccount(const MinecraftAccountPtr account);
|
||||||
|
void removeAccount(QModelIndex index);
|
||||||
|
int findAccountByProfileId(const QString &profileId) const;
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* Sets the path to load/save the list file from/to.
|
||||||
|
* If autosave is true, this list will automatically save to the given path whenever it changes.
|
||||||
|
* THIS FUNCTION DOES NOT LOAD THE LIST. If you set autosave, be sure to call loadList() immediately
|
||||||
|
* after calling this function to ensure an autosaved change doesn't overwrite the list you intended
|
||||||
|
* to load.
|
||||||
|
*/
|
||||||
|
void setListFilePath(QString path, bool autosave = false);
|
||||||
|
|
||||||
|
bool loadList();
|
||||||
|
bool loadV2(QJsonObject &root);
|
||||||
|
bool loadV3(QJsonObject &root);
|
||||||
|
bool saveList();
|
||||||
|
|
||||||
|
MinecraftAccountPtr activeAccount() const;
|
||||||
|
void setActiveAccount(const QString &profileId);
|
||||||
|
bool anyAccountIsValid();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void listChanged();
|
||||||
|
void activeAccountChanged();
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
/**
|
||||||
|
* This is called when one of the accounts changes and the list needs to be updated
|
||||||
|
*/
|
||||||
|
void accountChanged();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
/*!
|
||||||
|
* Called whenever the list changes.
|
||||||
|
* This emits the listChanged() signal and autosaves the list (if autosave is enabled).
|
||||||
|
*/
|
||||||
|
void onListChanged();
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* Called whenever the active account changes.
|
||||||
|
* Emits the activeAccountChanged() signal and autosaves the list if enabled.
|
||||||
|
*/
|
||||||
|
void onActiveChanged();
|
||||||
|
|
||||||
|
QList<MinecraftAccountPtr> m_accounts;
|
||||||
|
|
||||||
|
MinecraftAccountPtr m_activeAccount;
|
||||||
|
|
||||||
|
//! Path to the account list file. Empty string if there isn't one.
|
||||||
|
QString m_listFilePath;
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* If true, the account list will automatically save to the account list path when it changes.
|
||||||
|
* Ignored if m_listFilePath is blank.
|
||||||
|
*/
|
||||||
|
bool m_autosave = false;
|
||||||
|
};
|
69
launcher/minecraft/auth/AccountTask.cpp
Normal file
69
launcher/minecraft/auth/AccountTask.cpp
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
/* Copyright 2013-2021 MultiMC Contributors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "AccountTask.h"
|
||||||
|
#include "MinecraftAccount.h"
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QByteArray>
|
||||||
|
|
||||||
|
#include <Env.h>
|
||||||
|
|
||||||
|
#include <BuildConfig.h>
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
AccountTask::AccountTask(AccountData *data, QObject *parent)
|
||||||
|
: Task(parent), m_data(data)
|
||||||
|
{
|
||||||
|
changeState(STATE_CREATED);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString AccountTask::getStateMessage() const
|
||||||
|
{
|
||||||
|
switch (m_accountState)
|
||||||
|
{
|
||||||
|
case STATE_CREATED:
|
||||||
|
return "Waiting...";
|
||||||
|
case STATE_WORKING:
|
||||||
|
return tr("Sending request to auth servers...");
|
||||||
|
case STATE_SUCCEEDED:
|
||||||
|
return tr("Authentication task succeeded.");
|
||||||
|
case STATE_FAILED_SOFT:
|
||||||
|
return tr("Failed to contact the authentication server.");
|
||||||
|
case STATE_FAILED_HARD:
|
||||||
|
return tr("Failed to authenticate.");
|
||||||
|
default:
|
||||||
|
return tr("...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AccountTask::changeState(AccountTask::State newState, QString reason)
|
||||||
|
{
|
||||||
|
m_accountState = newState;
|
||||||
|
setStatus(getStateMessage());
|
||||||
|
if (newState == STATE_SUCCEEDED)
|
||||||
|
{
|
||||||
|
emitSucceeded();
|
||||||
|
}
|
||||||
|
else if (newState == STATE_FAILED_HARD || newState == STATE_FAILED_SOFT)
|
||||||
|
{
|
||||||
|
emitFailed(reason);
|
||||||
|
}
|
||||||
|
}
|
@ -22,19 +22,17 @@
|
|||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <qsslerror.h>
|
#include <qsslerror.h>
|
||||||
|
|
||||||
#include "MojangAccount.h"
|
#include "MinecraftAccount.h"
|
||||||
|
|
||||||
class QNetworkReply;
|
class QNetworkReply;
|
||||||
|
|
||||||
/**
|
class AccountTask : public Task
|
||||||
* A Yggdrasil task is a task that performs an operation on a given mojang account.
|
|
||||||
*/
|
|
||||||
class YggdrasilTask : public Task
|
|
||||||
{
|
{
|
||||||
|
friend class AuthContext;
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit YggdrasilTask(MojangAccount * account, QObject *parent = 0);
|
explicit AccountTask(AccountData * data, QObject *parent = 0);
|
||||||
virtual ~YggdrasilTask() {};
|
virtual ~AccountTask() {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* assign a session to this task. the session will be filled with required infomration
|
* assign a session to this task. the session will be filled with required infomration
|
||||||
@ -52,7 +50,7 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class describing a Yggdrasil error response.
|
* Class describing a Account error response.
|
||||||
*/
|
*/
|
||||||
struct Error
|
struct Error
|
||||||
{
|
{
|
||||||
@ -75,46 +73,18 @@ public:
|
|||||||
enum State
|
enum State
|
||||||
{
|
{
|
||||||
STATE_CREATED,
|
STATE_CREATED,
|
||||||
STATE_SENDING_REQUEST,
|
STATE_WORKING,
|
||||||
STATE_PROCESSING_RESPONSE,
|
|
||||||
STATE_FAILED_SOFT, //!< soft failure. this generally means the user auth details haven't been invalidated
|
STATE_FAILED_SOFT, //!< soft failure. this generally means the user auth details haven't been invalidated
|
||||||
STATE_FAILED_HARD, //!< hard failure. auth is invalid
|
STATE_FAILED_HARD, //!< hard failure. auth is invalid
|
||||||
STATE_SUCCEEDED
|
STATE_SUCCEEDED
|
||||||
} m_state = STATE_CREATED;
|
} m_accountState = STATE_CREATED;
|
||||||
|
|
||||||
|
State accountState() {
|
||||||
|
return m_accountState;
|
||||||
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|
||||||
virtual void executeTask() override;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the JSON object that will be sent to the authentication server.
|
|
||||||
* Should be overridden by subclasses.
|
|
||||||
*/
|
|
||||||
virtual QJsonObject getRequestContent() const = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the endpoint to POST to.
|
|
||||||
* No leading slash.
|
|
||||||
*/
|
|
||||||
virtual QString getEndpoint() const = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Processes the response received from the server.
|
|
||||||
* If an error occurred, this should emit a failed signal and return false.
|
|
||||||
* If Yggdrasil gave an error response, it should call setError() first, and then return false.
|
|
||||||
* Otherwise, it should return true.
|
|
||||||
* Note: If the response from the server was blank, and the HTTP code was 200, this function is called with
|
|
||||||
* an empty QJsonObject.
|
|
||||||
*/
|
|
||||||
virtual void processResponse(QJsonObject responseData) = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Processes an error response received from the server.
|
|
||||||
* The default implementation will read data from Yggdrasil's standard error response format and set it as this task's Error.
|
|
||||||
* \returns a QString error message that will be passed to emitFailed.
|
|
||||||
*/
|
|
||||||
virtual void processError(QJsonObject responseData);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the state message for the given state.
|
* Returns the state message for the given state.
|
||||||
* Used to set the status message for the task.
|
* Used to set the status message for the task.
|
||||||
@ -122,30 +92,12 @@ protected:
|
|||||||
*/
|
*/
|
||||||
virtual QString getStateMessage() const;
|
virtual QString getStateMessage() const;
|
||||||
|
|
||||||
protected
|
protected slots:
|
||||||
slots:
|
|
||||||
void processReply();
|
|
||||||
void refreshTimers(qint64, qint64);
|
|
||||||
void heartbeat();
|
|
||||||
void sslErrors(QList<QSslError>);
|
|
||||||
|
|
||||||
void changeState(State newState, QString reason=QString());
|
void changeState(State newState, QString reason=QString());
|
||||||
public
|
|
||||||
slots:
|
|
||||||
virtual bool abort() override;
|
|
||||||
void abortByTimeout();
|
|
||||||
State state();
|
|
||||||
protected:
|
protected:
|
||||||
// FIXME: segfault disaster waiting to happen
|
// FIXME: segfault disaster waiting to happen
|
||||||
MojangAccount *m_account = nullptr;
|
AccountData *m_data = nullptr;
|
||||||
QNetworkReply *m_netReply = nullptr;
|
|
||||||
std::shared_ptr<Error> m_error;
|
std::shared_ptr<Error> m_error;
|
||||||
QTimer timeout_keeper;
|
|
||||||
QTimer counter;
|
|
||||||
int count = 0; // num msec since time reset
|
|
||||||
|
|
||||||
const int timeout_max = 30000;
|
|
||||||
const int time_step = 50;
|
|
||||||
|
|
||||||
AuthSessionPtr m_session;
|
AuthSessionPtr m_session;
|
||||||
};
|
};
|
@ -7,11 +7,13 @@
|
|||||||
QString AuthSession::serializeUserProperties()
|
QString AuthSession::serializeUserProperties()
|
||||||
{
|
{
|
||||||
QJsonObject userAttrs;
|
QJsonObject userAttrs;
|
||||||
|
/*
|
||||||
for (auto key : u.properties.keys())
|
for (auto key : u.properties.keys())
|
||||||
{
|
{
|
||||||
auto array = QJsonArray::fromStringList(u.properties.values(key));
|
auto array = QJsonArray::fromStringList(u.properties.values(key));
|
||||||
userAttrs.insert(key, array);
|
userAttrs.insert(key, array);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
QJsonDocument value(userAttrs);
|
QJsonDocument value(userAttrs);
|
||||||
return value.toJson(QJsonDocument::Compact);
|
return value.toJson(QJsonDocument::Compact);
|
||||||
|
|
||||||
|
@ -4,13 +4,7 @@
|
|||||||
#include <QMultiMap>
|
#include <QMultiMap>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
class MojangAccount;
|
class MinecraftAccount;
|
||||||
|
|
||||||
struct User
|
|
||||||
{
|
|
||||||
QString id;
|
|
||||||
QMultiMap<QString, QString> properties;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct AuthSession
|
struct AuthSession
|
||||||
{
|
{
|
||||||
@ -21,13 +15,12 @@ struct AuthSession
|
|||||||
enum Status
|
enum Status
|
||||||
{
|
{
|
||||||
Undetermined,
|
Undetermined,
|
||||||
|
RequiresOAuth,
|
||||||
RequiresPassword,
|
RequiresPassword,
|
||||||
PlayableOffline,
|
PlayableOffline,
|
||||||
PlayableOnline
|
PlayableOnline
|
||||||
} status = Undetermined;
|
} status = Undetermined;
|
||||||
|
|
||||||
User u;
|
|
||||||
|
|
||||||
// client token
|
// client token
|
||||||
QString client_token;
|
QString client_token;
|
||||||
// account user name
|
// account user name
|
||||||
@ -46,7 +39,7 @@ struct AuthSession
|
|||||||
bool auth_server_online = false;
|
bool auth_server_online = false;
|
||||||
// Did the user request online mode?
|
// Did the user request online mode?
|
||||||
bool wants_online = true;
|
bool wants_online = true;
|
||||||
std::shared_ptr<MojangAccount> m_accountPtr;
|
std::shared_ptr<MinecraftAccount> m_accountPtr;
|
||||||
};
|
};
|
||||||
|
|
||||||
typedef std::shared_ptr<AuthSession> AuthSessionPtr;
|
typedef std::shared_ptr<AuthSession> AuthSessionPtr;
|
||||||
|
303
launcher/minecraft/auth/MinecraftAccount.cpp
Normal file
303
launcher/minecraft/auth/MinecraftAccount.cpp
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
/* Copyright 2013-2021 MultiMC Contributors
|
||||||
|
*
|
||||||
|
* Authors: Orochimarufan <orochimarufan.x3@gmail.com>
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "MinecraftAccount.h"
|
||||||
|
#include "flows/AuthContext.h"
|
||||||
|
|
||||||
|
#include <QUuid>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QRegExp>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
#include <QPainter>
|
||||||
|
#include <minecraft/auth/flows/MSASilent.h>
|
||||||
|
#include <minecraft/auth/flows/MSAInteractive.h>
|
||||||
|
|
||||||
|
#include <minecraft/auth/flows/MojangRefresh.h>
|
||||||
|
#include <minecraft/auth/flows/MojangLogin.h>
|
||||||
|
|
||||||
|
MinecraftAccountPtr MinecraftAccount::loadFromJsonV2(const QJsonObject& json) {
|
||||||
|
MinecraftAccountPtr account(new MinecraftAccount());
|
||||||
|
if(account->data.resumeStateFromV2(json)) {
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json) {
|
||||||
|
MinecraftAccountPtr account(new MinecraftAccount());
|
||||||
|
if(account->data.resumeStateFromV3(json)) {
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
MinecraftAccountPtr MinecraftAccount::createFromUsername(const QString &username)
|
||||||
|
{
|
||||||
|
MinecraftAccountPtr account(new MinecraftAccount());
|
||||||
|
account->data.type = AccountType::Mojang;
|
||||||
|
account->data.yggdrasilToken.extra["userName"] = username;
|
||||||
|
account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegExp("[{}-]"));
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
MinecraftAccountPtr MinecraftAccount::createBlankMSA()
|
||||||
|
{
|
||||||
|
MinecraftAccountPtr account(new MinecraftAccount());
|
||||||
|
account->data.type = AccountType::MSA;
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
QJsonObject MinecraftAccount::saveToJson() const
|
||||||
|
{
|
||||||
|
return data.saveState();
|
||||||
|
}
|
||||||
|
|
||||||
|
AccountStatus MinecraftAccount::accountStatus() const {
|
||||||
|
if(data.type == AccountType::Mojang) {
|
||||||
|
if (data.accessToken().isEmpty()) {
|
||||||
|
return NotVerified;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return Verified;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// MSA
|
||||||
|
// FIXME: this is extremely crude and probably wrong
|
||||||
|
if(data.msaToken.token.isEmpty()) {
|
||||||
|
return NotVerified;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return Verified;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QPixmap MinecraftAccount::getFace() const {
|
||||||
|
QPixmap skinTexture;
|
||||||
|
if(!skinTexture.loadFromData(data.minecraftProfile.skin.data, "PNG")) {
|
||||||
|
return QPixmap();
|
||||||
|
}
|
||||||
|
QPixmap skin = QPixmap(8, 8);
|
||||||
|
QPainter painter(&skin);
|
||||||
|
painter.drawPixmap(0, 0, skinTexture.copy(8, 8, 8, 8));
|
||||||
|
painter.drawPixmap(0, 0, skinTexture.copy(40, 8, 8, 8));
|
||||||
|
return skin.scaled(64, 64, Qt::KeepAspectRatio);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::shared_ptr<AccountTask> MinecraftAccount::login(AuthSessionPtr session, QString password)
|
||||||
|
{
|
||||||
|
Q_ASSERT(m_currentTask.get() == nullptr);
|
||||||
|
|
||||||
|
// take care of the true offline status
|
||||||
|
if (accountStatus() == NotVerified && password.isEmpty())
|
||||||
|
{
|
||||||
|
if (session)
|
||||||
|
{
|
||||||
|
session->status = AuthSession::RequiresPassword;
|
||||||
|
fillSession(session);
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(accountStatus() == Verified && !session->wants_online)
|
||||||
|
{
|
||||||
|
session->status = AuthSession::PlayableOffline;
|
||||||
|
session->auth_server_online = false;
|
||||||
|
fillSession(session);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (password.isEmpty())
|
||||||
|
{
|
||||||
|
m_currentTask.reset(new MojangRefresh(&data));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_currentTask.reset(new MojangLogin(&data, password));
|
||||||
|
}
|
||||||
|
m_currentTask->assignSession(session);
|
||||||
|
|
||||||
|
connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
|
||||||
|
connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
|
||||||
|
}
|
||||||
|
return m_currentTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<AccountTask> MinecraftAccount::loginMSA(AuthSessionPtr session) {
|
||||||
|
Q_ASSERT(m_currentTask.get() == nullptr);
|
||||||
|
|
||||||
|
if(accountStatus() == Verified && !session->wants_online)
|
||||||
|
{
|
||||||
|
session->status = AuthSession::PlayableOffline;
|
||||||
|
session->auth_server_online = false;
|
||||||
|
fillSession(session);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_currentTask.reset(new MSAInteractive(&data));
|
||||||
|
m_currentTask->assignSession(session);
|
||||||
|
|
||||||
|
connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
|
||||||
|
connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
|
||||||
|
}
|
||||||
|
return m_currentTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<AccountTask> MinecraftAccount::refresh(AuthSessionPtr session) {
|
||||||
|
Q_ASSERT(m_currentTask.get() == nullptr);
|
||||||
|
|
||||||
|
// take care of the true offline status
|
||||||
|
if (accountStatus() == NotVerified)
|
||||||
|
{
|
||||||
|
if (session)
|
||||||
|
{
|
||||||
|
if(data.type == AccountType::MSA) {
|
||||||
|
session->status = AuthSession::RequiresOAuth;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
session->status = AuthSession::RequiresPassword;
|
||||||
|
}
|
||||||
|
fillSession(session);
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(accountStatus() == Verified && !session->wants_online)
|
||||||
|
{
|
||||||
|
session->status = AuthSession::PlayableOffline;
|
||||||
|
session->auth_server_online = false;
|
||||||
|
fillSession(session);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if(data.type == AccountType::MSA) {
|
||||||
|
m_currentTask.reset(new MSASilent(&data));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
m_currentTask.reset(new MojangRefresh(&data));
|
||||||
|
}
|
||||||
|
m_currentTask->assignSession(session);
|
||||||
|
|
||||||
|
connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
|
||||||
|
connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
|
||||||
|
}
|
||||||
|
return m_currentTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void MinecraftAccount::authSucceeded()
|
||||||
|
{
|
||||||
|
auto session = m_currentTask->getAssignedSession();
|
||||||
|
if (session)
|
||||||
|
{
|
||||||
|
session->status =
|
||||||
|
session->wants_online ? AuthSession::PlayableOnline : AuthSession::PlayableOffline;
|
||||||
|
fillSession(session);
|
||||||
|
session->auth_server_online = true;
|
||||||
|
}
|
||||||
|
m_currentTask.reset();
|
||||||
|
emit changed();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MinecraftAccount::authFailed(QString reason)
|
||||||
|
{
|
||||||
|
auto session = m_currentTask->getAssignedSession();
|
||||||
|
// This is emitted when the yggdrasil tasks time out or are cancelled.
|
||||||
|
// -> we treat the error as no-op
|
||||||
|
if (m_currentTask->accountState() == AccountTask::STATE_FAILED_SOFT)
|
||||||
|
{
|
||||||
|
if (session)
|
||||||
|
{
|
||||||
|
session->status = accountStatus() == Verified ? AuthSession::PlayableOffline : AuthSession::RequiresPassword;
|
||||||
|
session->auth_server_online = false;
|
||||||
|
fillSession(session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// FIXME: MSA ...
|
||||||
|
data.yggdrasilToken.token = QString();
|
||||||
|
data.yggdrasilToken.validity = Katabasis::Validity::None;
|
||||||
|
data.validity_ = Katabasis::Validity::None;
|
||||||
|
emit changed();
|
||||||
|
if (session)
|
||||||
|
{
|
||||||
|
session->status = AuthSession::RequiresPassword;
|
||||||
|
session->auth_server_online = true;
|
||||||
|
fillSession(session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_currentTask.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MinecraftAccount::fillSession(AuthSessionPtr session)
|
||||||
|
{
|
||||||
|
// the user name. you have to have an user name
|
||||||
|
// FIXME: not with MSA
|
||||||
|
session->username = data.userName();
|
||||||
|
// volatile auth token
|
||||||
|
session->access_token = data.accessToken();
|
||||||
|
// the semi-permanent client token
|
||||||
|
session->client_token = data.clientToken();
|
||||||
|
// profile name
|
||||||
|
session->player_name = data.profileName();
|
||||||
|
// profile ID
|
||||||
|
session->uuid = data.profileId();
|
||||||
|
// 'legacy' or 'mojang', depending on account type
|
||||||
|
session->user_type = typeString();
|
||||||
|
if (!session->access_token.isEmpty())
|
||||||
|
{
|
||||||
|
session->session = "token:" + data.accessToken() + ":" + data.profileId();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
session->session = "-";
|
||||||
|
}
|
||||||
|
session->m_accountPtr = shared_from_this();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MinecraftAccount::decrementUses()
|
||||||
|
{
|
||||||
|
Usable::decrementUses();
|
||||||
|
if(!isInUse())
|
||||||
|
{
|
||||||
|
emit changed();
|
||||||
|
// FIXME: we now need a better way to identify accounts...
|
||||||
|
qWarning() << "Profile" << data.profileId() << "is no longer in use.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MinecraftAccount::incrementUses()
|
||||||
|
{
|
||||||
|
bool wasInUse = isInUse();
|
||||||
|
Usable::incrementUses();
|
||||||
|
if(!wasInUse)
|
||||||
|
{
|
||||||
|
emit changed();
|
||||||
|
// FIXME: we now need a better way to identify accounts...
|
||||||
|
qWarning() << "Profile" << data.profileId() << "is now in use.";
|
||||||
|
}
|
||||||
|
}
|
@ -21,17 +21,19 @@
|
|||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QPair>
|
#include <QPair>
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
|
#include <QPixmap>
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include "AuthSession.h"
|
#include "AuthSession.h"
|
||||||
#include "Usable.h"
|
#include "Usable.h"
|
||||||
|
#include "AccountData.h"
|
||||||
|
|
||||||
class Task;
|
class Task;
|
||||||
class YggdrasilTask;
|
class AccountTask;
|
||||||
class MojangAccount;
|
class MinecraftAccount;
|
||||||
|
|
||||||
typedef std::shared_ptr<MojangAccount> MojangAccountPtr;
|
typedef std::shared_ptr<MinecraftAccount> MinecraftAccountPtr;
|
||||||
Q_DECLARE_METATYPE(MojangAccountPtr)
|
Q_DECLARE_METATYPE(MinecraftAccountPtr)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A profile within someone's Mojang account.
|
* A profile within someone's Mojang account.
|
||||||
@ -59,75 +61,90 @@ enum AccountStatus
|
|||||||
* Said information may include things such as that account's username, client token, and access
|
* Said information may include things such as that account's username, client token, and access
|
||||||
* token if the user chose to stay logged in.
|
* token if the user chose to stay logged in.
|
||||||
*/
|
*/
|
||||||
class MojangAccount :
|
class MinecraftAccount :
|
||||||
public QObject,
|
public QObject,
|
||||||
public Usable,
|
public Usable,
|
||||||
public std::enable_shared_from_this<MojangAccount>
|
public std::enable_shared_from_this<MinecraftAccount>
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public: /* construction */
|
public: /* construction */
|
||||||
//! Do not copy accounts. ever.
|
//! Do not copy accounts. ever.
|
||||||
explicit MojangAccount(const MojangAccount &other, QObject *parent) = delete;
|
explicit MinecraftAccount(const MinecraftAccount &other, QObject *parent) = delete;
|
||||||
|
|
||||||
//! Default constructor
|
//! Default constructor
|
||||||
explicit MojangAccount(QObject *parent = 0) : QObject(parent) {};
|
explicit MinecraftAccount(QObject *parent = 0) : QObject(parent) {};
|
||||||
|
|
||||||
//! Creates an empty account for the specified user name.
|
static MinecraftAccountPtr createFromUsername(const QString &username);
|
||||||
static MojangAccountPtr createFromUsername(const QString &username);
|
|
||||||
|
|
||||||
//! Loads a MojangAccount from the given JSON object.
|
static MinecraftAccountPtr createBlankMSA();
|
||||||
static MojangAccountPtr loadFromJson(const QJsonObject &json);
|
|
||||||
|
|
||||||
//! Saves a MojangAccount to a JSON object and returns it.
|
static MinecraftAccountPtr loadFromJsonV2(const QJsonObject &json);
|
||||||
|
static MinecraftAccountPtr loadFromJsonV3(const QJsonObject &json);
|
||||||
|
|
||||||
|
//! Saves a MinecraftAccount to a JSON object and returns it.
|
||||||
QJsonObject saveToJson() const;
|
QJsonObject saveToJson() const;
|
||||||
|
|
||||||
public: /* manipulation */
|
public: /* manipulation */
|
||||||
/**
|
|
||||||
* Sets the currently selected profile to the profile with the given ID string.
|
|
||||||
* If profileId is not in the list of available profiles, the function will simply return
|
|
||||||
* false.
|
|
||||||
*/
|
|
||||||
bool setCurrentProfile(const QString &profileId);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt to login. Empty password means we use the token.
|
* Attempt to login. Empty password means we use the token.
|
||||||
* If the attempt fails because we already are performing some task, it returns false.
|
* If the attempt fails because we already are performing some task, it returns false.
|
||||||
*/
|
*/
|
||||||
std::shared_ptr<YggdrasilTask> login(AuthSessionPtr session, QString password = QString());
|
std::shared_ptr<AccountTask> login(AuthSessionPtr session, QString password = QString());
|
||||||
void invalidateClientToken();
|
|
||||||
|
std::shared_ptr<AccountTask> loginMSA(AuthSessionPtr session);
|
||||||
|
|
||||||
|
std::shared_ptr<AccountTask> refresh(AuthSessionPtr session);
|
||||||
|
|
||||||
public: /* queries */
|
public: /* queries */
|
||||||
const QString &username() const
|
QString accountDisplayString() const {
|
||||||
{
|
return data.accountDisplayString();
|
||||||
return m_username;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const QString &clientToken() const
|
QString mojangUserName() const {
|
||||||
{
|
return data.userName();
|
||||||
return m_clientToken;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const QString &accessToken() const
|
QString accessToken() const {
|
||||||
{
|
return data.accessToken();
|
||||||
return m_accessToken;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const QList<AccountProfile> &profiles() const
|
QString profileId() const {
|
||||||
{
|
return data.profileId();
|
||||||
return m_profiles;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const User &user()
|
QString profileName() const {
|
||||||
{
|
return data.profileName();
|
||||||
return m_user;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//! Returns the currently selected profile (if none, returns nullptr)
|
QString typeString() const {
|
||||||
const AccountProfile *currentProfile() const;
|
switch(data.type) {
|
||||||
|
case AccountType::Mojang: {
|
||||||
|
if(data.legacy) {
|
||||||
|
return "legacy";
|
||||||
|
}
|
||||||
|
return "mojang";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case AccountType::MSA: {
|
||||||
|
return "msa";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default: {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QPixmap getFace() const;
|
||||||
|
|
||||||
//! Returns whether the account is NotVerified, Verified or Online
|
//! Returns whether the account is NotVerified, Verified or Online
|
||||||
AccountStatus accountStatus() const;
|
AccountStatus accountStatus() const;
|
||||||
|
|
||||||
|
AccountData * accountData() {
|
||||||
|
return &data;
|
||||||
|
}
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
/**
|
/**
|
||||||
* This signal is emitted when the account changes
|
* This signal is emitted when the account changes
|
||||||
@ -137,27 +154,10 @@ signals:
|
|||||||
// TODO: better signalling for the various possible state changes - especially errors
|
// TODO: better signalling for the various possible state changes - especially errors
|
||||||
|
|
||||||
protected: /* variables */
|
protected: /* variables */
|
||||||
QString m_username;
|
AccountData data;
|
||||||
|
|
||||||
// Used to identify the client - the user can have multiple clients for the same account
|
|
||||||
// Think: different launchers, all connecting to the same account/profile
|
|
||||||
QString m_clientToken;
|
|
||||||
|
|
||||||
// Blank if not logged in.
|
|
||||||
QString m_accessToken;
|
|
||||||
|
|
||||||
// Index of the selected profile within the list of available
|
|
||||||
// profiles. -1 if nothing is selected.
|
|
||||||
int m_currentProfile = -1;
|
|
||||||
|
|
||||||
// List of available profiles.
|
|
||||||
QList<AccountProfile> m_profiles;
|
|
||||||
|
|
||||||
// the user structure, whatever it is.
|
|
||||||
User m_user;
|
|
||||||
|
|
||||||
// current task we are executing here
|
// current task we are executing here
|
||||||
std::shared_ptr<YggdrasilTask> m_currentTask;
|
std::shared_ptr<AccountTask> m_currentTask;
|
||||||
|
|
||||||
protected: /* methods */
|
protected: /* methods */
|
||||||
|
|
||||||
@ -171,10 +171,4 @@ slots:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
void fillSession(AuthSessionPtr session);
|
void fillSession(AuthSessionPtr session);
|
||||||
|
|
||||||
public:
|
|
||||||
friend class YggdrasilTask;
|
|
||||||
friend class AuthenticateTask;
|
|
||||||
friend class ValidateTask;
|
|
||||||
friend class RefreshTask;
|
|
||||||
};
|
};
|
@ -1,315 +0,0 @@
|
|||||||
/* Copyright 2013-2021 MultiMC Contributors
|
|
||||||
*
|
|
||||||
* Authors: Orochimarufan <orochimarufan.x3@gmail.com>
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "MojangAccount.h"
|
|
||||||
#include "flows/RefreshTask.h"
|
|
||||||
#include "flows/AuthenticateTask.h"
|
|
||||||
|
|
||||||
#include <QUuid>
|
|
||||||
#include <QJsonObject>
|
|
||||||
#include <QJsonArray>
|
|
||||||
#include <QRegExp>
|
|
||||||
#include <QStringList>
|
|
||||||
#include <QJsonDocument>
|
|
||||||
|
|
||||||
#include <QDebug>
|
|
||||||
|
|
||||||
MojangAccountPtr MojangAccount::loadFromJson(const QJsonObject &object)
|
|
||||||
{
|
|
||||||
// The JSON object must at least have a username for it to be valid.
|
|
||||||
if (!object.value("username").isString())
|
|
||||||
{
|
|
||||||
qCritical() << "Can't load Mojang account info from JSON object. Username field is "
|
|
||||||
"missing or of the wrong type.";
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString username = object.value("username").toString("");
|
|
||||||
QString clientToken = object.value("clientToken").toString("");
|
|
||||||
QString accessToken = object.value("accessToken").toString("");
|
|
||||||
|
|
||||||
QJsonArray profileArray = object.value("profiles").toArray();
|
|
||||||
if (profileArray.size() < 1)
|
|
||||||
{
|
|
||||||
qCritical() << "Can't load Mojang account with username \"" << username
|
|
||||||
<< "\". No profiles found.";
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
QList<AccountProfile> profiles;
|
|
||||||
for (QJsonValue profileVal : profileArray)
|
|
||||||
{
|
|
||||||
QJsonObject profileObject = profileVal.toObject();
|
|
||||||
QString id = profileObject.value("id").toString("");
|
|
||||||
QString name = profileObject.value("name").toString("");
|
|
||||||
bool legacy = profileObject.value("legacy").toBool(false);
|
|
||||||
if (id.isEmpty() || name.isEmpty())
|
|
||||||
{
|
|
||||||
qWarning() << "Unable to load a profile because it was missing an ID or a name.";
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
profiles.append({id, name, legacy});
|
|
||||||
}
|
|
||||||
|
|
||||||
MojangAccountPtr account(new MojangAccount());
|
|
||||||
if (object.value("user").isObject())
|
|
||||||
{
|
|
||||||
User u;
|
|
||||||
QJsonObject userStructure = object.value("user").toObject();
|
|
||||||
u.id = userStructure.value("id").toString();
|
|
||||||
/*
|
|
||||||
QJsonObject propMap = userStructure.value("properties").toObject();
|
|
||||||
for(auto key: propMap.keys())
|
|
||||||
{
|
|
||||||
auto values = propMap.operator[](key).toArray();
|
|
||||||
for(auto value: values)
|
|
||||||
u.properties.insert(key, value.toString());
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
account->m_user = u;
|
|
||||||
}
|
|
||||||
account->m_username = username;
|
|
||||||
account->m_clientToken = clientToken;
|
|
||||||
account->m_accessToken = accessToken;
|
|
||||||
account->m_profiles = profiles;
|
|
||||||
|
|
||||||
// Get the currently selected profile.
|
|
||||||
QString currentProfile = object.value("activeProfile").toString("");
|
|
||||||
if (!currentProfile.isEmpty())
|
|
||||||
account->setCurrentProfile(currentProfile);
|
|
||||||
|
|
||||||
return account;
|
|
||||||
}
|
|
||||||
|
|
||||||
MojangAccountPtr MojangAccount::createFromUsername(const QString &username)
|
|
||||||
{
|
|
||||||
MojangAccountPtr account(new MojangAccount());
|
|
||||||
account->m_clientToken = QUuid::createUuid().toString().remove(QRegExp("[{}-]"));
|
|
||||||
account->m_username = username;
|
|
||||||
return account;
|
|
||||||
}
|
|
||||||
|
|
||||||
QJsonObject MojangAccount::saveToJson() const
|
|
||||||
{
|
|
||||||
QJsonObject json;
|
|
||||||
json.insert("username", m_username);
|
|
||||||
json.insert("clientToken", m_clientToken);
|
|
||||||
json.insert("accessToken", m_accessToken);
|
|
||||||
|
|
||||||
QJsonArray profileArray;
|
|
||||||
for (AccountProfile profile : m_profiles)
|
|
||||||
{
|
|
||||||
QJsonObject profileObj;
|
|
||||||
profileObj.insert("id", profile.id);
|
|
||||||
profileObj.insert("name", profile.name);
|
|
||||||
profileObj.insert("legacy", profile.legacy);
|
|
||||||
profileArray.append(profileObj);
|
|
||||||
}
|
|
||||||
json.insert("profiles", profileArray);
|
|
||||||
|
|
||||||
QJsonObject userStructure;
|
|
||||||
{
|
|
||||||
userStructure.insert("id", m_user.id);
|
|
||||||
/*
|
|
||||||
QJsonObject userAttrs;
|
|
||||||
for(auto key: m_user.properties.keys())
|
|
||||||
{
|
|
||||||
auto array = QJsonArray::fromStringList(m_user.properties.values(key));
|
|
||||||
userAttrs.insert(key, array);
|
|
||||||
}
|
|
||||||
userStructure.insert("properties", userAttrs);
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
json.insert("user", userStructure);
|
|
||||||
|
|
||||||
if (m_currentProfile != -1)
|
|
||||||
json.insert("activeProfile", currentProfile()->id);
|
|
||||||
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool MojangAccount::setCurrentProfile(const QString &profileId)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < m_profiles.length(); i++)
|
|
||||||
{
|
|
||||||
if (m_profiles[i].id == profileId)
|
|
||||||
{
|
|
||||||
m_currentProfile = i;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AccountProfile *MojangAccount::currentProfile() const
|
|
||||||
{
|
|
||||||
if (m_currentProfile == -1)
|
|
||||||
return nullptr;
|
|
||||||
return &m_profiles[m_currentProfile];
|
|
||||||
}
|
|
||||||
|
|
||||||
AccountStatus MojangAccount::accountStatus() const
|
|
||||||
{
|
|
||||||
if (m_accessToken.isEmpty())
|
|
||||||
return NotVerified;
|
|
||||||
else
|
|
||||||
return Verified;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::shared_ptr<YggdrasilTask> MojangAccount::login(AuthSessionPtr session, QString password)
|
|
||||||
{
|
|
||||||
Q_ASSERT(m_currentTask.get() == nullptr);
|
|
||||||
|
|
||||||
// take care of the true offline status
|
|
||||||
if (accountStatus() == NotVerified && password.isEmpty())
|
|
||||||
{
|
|
||||||
if (session)
|
|
||||||
{
|
|
||||||
session->status = AuthSession::RequiresPassword;
|
|
||||||
fillSession(session);
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(accountStatus() == Verified && !session->wants_online)
|
|
||||||
{
|
|
||||||
session->status = AuthSession::PlayableOffline;
|
|
||||||
session->auth_server_online = false;
|
|
||||||
fillSession(session);
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (password.isEmpty())
|
|
||||||
{
|
|
||||||
m_currentTask.reset(new RefreshTask(this));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
m_currentTask.reset(new AuthenticateTask(this, password));
|
|
||||||
}
|
|
||||||
m_currentTask->assignSession(session);
|
|
||||||
|
|
||||||
connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
|
|
||||||
connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
|
|
||||||
}
|
|
||||||
return m_currentTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
void MojangAccount::authSucceeded()
|
|
||||||
{
|
|
||||||
auto session = m_currentTask->getAssignedSession();
|
|
||||||
if (session)
|
|
||||||
{
|
|
||||||
session->status =
|
|
||||||
session->wants_online ? AuthSession::PlayableOnline : AuthSession::PlayableOffline;
|
|
||||||
fillSession(session);
|
|
||||||
session->auth_server_online = true;
|
|
||||||
}
|
|
||||||
m_currentTask.reset();
|
|
||||||
emit changed();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MojangAccount::authFailed(QString reason)
|
|
||||||
{
|
|
||||||
auto session = m_currentTask->getAssignedSession();
|
|
||||||
// This is emitted when the yggdrasil tasks time out or are cancelled.
|
|
||||||
// -> we treat the error as no-op
|
|
||||||
if (m_currentTask->state() == YggdrasilTask::STATE_FAILED_SOFT)
|
|
||||||
{
|
|
||||||
if (session)
|
|
||||||
{
|
|
||||||
session->status = accountStatus() == Verified ? AuthSession::PlayableOffline
|
|
||||||
: AuthSession::RequiresPassword;
|
|
||||||
session->auth_server_online = false;
|
|
||||||
fillSession(session);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
m_accessToken = QString();
|
|
||||||
emit changed();
|
|
||||||
if (session)
|
|
||||||
{
|
|
||||||
session->status = AuthSession::RequiresPassword;
|
|
||||||
session->auth_server_online = true;
|
|
||||||
fillSession(session);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m_currentTask.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MojangAccount::fillSession(AuthSessionPtr session)
|
|
||||||
{
|
|
||||||
// the user name. you have to have an user name
|
|
||||||
session->username = m_username;
|
|
||||||
// volatile auth token
|
|
||||||
session->access_token = m_accessToken;
|
|
||||||
// the semi-permanent client token
|
|
||||||
session->client_token = m_clientToken;
|
|
||||||
if (currentProfile())
|
|
||||||
{
|
|
||||||
// profile name
|
|
||||||
session->player_name = currentProfile()->name;
|
|
||||||
// profile ID
|
|
||||||
session->uuid = currentProfile()->id;
|
|
||||||
// 'legacy' or 'mojang', depending on account type
|
|
||||||
session->user_type = currentProfile()->legacy ? "legacy" : "mojang";
|
|
||||||
if (!session->access_token.isEmpty())
|
|
||||||
{
|
|
||||||
session->session = "token:" + m_accessToken + ":" + m_profiles[m_currentProfile].id;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
session->session = "-";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
session->player_name = "Player";
|
|
||||||
session->session = "-";
|
|
||||||
}
|
|
||||||
session->u = user();
|
|
||||||
session->m_accountPtr = shared_from_this();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MojangAccount::decrementUses()
|
|
||||||
{
|
|
||||||
Usable::decrementUses();
|
|
||||||
if(!isInUse())
|
|
||||||
{
|
|
||||||
emit changed();
|
|
||||||
qWarning() << "Account" << m_username << "is no longer in use.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MojangAccount::incrementUses()
|
|
||||||
{
|
|
||||||
bool wasInUse = isInUse();
|
|
||||||
Usable::incrementUses();
|
|
||||||
if(!wasInUse)
|
|
||||||
{
|
|
||||||
emit changed();
|
|
||||||
qWarning() << "Account" << m_username << "is now in use.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MojangAccount::invalidateClientToken()
|
|
||||||
{
|
|
||||||
m_clientToken = QUuid::createUuid().toString().remove(QRegExp("[{}-]"));
|
|
||||||
emit changed();
|
|
||||||
}
|
|
@ -1,199 +0,0 @@
|
|||||||
/* Copyright 2013-2021 MultiMC Contributors
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "MojangAccount.h"
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QVariant>
|
|
||||||
#include <QAbstractListModel>
|
|
||||||
#include <QSharedPointer>
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief List of available Mojang accounts.
|
|
||||||
* This should be loaded in the background by MultiMC on startup.
|
|
||||||
*
|
|
||||||
* This class also inherits from QAbstractListModel. Methods from that
|
|
||||||
* class determine how this list shows up in a list view. Said methods
|
|
||||||
* all have a default implementation, but they can be overridden by subclasses to
|
|
||||||
* change the behavior of the list.
|
|
||||||
*/
|
|
||||||
class MojangAccountList : public QAbstractListModel
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
enum ModelRoles
|
|
||||||
{
|
|
||||||
PointerRole = 0x34B1CB48
|
|
||||||
};
|
|
||||||
|
|
||||||
enum VListColumns
|
|
||||||
{
|
|
||||||
// TODO: Add icon column.
|
|
||||||
|
|
||||||
// First column - Active?
|
|
||||||
ActiveColumn = 0,
|
|
||||||
|
|
||||||
// Second column - Name
|
|
||||||
NameColumn,
|
|
||||||
};
|
|
||||||
|
|
||||||
explicit MojangAccountList(QObject *parent = 0);
|
|
||||||
|
|
||||||
//! Gets the account at the given index.
|
|
||||||
virtual const MojangAccountPtr at(int i) const;
|
|
||||||
|
|
||||||
//! Returns the number of accounts in the list.
|
|
||||||
virtual int count() const;
|
|
||||||
|
|
||||||
//////// List Model Functions ////////
|
|
||||||
virtual QVariant data(const QModelIndex &index, int role) const;
|
|
||||||
virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const;
|
|
||||||
virtual int rowCount(const QModelIndex &parent) const;
|
|
||||||
virtual int columnCount(const QModelIndex &parent) const;
|
|
||||||
virtual Qt::ItemFlags flags(const QModelIndex &index) const;
|
|
||||||
virtual bool setData(const QModelIndex &index, const QVariant &value, int role);
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* Adds a the given Mojang account to the account list.
|
|
||||||
*/
|
|
||||||
virtual void addAccount(const MojangAccountPtr account);
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* Removes the mojang account with the given username from the account list.
|
|
||||||
*/
|
|
||||||
virtual void removeAccount(const QString &username);
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* Removes the account at the given QModelIndex.
|
|
||||||
*/
|
|
||||||
virtual void removeAccount(QModelIndex index);
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Finds an account by its username.
|
|
||||||
* \param The username of the account to find.
|
|
||||||
* \return A const pointer to the account with the given username. NULL if
|
|
||||||
* one doesn't exist.
|
|
||||||
*/
|
|
||||||
virtual MojangAccountPtr findAccount(const QString &username) const;
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* Sets the default path to save the list file to.
|
|
||||||
* If autosave is true, this list will automatically save to the given path whenever it changes.
|
|
||||||
* THIS FUNCTION DOES NOT LOAD THE LIST. If you set autosave, be sure to call loadList() immediately
|
|
||||||
* after calling this function to ensure an autosaved change doesn't overwrite the list you intended
|
|
||||||
* to load.
|
|
||||||
*/
|
|
||||||
virtual void setListFilePath(QString path, bool autosave = false);
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Loads the account list from the given file path.
|
|
||||||
* If the given file is an empty string (default), will load from the default account list file.
|
|
||||||
* \return True if successful, otherwise false.
|
|
||||||
*/
|
|
||||||
virtual bool loadList(const QString &file = "");
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Saves the account list to the given file.
|
|
||||||
* If the given file is an empty string (default), will save from the default account list file.
|
|
||||||
* \return True if successful, otherwise false.
|
|
||||||
*/
|
|
||||||
virtual bool saveList(const QString &file = "");
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Gets a pointer to the account that the user has selected as their "active" account.
|
|
||||||
* Which account is active can be overridden on a per-instance basis, but this will return the one that
|
|
||||||
* is set as active globally.
|
|
||||||
* \return The currently active MojangAccount. If there isn't an active account, returns a null pointer.
|
|
||||||
*/
|
|
||||||
virtual MojangAccountPtr activeAccount() const;
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* Sets the given account as the current active account.
|
|
||||||
* If the username given is an empty string, sets the active account to nothing.
|
|
||||||
*/
|
|
||||||
virtual void setActiveAccount(const QString &username);
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* Returns true if any of the account is at least Validated
|
|
||||||
*/
|
|
||||||
bool anyAccountIsValid();
|
|
||||||
|
|
||||||
signals:
|
|
||||||
/*!
|
|
||||||
* Signal emitted to indicate that the account list has changed.
|
|
||||||
* This will also fire if the value of an element in the list changes (will be implemented
|
|
||||||
* later).
|
|
||||||
*/
|
|
||||||
void listChanged();
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* Signal emitted to indicate that the active account has changed.
|
|
||||||
*/
|
|
||||||
void activeAccountChanged();
|
|
||||||
|
|
||||||
public
|
|
||||||
slots:
|
|
||||||
/**
|
|
||||||
* This is called when one of the accounts changes and the list needs to be updated
|
|
||||||
*/
|
|
||||||
void accountChanged();
|
|
||||||
|
|
||||||
protected:
|
|
||||||
/*!
|
|
||||||
* Called whenever the list changes.
|
|
||||||
* This emits the listChanged() signal and autosaves the list (if autosave is enabled).
|
|
||||||
*/
|
|
||||||
void onListChanged();
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* Called whenever the active account changes.
|
|
||||||
* Emits the activeAccountChanged() signal and autosaves the list if enabled.
|
|
||||||
*/
|
|
||||||
void onActiveChanged();
|
|
||||||
|
|
||||||
QList<MojangAccountPtr> m_accounts;
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* Account that is currently active.
|
|
||||||
*/
|
|
||||||
MojangAccountPtr m_activeAccount;
|
|
||||||
|
|
||||||
//! Path to the account list file. Empty string if there isn't one.
|
|
||||||
QString m_listFilePath;
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* If true, the account list will automatically save to the account list path when it changes.
|
|
||||||
* Ignored if m_listFilePath is blank.
|
|
||||||
*/
|
|
||||||
bool m_autosave = false;
|
|
||||||
|
|
||||||
protected
|
|
||||||
slots:
|
|
||||||
/*!
|
|
||||||
* Updates this list with the given list of accounts.
|
|
||||||
* This is done by copying each account in the given list and inserting it
|
|
||||||
* into this one.
|
|
||||||
* We need to do this so that we can set the parents of the accounts are set to this
|
|
||||||
* account list. This can't be done in the load task, because the accounts the load
|
|
||||||
* task creates are on the load task's thread and Qt won't allow their parents
|
|
||||||
* to be set to something created on another thread.
|
|
||||||
* To get around that problem, we invoke this method on the GUI thread, which
|
|
||||||
* then copies the accounts and sets their parents correctly.
|
|
||||||
* \param accounts List of accounts whose parents should be set.
|
|
||||||
*/
|
|
||||||
virtual void updateListData(QList<MojangAccountPtr> versions);
|
|
||||||
};
|
|
@ -14,9 +14,8 @@
|
|||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
|
|
||||||
#include "context.h"
|
#include "AuthContext.h"
|
||||||
#include "katabasis/Globals.h"
|
#include "katabasis/Globals.h"
|
||||||
#include "katabasis/StoreQSettings.h"
|
|
||||||
#include "katabasis/Requestor.h"
|
#include "katabasis/Requestor.h"
|
||||||
#include "BuildConfig.h"
|
#include "BuildConfig.h"
|
||||||
|
|
||||||
@ -24,117 +23,107 @@ using OAuth2 = Katabasis::OAuth2;
|
|||||||
using Requestor = Katabasis::Requestor;
|
using Requestor = Katabasis::Requestor;
|
||||||
using Activity = Katabasis::Activity;
|
using Activity = Katabasis::Activity;
|
||||||
|
|
||||||
Context::Context(QObject *parent) :
|
AuthContext::AuthContext(AccountData * data, QObject *parent) :
|
||||||
QObject(parent)
|
AccountTask(data, parent)
|
||||||
{
|
{
|
||||||
mgr = new QNetworkAccessManager(this);
|
mgr = new QNetworkAccessManager(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AuthContext::beginActivity(Activity activity) {
|
||||||
|
if(isBusy()) {
|
||||||
|
throw 0;
|
||||||
|
}
|
||||||
|
m_activity = activity;
|
||||||
|
changeState(STATE_WORKING, "Initializing");
|
||||||
|
emit activityChanged(m_activity);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AuthContext::finishActivity() {
|
||||||
|
if(!isBusy()) {
|
||||||
|
throw 0;
|
||||||
|
}
|
||||||
|
m_activity = Katabasis::Activity::Idle;
|
||||||
|
m_stage = MSAStage::Idle;
|
||||||
|
m_data->validity_ = m_data->minecraftProfile.validity;
|
||||||
|
emit activityChanged(m_activity);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AuthContext::initMSA() {
|
||||||
|
if(m_oauth2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
Katabasis::OAuth2::Options opts;
|
Katabasis::OAuth2::Options opts;
|
||||||
opts.scope = "XboxLive.signin offline_access";
|
opts.scope = "XboxLive.signin offline_access";
|
||||||
opts.clientIdentifier = BuildConfig.CLIENT_ID;
|
opts.clientIdentifier = BuildConfig.MSA_CLIENT_ID;
|
||||||
opts.authorizationUrl = "https://login.live.com/oauth20_authorize.srf";
|
opts.authorizationUrl = "https://login.live.com/oauth20_authorize.srf";
|
||||||
opts.accessTokenUrl = "https://login.live.com/oauth20_token.srf";
|
opts.accessTokenUrl = "https://login.live.com/oauth20_token.srf";
|
||||||
opts.listenerPorts = {28562, 28563, 28564, 28565, 28566};
|
opts.listenerPorts = {28562, 28563, 28564, 28565, 28566};
|
||||||
|
|
||||||
oauth2 = new OAuth2(opts, m_account.msaToken, this, mgr);
|
m_oauth2 = new OAuth2(opts, m_data->msaToken, this, mgr);
|
||||||
|
|
||||||
connect(oauth2, &OAuth2::linkingFailed, this, &Context::onLinkingFailed);
|
connect(m_oauth2, &OAuth2::linkingFailed, this, &AuthContext::onOAuthLinkingFailed);
|
||||||
connect(oauth2, &OAuth2::linkingSucceeded, this, &Context::onLinkingSucceeded);
|
connect(m_oauth2, &OAuth2::linkingSucceeded, this, &AuthContext::onOAuthLinkingSucceeded);
|
||||||
connect(oauth2, &OAuth2::openBrowser, this, &Context::onOpenBrowser);
|
connect(m_oauth2, &OAuth2::openBrowser, this, &AuthContext::onOpenBrowser);
|
||||||
connect(oauth2, &OAuth2::closeBrowser, this, &Context::onCloseBrowser);
|
connect(m_oauth2, &OAuth2::closeBrowser, this, &AuthContext::onCloseBrowser);
|
||||||
connect(oauth2, &OAuth2::activityChanged, this, &Context::onOAuthActivityChanged);
|
connect(m_oauth2, &OAuth2::activityChanged, this, &AuthContext::onOAuthActivityChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Context::beginActivity(Activity activity) {
|
void AuthContext::initMojang() {
|
||||||
if(isBusy()) {
|
if(m_yggdrasil) {
|
||||||
throw 0;
|
return;
|
||||||
}
|
}
|
||||||
activity_ = activity;
|
m_yggdrasil = new Yggdrasil(m_data, this);
|
||||||
emit activityChanged(activity_);
|
|
||||||
|
connect(m_yggdrasil, &Task::failed, this, &AuthContext::onMojangFailed);
|
||||||
|
connect(m_yggdrasil, &Task::succeeded, this, &AuthContext::onMojangSucceeded);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Context::finishActivity() {
|
void AuthContext::onMojangSucceeded() {
|
||||||
if(!isBusy()) {
|
doMinecraftProfile();
|
||||||
throw 0;
|
|
||||||
}
|
|
||||||
activity_ = Katabasis::Activity::Idle;
|
|
||||||
m_account.validity_ = m_account.minecraftProfile.validity;
|
|
||||||
emit activityChanged(activity_);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QString Context::gameToken() {
|
|
||||||
return m_account.minecraftToken.token;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString Context::userId() {
|
void AuthContext::onMojangFailed() {
|
||||||
return m_account.minecraftProfile.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString Context::userName() {
|
|
||||||
return m_account.minecraftProfile.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Context::silentSignIn() {
|
|
||||||
if(isBusy()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
beginActivity(Activity::Refreshing);
|
|
||||||
if(!oauth2->refresh()) {
|
|
||||||
finishActivity();
|
finishActivity();
|
||||||
return false;
|
m_error = m_yggdrasil->m_error;
|
||||||
|
m_aborted = m_yggdrasil->m_aborted;
|
||||||
|
changeState(m_yggdrasil->accountState(), "Microsoft user authentication failed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
requestsDone = 0;
|
/*
|
||||||
xboxProfileSucceeded = false;
|
bool AuthContext::signOut() {
|
||||||
mcAuthSucceeded = false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Context::signIn() {
|
|
||||||
if(isBusy()) {
|
if(isBusy()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
requestsDone = 0;
|
start();
|
||||||
xboxProfileSucceeded = false;
|
|
||||||
mcAuthSucceeded = false;
|
|
||||||
|
|
||||||
beginActivity(Activity::LoggingIn);
|
|
||||||
oauth2->unlink();
|
|
||||||
m_account = AccountData();
|
|
||||||
oauth2->link();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Context::signOut() {
|
|
||||||
if(isBusy()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
beginActivity(Activity::LoggingOut);
|
beginActivity(Activity::LoggingOut);
|
||||||
oauth2->unlink();
|
m_oauth2->unlink();
|
||||||
m_account = AccountData();
|
m_account = AccountData();
|
||||||
finishActivity();
|
finishActivity();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
void AuthContext::onOpenBrowser(const QUrl &url) {
|
||||||
void Context::onOpenBrowser(const QUrl &url) {
|
|
||||||
QDesktopServices::openUrl(url);
|
QDesktopServices::openUrl(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Context::onCloseBrowser() {
|
void AuthContext::onCloseBrowser() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Context::onLinkingFailed() {
|
void AuthContext::onOAuthLinkingFailed() {
|
||||||
finishActivity();
|
finishActivity();
|
||||||
|
changeState(STATE_FAILED_HARD, "Microsoft user authentication failed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
void Context::onLinkingSucceeded() {
|
void AuthContext::onOAuthLinkingSucceeded() {
|
||||||
auto *o2t = qobject_cast<OAuth2 *>(sender());
|
auto *o2t = qobject_cast<OAuth2 *>(sender());
|
||||||
if (!o2t->linked()) {
|
if (!o2t->linked()) {
|
||||||
finishActivity();
|
finishActivity();
|
||||||
|
changeState(STATE_FAILED_HARD, "Microsoft user authentication ended with an impossible state (succeeded, but not succeeded at the same time).");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
QVariantMap extraTokens = o2t->extraTokens();
|
QVariantMap extraTokens = o2t->extraTokens();
|
||||||
@ -147,11 +136,14 @@ void Context::onLinkingSucceeded() {
|
|||||||
doUserAuth();
|
doUserAuth();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Context::onOAuthActivityChanged(Katabasis::Activity activity) {
|
void AuthContext::onOAuthActivityChanged(Katabasis::Activity activity) {
|
||||||
// respond to activity change here
|
// respond to activity change here
|
||||||
}
|
}
|
||||||
|
|
||||||
void Context::doUserAuth() {
|
void AuthContext::doUserAuth() {
|
||||||
|
m_stage = MSAStage::UserAuth;
|
||||||
|
changeState(STATE_WORKING, "Starting user authentication");
|
||||||
|
|
||||||
QString xbox_auth_template = R"XXX(
|
QString xbox_auth_template = R"XXX(
|
||||||
{
|
{
|
||||||
"Properties": {
|
"Properties": {
|
||||||
@ -163,15 +155,15 @@ void Context::doUserAuth() {
|
|||||||
"TokenType": "JWT"
|
"TokenType": "JWT"
|
||||||
}
|
}
|
||||||
)XXX";
|
)XXX";
|
||||||
auto xbox_auth_data = xbox_auth_template.arg(m_account.msaToken.token);
|
auto xbox_auth_data = xbox_auth_template.arg(m_data->msaToken.token);
|
||||||
|
|
||||||
QNetworkRequest request = QNetworkRequest(QUrl("https://user.auth.xboxlive.com/user/authenticate"));
|
QNetworkRequest request = QNetworkRequest(QUrl("https://user.auth.xboxlive.com/user/authenticate"));
|
||||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
request.setRawHeader("Accept", "application/json");
|
request.setRawHeader("Accept", "application/json");
|
||||||
auto *requestor = new Katabasis::Requestor(mgr, oauth2, this);
|
auto *requestor = new Katabasis::Requestor(mgr, m_oauth2, this);
|
||||||
requestor->setAddAccessTokenInQuery(false);
|
requestor->setAddAccessTokenInQuery(false);
|
||||||
|
|
||||||
connect(requestor, &Requestor::finished, this, &Context::onUserAuthDone);
|
connect(requestor, &Requestor::finished, this, &AuthContext::onUserAuthDone);
|
||||||
requestor->post(request, xbox_auth_data.toUtf8());
|
requestor->post(request, xbox_auth_data.toUtf8());
|
||||||
qDebug() << "First layer of XBox auth ... commencing.";
|
qDebug() << "First layer of XBox auth ... commencing.";
|
||||||
}
|
}
|
||||||
@ -181,7 +173,7 @@ bool getDateTime(QJsonValue value, QDateTime & out) {
|
|||||||
if(!value.isString()) {
|
if(!value.isString()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
out = QDateTime::fromString(value.toString(), Qt::ISODateWithMs);
|
out = QDateTime::fromString(value.toString(), Qt::ISODate);
|
||||||
return out.isValid();
|
return out.isValid();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -294,7 +286,7 @@ bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Context::onUserAuthDone(
|
void AuthContext::onUserAuthDone(
|
||||||
int requestId,
|
int requestId,
|
||||||
QNetworkReply::NetworkError error,
|
QNetworkReply::NetworkError error,
|
||||||
QByteArray replyData,
|
QByteArray replyData,
|
||||||
@ -303,6 +295,7 @@ void Context::onUserAuthDone(
|
|||||||
if (error != QNetworkReply::NoError) {
|
if (error != QNetworkReply::NoError) {
|
||||||
qWarning() << "Reply error:" << error;
|
qWarning() << "Reply error:" << error;
|
||||||
finishActivity();
|
finishActivity();
|
||||||
|
changeState(STATE_FAILED_HARD, "XBox user authentication failed.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -310,9 +303,13 @@ void Context::onUserAuthDone(
|
|||||||
if(!parseXTokenResponse(replyData, temp)) {
|
if(!parseXTokenResponse(replyData, temp)) {
|
||||||
qWarning() << "Could not parse user authentication response...";
|
qWarning() << "Could not parse user authentication response...";
|
||||||
finishActivity();
|
finishActivity();
|
||||||
|
changeState(STATE_FAILED_HARD, "XBox user authentication response could not be understood.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
m_account.userToken = temp;
|
m_data->userToken = temp;
|
||||||
|
|
||||||
|
m_stage = MSAStage::XboxAuth;
|
||||||
|
changeState(STATE_WORKING, "Starting XBox authentication");
|
||||||
|
|
||||||
doSTSAuthMinecraft();
|
doSTSAuthMinecraft();
|
||||||
doSTSAuthGeneric();
|
doSTSAuthGeneric();
|
||||||
@ -329,7 +326,7 @@ void Context::onUserAuthDone(
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
void Context::doSTSAuthMinecraft() {
|
void AuthContext::doSTSAuthMinecraft() {
|
||||||
QString xbox_auth_template = R"XXX(
|
QString xbox_auth_template = R"XXX(
|
||||||
{
|
{
|
||||||
"Properties": {
|
"Properties": {
|
||||||
@ -342,20 +339,20 @@ void Context::doSTSAuthMinecraft() {
|
|||||||
"TokenType": "JWT"
|
"TokenType": "JWT"
|
||||||
}
|
}
|
||||||
)XXX";
|
)XXX";
|
||||||
auto xbox_auth_data = xbox_auth_template.arg(m_account.userToken.token);
|
auto xbox_auth_data = xbox_auth_template.arg(m_data->userToken.token);
|
||||||
|
|
||||||
QNetworkRequest request = QNetworkRequest(QUrl("https://xsts.auth.xboxlive.com/xsts/authorize"));
|
QNetworkRequest request = QNetworkRequest(QUrl("https://xsts.auth.xboxlive.com/xsts/authorize"));
|
||||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
request.setRawHeader("Accept", "application/json");
|
request.setRawHeader("Accept", "application/json");
|
||||||
Requestor *requestor = new Requestor(mgr, oauth2, this);
|
Requestor *requestor = new Requestor(mgr, m_oauth2, this);
|
||||||
requestor->setAddAccessTokenInQuery(false);
|
requestor->setAddAccessTokenInQuery(false);
|
||||||
|
|
||||||
connect(requestor, &Requestor::finished, this, &Context::onSTSAuthMinecraftDone);
|
connect(requestor, &Requestor::finished, this, &AuthContext::onSTSAuthMinecraftDone);
|
||||||
requestor->post(request, xbox_auth_data.toUtf8());
|
requestor->post(request, xbox_auth_data.toUtf8());
|
||||||
qDebug() << "Second layer of XBox auth ... commencing.";
|
qDebug() << "Second layer of XBox auth ... commencing.";
|
||||||
}
|
}
|
||||||
|
|
||||||
void Context::onSTSAuthMinecraftDone(
|
void AuthContext::onSTSAuthMinecraftDone(
|
||||||
int requestId,
|
int requestId,
|
||||||
QNetworkReply::NetworkError error,
|
QNetworkReply::NetworkError error,
|
||||||
QByteArray replyData,
|
QByteArray replyData,
|
||||||
@ -363,29 +360,29 @@ void Context::onSTSAuthMinecraftDone(
|
|||||||
) {
|
) {
|
||||||
if (error != QNetworkReply::NoError) {
|
if (error != QNetworkReply::NoError) {
|
||||||
qWarning() << "Reply error:" << error;
|
qWarning() << "Reply error:" << error;
|
||||||
finishActivity();
|
m_requestsDone ++;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Katabasis::Token temp;
|
Katabasis::Token temp;
|
||||||
if(!parseXTokenResponse(replyData, temp)) {
|
if(!parseXTokenResponse(replyData, temp)) {
|
||||||
qWarning() << "Could not parse authorization response for access to mojang services...";
|
qWarning() << "Could not parse authorization response for access to mojang services...";
|
||||||
finishActivity();
|
m_requestsDone ++;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(temp.extra["uhs"] != m_account.userToken.extra["uhs"]) {
|
if(temp.extra["uhs"] != m_data->userToken.extra["uhs"]) {
|
||||||
qWarning() << "Server has changed user hash in the reply... something is wrong. ABORTING";
|
qWarning() << "Server has changed user hash in the reply... something is wrong. ABORTING";
|
||||||
qDebug() << replyData;
|
qDebug() << replyData;
|
||||||
finishActivity();
|
m_requestsDone ++;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
m_account.mojangservicesToken = temp;
|
m_data->mojangservicesToken = temp;
|
||||||
|
|
||||||
doMinecraftAuth();
|
doMinecraftAuth();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Context::doSTSAuthGeneric() {
|
void AuthContext::doSTSAuthGeneric() {
|
||||||
QString xbox_auth_template = R"XXX(
|
QString xbox_auth_template = R"XXX(
|
||||||
{
|
{
|
||||||
"Properties": {
|
"Properties": {
|
||||||
@ -398,20 +395,20 @@ void Context::doSTSAuthGeneric() {
|
|||||||
"TokenType": "JWT"
|
"TokenType": "JWT"
|
||||||
}
|
}
|
||||||
)XXX";
|
)XXX";
|
||||||
auto xbox_auth_data = xbox_auth_template.arg(m_account.userToken.token);
|
auto xbox_auth_data = xbox_auth_template.arg(m_data->userToken.token);
|
||||||
|
|
||||||
QNetworkRequest request = QNetworkRequest(QUrl("https://xsts.auth.xboxlive.com/xsts/authorize"));
|
QNetworkRequest request = QNetworkRequest(QUrl("https://xsts.auth.xboxlive.com/xsts/authorize"));
|
||||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
request.setRawHeader("Accept", "application/json");
|
request.setRawHeader("Accept", "application/json");
|
||||||
Requestor *requestor = new Requestor(mgr, oauth2, this);
|
Requestor *requestor = new Requestor(mgr, m_oauth2, this);
|
||||||
requestor->setAddAccessTokenInQuery(false);
|
requestor->setAddAccessTokenInQuery(false);
|
||||||
|
|
||||||
connect(requestor, &Requestor::finished, this, &Context::onSTSAuthGenericDone);
|
connect(requestor, &Requestor::finished, this, &AuthContext::onSTSAuthGenericDone);
|
||||||
requestor->post(request, xbox_auth_data.toUtf8());
|
requestor->post(request, xbox_auth_data.toUtf8());
|
||||||
qDebug() << "Second layer of XBox auth ... commencing.";
|
qDebug() << "Second layer of XBox auth ... commencing.";
|
||||||
}
|
}
|
||||||
|
|
||||||
void Context::onSTSAuthGenericDone(
|
void AuthContext::onSTSAuthGenericDone(
|
||||||
int requestId,
|
int requestId,
|
||||||
QNetworkReply::NetworkError error,
|
QNetworkReply::NetworkError error,
|
||||||
QByteArray replyData,
|
QByteArray replyData,
|
||||||
@ -419,44 +416,44 @@ void Context::onSTSAuthGenericDone(
|
|||||||
) {
|
) {
|
||||||
if (error != QNetworkReply::NoError) {
|
if (error != QNetworkReply::NoError) {
|
||||||
qWarning() << "Reply error:" << error;
|
qWarning() << "Reply error:" << error;
|
||||||
finishActivity();
|
m_requestsDone ++;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Katabasis::Token temp;
|
Katabasis::Token temp;
|
||||||
if(!parseXTokenResponse(replyData, temp)) {
|
if(!parseXTokenResponse(replyData, temp)) {
|
||||||
qWarning() << "Could not parse authorization response for access to xbox API...";
|
qWarning() << "Could not parse authorization response for access to xbox API...";
|
||||||
finishActivity();
|
m_requestsDone ++;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(temp.extra["uhs"] != m_account.userToken.extra["uhs"]) {
|
if(temp.extra["uhs"] != m_data->userToken.extra["uhs"]) {
|
||||||
qWarning() << "Server has changed user hash in the reply... something is wrong. ABORTING";
|
qWarning() << "Server has changed user hash in the reply... something is wrong. ABORTING";
|
||||||
qDebug() << replyData;
|
qDebug() << replyData;
|
||||||
finishActivity();
|
m_requestsDone ++;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
m_account.xboxApiToken = temp;
|
m_data->xboxApiToken = temp;
|
||||||
|
|
||||||
doXBoxProfile();
|
doXBoxProfile();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void Context::doMinecraftAuth() {
|
void AuthContext::doMinecraftAuth() {
|
||||||
QString mc_auth_template = R"XXX(
|
QString mc_auth_template = R"XXX(
|
||||||
{
|
{
|
||||||
"identityToken": "XBL3.0 x=%1;%2"
|
"identityToken": "XBL3.0 x=%1;%2"
|
||||||
}
|
}
|
||||||
)XXX";
|
)XXX";
|
||||||
auto data = mc_auth_template.arg(m_account.mojangservicesToken.extra["uhs"].toString(), m_account.mojangservicesToken.token);
|
auto data = mc_auth_template.arg(m_data->mojangservicesToken.extra["uhs"].toString(), m_data->mojangservicesToken.token);
|
||||||
|
|
||||||
QNetworkRequest request = QNetworkRequest(QUrl("https://api.minecraftservices.com/authentication/login_with_xbox"));
|
QNetworkRequest request = QNetworkRequest(QUrl("https://api.minecraftservices.com/authentication/login_with_xbox"));
|
||||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
request.setRawHeader("Accept", "application/json");
|
request.setRawHeader("Accept", "application/json");
|
||||||
Requestor *requestor = new Requestor(mgr, oauth2, this);
|
Requestor *requestor = new Requestor(mgr, m_oauth2, this);
|
||||||
requestor->setAddAccessTokenInQuery(false);
|
requestor->setAddAccessTokenInQuery(false);
|
||||||
|
|
||||||
connect(requestor, &Requestor::finished, this, &Context::onMinecraftAuthDone);
|
connect(requestor, &Requestor::finished, this, &AuthContext::onMinecraftAuthDone);
|
||||||
requestor->post(request, data.toUtf8());
|
requestor->post(request, data.toUtf8());
|
||||||
qDebug() << "Getting Minecraft access token...";
|
qDebug() << "Getting Minecraft access token...";
|
||||||
}
|
}
|
||||||
@ -501,33 +498,31 @@ bool parseMojangResponse(QByteArray & data, Katabasis::Token &output) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Context::onMinecraftAuthDone(
|
void AuthContext::onMinecraftAuthDone(
|
||||||
int requestId,
|
int requestId,
|
||||||
QNetworkReply::NetworkError error,
|
QNetworkReply::NetworkError error,
|
||||||
QByteArray replyData,
|
QByteArray replyData,
|
||||||
QList<QNetworkReply::RawHeaderPair> headers
|
QList<QNetworkReply::RawHeaderPair> headers
|
||||||
) {
|
) {
|
||||||
requestsDone++;
|
m_requestsDone ++;
|
||||||
|
|
||||||
if (error != QNetworkReply::NoError) {
|
if (error != QNetworkReply::NoError) {
|
||||||
qWarning() << "Reply error:" << error;
|
qWarning() << "Reply error:" << error;
|
||||||
qDebug() << replyData;
|
qDebug() << replyData;
|
||||||
finishActivity();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!parseMojangResponse(replyData, m_account.minecraftToken)) {
|
if(!parseMojangResponse(replyData, m_data->yggdrasilToken)) {
|
||||||
qWarning() << "Could not parse login_with_xbox response...";
|
qWarning() << "Could not parse login_with_xbox response...";
|
||||||
qDebug() << replyData;
|
qDebug() << replyData;
|
||||||
finishActivity();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mcAuthSucceeded = true;
|
m_mcAuthSucceeded = true;
|
||||||
|
|
||||||
checkResult();
|
checkResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Context::doXBoxProfile() {
|
void AuthContext::doXBoxProfile() {
|
||||||
auto url = QUrl("https://profile.xboxlive.com/users/me/profile/settings");
|
auto url = QUrl("https://profile.xboxlive.com/users/me/profile/settings");
|
||||||
QUrlQuery q;
|
QUrlQuery q;
|
||||||
q.addQueryItem(
|
q.addQueryItem(
|
||||||
@ -544,45 +539,45 @@ void Context::doXBoxProfile() {
|
|||||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
request.setRawHeader("Accept", "application/json");
|
request.setRawHeader("Accept", "application/json");
|
||||||
request.setRawHeader("x-xbl-contract-version", "3");
|
request.setRawHeader("x-xbl-contract-version", "3");
|
||||||
request.setRawHeader("Authorization", QString("XBL3.0 x=%1;%2").arg(m_account.userToken.extra["uhs"].toString(), m_account.xboxApiToken.token).toUtf8());
|
request.setRawHeader("Authorization", QString("XBL3.0 x=%1;%2").arg(m_data->userToken.extra["uhs"].toString(), m_data->xboxApiToken.token).toUtf8());
|
||||||
Requestor *requestor = new Requestor(mgr, oauth2, this);
|
Requestor *requestor = new Requestor(mgr, m_oauth2, this);
|
||||||
requestor->setAddAccessTokenInQuery(false);
|
requestor->setAddAccessTokenInQuery(false);
|
||||||
|
|
||||||
connect(requestor, &Requestor::finished, this, &Context::onXBoxProfileDone);
|
connect(requestor, &Requestor::finished, this, &AuthContext::onXBoxProfileDone);
|
||||||
requestor->get(request);
|
requestor->get(request);
|
||||||
qDebug() << "Getting Xbox profile...";
|
qDebug() << "Getting Xbox profile...";
|
||||||
}
|
}
|
||||||
|
|
||||||
void Context::onXBoxProfileDone(
|
void AuthContext::onXBoxProfileDone(
|
||||||
int requestId,
|
int requestId,
|
||||||
QNetworkReply::NetworkError error,
|
QNetworkReply::NetworkError error,
|
||||||
QByteArray replyData,
|
QByteArray replyData,
|
||||||
QList<QNetworkReply::RawHeaderPair> headers
|
QList<QNetworkReply::RawHeaderPair> headers
|
||||||
) {
|
) {
|
||||||
requestsDone ++;
|
m_requestsDone ++;
|
||||||
|
|
||||||
if (error != QNetworkReply::NoError) {
|
if (error != QNetworkReply::NoError) {
|
||||||
qWarning() << "Reply error:" << error;
|
qWarning() << "Reply error:" << error;
|
||||||
qDebug() << replyData;
|
qDebug() << replyData;
|
||||||
finishActivity();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
qDebug() << "XBox profile: " << replyData;
|
qDebug() << "XBox profile: " << replyData;
|
||||||
|
|
||||||
xboxProfileSucceeded = true;
|
m_xboxProfileSucceeded = true;
|
||||||
checkResult();
|
checkResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Context::checkResult() {
|
void AuthContext::checkResult() {
|
||||||
if(requestsDone != 2) {
|
if(m_requestsDone != 2) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(mcAuthSucceeded && xboxProfileSucceeded) {
|
if(m_mcAuthSucceeded && m_xboxProfileSucceeded) {
|
||||||
doMinecraftProfile();
|
doMinecraftProfile();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
finishActivity();
|
finishActivity();
|
||||||
|
changeState(STATE_FAILED_HARD, "XBox and/or Mojang authentication steps did not succeed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -666,273 +661,92 @@ bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Context::doMinecraftProfile() {
|
void AuthContext::doMinecraftProfile() {
|
||||||
|
m_stage = MSAStage::MinecraftProfile;
|
||||||
|
changeState(STATE_WORKING, "Starting minecraft profile acquisition");
|
||||||
|
|
||||||
auto url = QUrl("https://api.minecraftservices.com/minecraft/profile");
|
auto url = QUrl("https://api.minecraftservices.com/minecraft/profile");
|
||||||
QNetworkRequest request = QNetworkRequest(url);
|
QNetworkRequest request = QNetworkRequest(url);
|
||||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
// request.setRawHeader("Accept", "application/json");
|
// request.setRawHeader("Accept", "application/json");
|
||||||
request.setRawHeader("Authorization", QString("Bearer %1").arg(m_account.minecraftToken.token).toUtf8());
|
request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8());
|
||||||
|
|
||||||
Requestor *requestor = new Requestor(mgr, oauth2, this);
|
Requestor *requestor = new Requestor(mgr, m_oauth2, this);
|
||||||
requestor->setAddAccessTokenInQuery(false);
|
requestor->setAddAccessTokenInQuery(false);
|
||||||
|
|
||||||
connect(requestor, &Requestor::finished, this, &Context::onMinecraftProfileDone);
|
connect(requestor, &Requestor::finished, this, &AuthContext::onMinecraftProfileDone);
|
||||||
requestor->get(request);
|
requestor->get(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Context::onMinecraftProfileDone(int, QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers) {
|
void AuthContext::onMinecraftProfileDone(int, QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers) {
|
||||||
qDebug() << data;
|
qDebug() << data;
|
||||||
if (error == QNetworkReply::ContentNotFoundError) {
|
if (error == QNetworkReply::ContentNotFoundError) {
|
||||||
m_account.minecraftProfile = MinecraftProfile();
|
m_data->minecraftProfile = MinecraftProfile();
|
||||||
finishActivity();
|
finishActivity();
|
||||||
|
changeState(STATE_FAILED_HARD, "Account is missing a profile");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (error != QNetworkReply::NoError) {
|
if (error != QNetworkReply::NoError) {
|
||||||
finishActivity();
|
finishActivity();
|
||||||
|
changeState(STATE_FAILED_HARD, "Profile acquisition failed");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(!parseMinecraftProfile(data, m_account.minecraftProfile)) {
|
if(!parseMinecraftProfile(data, m_data->minecraftProfile)) {
|
||||||
m_account.minecraftProfile = MinecraftProfile();
|
m_data->minecraftProfile = MinecraftProfile();
|
||||||
finishActivity();
|
finishActivity();
|
||||||
|
changeState(STATE_FAILED_HARD, "Profile response could not be parsed");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
doGetSkin();
|
doGetSkin();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Context::doGetSkin() {
|
void AuthContext::doGetSkin() {
|
||||||
auto url = QUrl(m_account.minecraftProfile.skin.url);
|
m_stage = MSAStage::Skin;
|
||||||
|
changeState(STATE_WORKING, "Starting skin acquisition");
|
||||||
|
|
||||||
|
auto url = QUrl(m_data->minecraftProfile.skin.url);
|
||||||
QNetworkRequest request = QNetworkRequest(url);
|
QNetworkRequest request = QNetworkRequest(url);
|
||||||
Requestor *requestor = new Requestor(mgr, oauth2, this);
|
Requestor *requestor = new Requestor(mgr, m_oauth2, this);
|
||||||
requestor->setAddAccessTokenInQuery(false);
|
requestor->setAddAccessTokenInQuery(false);
|
||||||
connect(requestor, &Requestor::finished, this, &Context::onSkinDone);
|
connect(requestor, &Requestor::finished, this, &AuthContext::onSkinDone);
|
||||||
requestor->get(request);
|
requestor->get(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Context::onSkinDone(int, QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair>) {
|
void AuthContext::onSkinDone(int, QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair>) {
|
||||||
if (error == QNetworkReply::NoError) {
|
if (error == QNetworkReply::NoError) {
|
||||||
m_account.minecraftProfile.skin.data = data;
|
m_data->minecraftProfile.skin.data = data;
|
||||||
}
|
}
|
||||||
|
m_data->validity_ = Katabasis::Validity::Certain;
|
||||||
finishActivity();
|
finishActivity();
|
||||||
|
changeState(STATE_SUCCEEDED, "Finished whole chain");
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace {
|
QString AuthContext::getStateMessage() const {
|
||||||
void tokenToJSON(QJsonObject &parent, Katabasis::Token t, const char * tokenName) {
|
switch (m_accountState)
|
||||||
if(t.validity == Katabasis::Validity::None || !t.persistent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
QJsonObject out;
|
|
||||||
if(t.issueInstant.isValid()) {
|
|
||||||
out["iat"] = QJsonValue(t.issueInstant.toSecsSinceEpoch());
|
|
||||||
}
|
|
||||||
|
|
||||||
if(t.notAfter.isValid()) {
|
|
||||||
out["exp"] = QJsonValue(t.notAfter.toSecsSinceEpoch());
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!t.token.isEmpty()) {
|
|
||||||
out["token"] = QJsonValue(t.token);
|
|
||||||
}
|
|
||||||
if(!t.refresh_token.isEmpty()) {
|
|
||||||
out["refresh_token"] = QJsonValue(t.refresh_token);
|
|
||||||
}
|
|
||||||
if(t.extra.size()) {
|
|
||||||
out["extra"] = QJsonObject::fromVariantMap(t.extra);
|
|
||||||
}
|
|
||||||
if(out.size()) {
|
|
||||||
parent[tokenName] = out;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Katabasis::Token tokenFromJSON(const QJsonObject &parent, const char * tokenName) {
|
|
||||||
Katabasis::Token out;
|
|
||||||
auto tokenObject = parent.value(tokenName).toObject();
|
|
||||||
if(tokenObject.isEmpty()) {
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
auto issueInstant = tokenObject.value("iat");
|
|
||||||
if(issueInstant.isDouble()) {
|
|
||||||
out.issueInstant = QDateTime::fromSecsSinceEpoch((int64_t) issueInstant.toDouble());
|
|
||||||
}
|
|
||||||
|
|
||||||
auto notAfter = tokenObject.value("exp");
|
|
||||||
if(notAfter.isDouble()) {
|
|
||||||
out.notAfter = QDateTime::fromSecsSinceEpoch((int64_t) notAfter.toDouble());
|
|
||||||
}
|
|
||||||
|
|
||||||
auto token = tokenObject.value("token");
|
|
||||||
if(token.isString()) {
|
|
||||||
out.token = token.toString();
|
|
||||||
out.validity = Katabasis::Validity::Assumed;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto refresh_token = tokenObject.value("refresh_token");
|
|
||||||
if(refresh_token.isString()) {
|
|
||||||
out.refresh_token = refresh_token.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
auto extra = tokenObject.value("extra");
|
|
||||||
if(extra.isObject()) {
|
|
||||||
out.extra = extra.toObject().toVariantMap();
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
void profileToJSON(QJsonObject &parent, MinecraftProfile p, const char * tokenName) {
|
|
||||||
if(p.id.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
QJsonObject out;
|
|
||||||
out["id"] = QJsonValue(p.id);
|
|
||||||
out["name"] = QJsonValue(p.name);
|
|
||||||
if(p.currentCape != -1) {
|
|
||||||
out["cape"] = p.capes[p.currentCape].id;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
QJsonObject skinObj;
|
case STATE_WORKING:
|
||||||
skinObj["id"] = p.skin.id;
|
switch(m_stage) {
|
||||||
skinObj["url"] = p.skin.url;
|
case MSAStage::Idle: {
|
||||||
skinObj["variant"] = p.skin.variant;
|
QString loginMessage = tr("Logging in as %1 user");
|
||||||
if(p.skin.data.size()) {
|
if(m_data->type == AccountType::MSA) {
|
||||||
skinObj["data"] = QString::fromLatin1(p.skin.data.toBase64());
|
return loginMessage.arg("Microsoft");
|
||||||
}
|
}
|
||||||
out["skin"] = skinObj;
|
else {
|
||||||
}
|
return loginMessage.arg("Mojang");
|
||||||
|
|
||||||
QJsonArray capesArray;
|
|
||||||
for(auto & cape: p.capes) {
|
|
||||||
QJsonObject capeObj;
|
|
||||||
capeObj["id"] = cape.id;
|
|
||||||
capeObj["url"] = cape.url;
|
|
||||||
capeObj["alias"] = cape.alias;
|
|
||||||
if(cape.data.size()) {
|
|
||||||
capeObj["data"] = QString::fromLatin1(cape.data.toBase64());
|
|
||||||
}
|
|
||||||
capesArray.push_back(capeObj);
|
|
||||||
}
|
|
||||||
out["capes"] = capesArray;
|
|
||||||
parent[tokenName] = out;
|
|
||||||
}
|
|
||||||
|
|
||||||
MinecraftProfile profileFromJSON(const QJsonObject &parent, const char * tokenName) {
|
|
||||||
MinecraftProfile out;
|
|
||||||
auto tokenObject = parent.value(tokenName).toObject();
|
|
||||||
if(tokenObject.isEmpty()) {
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
{
|
|
||||||
auto idV = tokenObject.value("id");
|
|
||||||
auto nameV = tokenObject.value("name");
|
|
||||||
if(!idV.isString() || !nameV.isString()) {
|
|
||||||
qWarning() << "mandatory profile attributes are missing or of unexpected type";
|
|
||||||
return MinecraftProfile();
|
|
||||||
}
|
|
||||||
out.name = nameV.toString();
|
|
||||||
out.id = idV.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
auto skinV = tokenObject.value("skin");
|
|
||||||
if(!skinV.isObject()) {
|
|
||||||
qWarning() << "skin is missing";
|
|
||||||
return MinecraftProfile();
|
|
||||||
}
|
|
||||||
auto skinObj = skinV.toObject();
|
|
||||||
auto idV = skinObj.value("id");
|
|
||||||
auto urlV = skinObj.value("url");
|
|
||||||
auto variantV = skinObj.value("variant");
|
|
||||||
if(!idV.isString() || !urlV.isString() || !variantV.isString()) {
|
|
||||||
qWarning() << "mandatory skin attributes are missing or of unexpected type";
|
|
||||||
return MinecraftProfile();
|
|
||||||
}
|
|
||||||
out.skin.id = idV.toString();
|
|
||||||
out.skin.url = urlV.toString();
|
|
||||||
out.skin.variant = variantV.toString();
|
|
||||||
|
|
||||||
// data for skin is optional
|
|
||||||
auto dataV = skinObj.value("data");
|
|
||||||
if(dataV.isString()) {
|
|
||||||
// TODO: validate base64
|
|
||||||
out.skin.data = QByteArray::fromBase64(dataV.toString().toLatin1());
|
|
||||||
}
|
|
||||||
else if (!dataV.isUndefined()) {
|
|
||||||
qWarning() << "skin data is something unexpected";
|
|
||||||
return MinecraftProfile();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case MSAStage::UserAuth:
|
||||||
auto capesV = tokenObject.value("capes");
|
return tr("Logging in as XBox user");
|
||||||
if(!capesV.isArray()) {
|
case MSAStage::XboxAuth:
|
||||||
qWarning() << "capes is not an array!";
|
return tr("Logging in with XBox and Mojang services");
|
||||||
return MinecraftProfile();
|
case MSAStage::MinecraftProfile:
|
||||||
|
return tr("Getting Minecraft profile");
|
||||||
|
case MSAStage::Skin:
|
||||||
|
return tr("Getting Minecraft skin");
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
auto capesArray = capesV.toArray();
|
default:
|
||||||
for(auto capeV: capesArray) {
|
return AccountTask::getStateMessage();
|
||||||
if(!capeV.isObject()) {
|
|
||||||
qWarning() << "cape is not an object!";
|
|
||||||
return MinecraftProfile();
|
|
||||||
}
|
}
|
||||||
auto capeObj = capeV.toObject();
|
|
||||||
auto idV = capeObj.value("id");
|
|
||||||
auto urlV = capeObj.value("url");
|
|
||||||
auto aliasV = capeObj.value("alias");
|
|
||||||
if(!idV.isString() || !urlV.isString() || !aliasV.isString()) {
|
|
||||||
qWarning() << "mandatory skin attributes are missing or of unexpected type";
|
|
||||||
return MinecraftProfile();
|
|
||||||
}
|
|
||||||
Cape cape;
|
|
||||||
cape.id = idV.toString();
|
|
||||||
cape.url = urlV.toString();
|
|
||||||
cape.alias = aliasV.toString();
|
|
||||||
|
|
||||||
// data for cape is optional.
|
|
||||||
auto dataV = capeObj.value("data");
|
|
||||||
if(dataV.isString()) {
|
|
||||||
// TODO: validate base64
|
|
||||||
cape.data = QByteArray::fromBase64(dataV.toString().toLatin1());
|
|
||||||
}
|
|
||||||
else if (!dataV.isUndefined()) {
|
|
||||||
qWarning() << "cape data is something unexpected";
|
|
||||||
return MinecraftProfile();
|
|
||||||
}
|
|
||||||
out.capes.push_back(cape);
|
|
||||||
}
|
|
||||||
out.validity = Katabasis::Validity::Assumed;
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Context::resumeFromState(QByteArray data) {
|
|
||||||
QJsonParseError error;
|
|
||||||
auto doc = QJsonDocument::fromJson(data, &error);
|
|
||||||
if(error.error != QJsonParseError::NoError) {
|
|
||||||
qWarning() << "Failed to parse account data as JSON.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
auto docObject = doc.object();
|
|
||||||
m_account.msaToken = tokenFromJSON(docObject, "msa");
|
|
||||||
m_account.userToken = tokenFromJSON(docObject, "utoken");
|
|
||||||
m_account.xboxApiToken = tokenFromJSON(docObject, "xrp-main");
|
|
||||||
m_account.mojangservicesToken = tokenFromJSON(docObject, "xrp-mc");
|
|
||||||
m_account.minecraftToken = tokenFromJSON(docObject, "ygg");
|
|
||||||
|
|
||||||
m_account.minecraftProfile = profileFromJSON(docObject, "profile");
|
|
||||||
|
|
||||||
m_account.validity_ = m_account.minecraftProfile.validity;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
QByteArray Context::saveState() {
|
|
||||||
QJsonDocument doc;
|
|
||||||
QJsonObject output;
|
|
||||||
tokenToJSON(output, m_account.msaToken, "msa");
|
|
||||||
tokenToJSON(output, m_account.userToken, "utoken");
|
|
||||||
tokenToJSON(output, m_account.xboxApiToken, "xrp-main");
|
|
||||||
tokenToJSON(output, m_account.mojangservicesToken, "xrp-mc");
|
|
||||||
tokenToJSON(output, m_account.minecraftToken, "ygg");
|
|
||||||
profileToJSON(output, m_account.minecraftProfile, "profile");
|
|
||||||
doc.setObject(output);
|
|
||||||
return doc.toJson(QJsonDocument::Indented);
|
|
||||||
}
|
}
|
@ -7,87 +7,47 @@
|
|||||||
#include <QImage>
|
#include <QImage>
|
||||||
|
|
||||||
#include <katabasis/OAuth2.h>
|
#include <katabasis/OAuth2.h>
|
||||||
|
#include "Yggdrasil.h"
|
||||||
|
#include "../AccountData.h"
|
||||||
|
#include "../AccountTask.h"
|
||||||
|
|
||||||
struct Skin {
|
class AuthContext : public AccountTask
|
||||||
QString id;
|
|
||||||
QString url;
|
|
||||||
QString variant;
|
|
||||||
|
|
||||||
QByteArray data;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Cape {
|
|
||||||
QString id;
|
|
||||||
QString url;
|
|
||||||
QString alias;
|
|
||||||
|
|
||||||
QByteArray data;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct MinecraftProfile {
|
|
||||||
QString id;
|
|
||||||
QString name;
|
|
||||||
Skin skin;
|
|
||||||
int currentCape = -1;
|
|
||||||
QVector<Cape> capes;
|
|
||||||
Katabasis::Validity validity = Katabasis::Validity::None;
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class AccountType {
|
|
||||||
MSA,
|
|
||||||
Mojang
|
|
||||||
};
|
|
||||||
|
|
||||||
struct AccountData {
|
|
||||||
AccountType type = AccountType::MSA;
|
|
||||||
|
|
||||||
Katabasis::Token msaToken;
|
|
||||||
Katabasis::Token userToken;
|
|
||||||
Katabasis::Token xboxApiToken;
|
|
||||||
Katabasis::Token mojangservicesToken;
|
|
||||||
Katabasis::Token minecraftToken;
|
|
||||||
|
|
||||||
MinecraftProfile minecraftProfile;
|
|
||||||
Katabasis::Validity validity_ = Katabasis::Validity::None;
|
|
||||||
};
|
|
||||||
|
|
||||||
class Context : public QObject
|
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit Context(QObject *parent = 0);
|
explicit AuthContext(AccountData * data, QObject *parent = 0);
|
||||||
|
|
||||||
QByteArray saveState();
|
|
||||||
bool resumeFromState(QByteArray data);
|
|
||||||
|
|
||||||
bool isBusy() {
|
bool isBusy() {
|
||||||
return activity_ != Katabasis::Activity::Idle;
|
return m_activity != Katabasis::Activity::Idle;
|
||||||
};
|
};
|
||||||
Katabasis::Validity validity() {
|
Katabasis::Validity validity() {
|
||||||
return m_account.validity_;
|
return m_data->validity_;
|
||||||
};
|
};
|
||||||
|
|
||||||
bool signIn();
|
//bool signOut();
|
||||||
bool silentSignIn();
|
|
||||||
bool signOut();
|
QString getStateMessage() const override;
|
||||||
|
|
||||||
QString userName();
|
|
||||||
QString userId();
|
|
||||||
QString gameToken();
|
|
||||||
signals:
|
signals:
|
||||||
void succeeded();
|
|
||||||
void failed();
|
|
||||||
void activityChanged(Katabasis::Activity activity);
|
void activityChanged(Katabasis::Activity activity);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onLinkingSucceeded();
|
// OAuth-specific callbacks
|
||||||
void onLinkingFailed();
|
void onOAuthLinkingSucceeded();
|
||||||
|
void onOAuthLinkingFailed();
|
||||||
void onOpenBrowser(const QUrl &url);
|
void onOpenBrowser(const QUrl &url);
|
||||||
void onCloseBrowser();
|
void onCloseBrowser();
|
||||||
void onOAuthActivityChanged(Katabasis::Activity activity);
|
void onOAuthActivityChanged(Katabasis::Activity activity);
|
||||||
|
|
||||||
private:
|
// Yggdrasil specific callbacks
|
||||||
|
void onMojangSucceeded();
|
||||||
|
void onMojangFailed();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void initMSA();
|
||||||
|
void initMojang();
|
||||||
|
|
||||||
void doUserAuth();
|
void doUserAuth();
|
||||||
Q_SLOT void onUserAuthDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
|
Q_SLOT void onUserAuthDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
|
||||||
|
|
||||||
@ -109,20 +69,26 @@ private:
|
|||||||
|
|
||||||
void checkResult();
|
void checkResult();
|
||||||
|
|
||||||
private:
|
protected:
|
||||||
void beginActivity(Katabasis::Activity activity);
|
void beginActivity(Katabasis::Activity activity);
|
||||||
void finishActivity();
|
void finishActivity();
|
||||||
void clearTokens();
|
void clearTokens();
|
||||||
|
|
||||||
private:
|
protected:
|
||||||
Katabasis::OAuth2 *oauth2 = nullptr;
|
Katabasis::OAuth2 *m_oauth2 = nullptr;
|
||||||
|
Yggdrasil *m_yggdrasil = nullptr;
|
||||||
|
|
||||||
int requestsDone = 0;
|
int m_requestsDone = 0;
|
||||||
bool xboxProfileSucceeded = false;
|
bool m_xboxProfileSucceeded = false;
|
||||||
bool mcAuthSucceeded = false;
|
bool m_mcAuthSucceeded = false;
|
||||||
Katabasis::Activity activity_ = Katabasis::Activity::Idle;
|
Katabasis::Activity m_activity = Katabasis::Activity::Idle;
|
||||||
|
enum class MSAStage {
|
||||||
AccountData m_account;
|
Idle,
|
||||||
|
UserAuth,
|
||||||
|
XboxAuth,
|
||||||
|
MinecraftProfile,
|
||||||
|
Skin
|
||||||
|
} m_stage = MSAStage::Idle;
|
||||||
|
|
||||||
QNetworkAccessManager *mgr = nullptr;
|
QNetworkAccessManager *mgr = nullptr;
|
||||||
};
|
};
|
@ -1,202 +0,0 @@
|
|||||||
|
|
||||||
/* Copyright 2013-2021 MultiMC Contributors
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "AuthenticateTask.h"
|
|
||||||
#include "../MojangAccount.h"
|
|
||||||
|
|
||||||
#include <QJsonDocument>
|
|
||||||
#include <QJsonObject>
|
|
||||||
#include <QJsonArray>
|
|
||||||
#include <QVariant>
|
|
||||||
|
|
||||||
#include <QDebug>
|
|
||||||
#include <QUuid>
|
|
||||||
|
|
||||||
AuthenticateTask::AuthenticateTask(MojangAccount * account, const QString &password,
|
|
||||||
QObject *parent)
|
|
||||||
: YggdrasilTask(account, parent), m_password(password)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
QJsonObject AuthenticateTask::getRequestContent() const
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
* {
|
|
||||||
* "agent": { // optional
|
|
||||||
* "name": "Minecraft", // So far this is the only encountered value
|
|
||||||
* "version": 1 // This number might be increased
|
|
||||||
* // by the vanilla client in the future
|
|
||||||
* },
|
|
||||||
* "username": "mojang account name", // Can be an email address or player name for
|
|
||||||
// unmigrated accounts
|
|
||||||
* "password": "mojang account password",
|
|
||||||
* "clientToken": "client identifier" // optional
|
|
||||||
* "requestUser": true/false // request the user structure
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
QJsonObject req;
|
|
||||||
|
|
||||||
{
|
|
||||||
QJsonObject agent;
|
|
||||||
// C++ makes string literals void* for some stupid reason, so we have to tell it
|
|
||||||
// QString... Thanks Obama.
|
|
||||||
agent.insert("name", QString("Minecraft"));
|
|
||||||
agent.insert("version", 1);
|
|
||||||
req.insert("agent", agent);
|
|
||||||
}
|
|
||||||
|
|
||||||
req.insert("username", m_account->username());
|
|
||||||
req.insert("password", m_password);
|
|
||||||
req.insert("requestUser", true);
|
|
||||||
|
|
||||||
// If we already have a client token, give it to the server.
|
|
||||||
// Otherwise, let the server give us one.
|
|
||||||
|
|
||||||
if(m_account->m_clientToken.isEmpty())
|
|
||||||
{
|
|
||||||
auto uuid = QUuid::createUuid();
|
|
||||||
auto uuidString = uuid.toString().remove('{').remove('-').remove('}');
|
|
||||||
m_account->m_clientToken = uuidString;
|
|
||||||
}
|
|
||||||
req.insert("clientToken", m_account->m_clientToken);
|
|
||||||
|
|
||||||
return req;
|
|
||||||
}
|
|
||||||
|
|
||||||
void AuthenticateTask::processResponse(QJsonObject responseData)
|
|
||||||
{
|
|
||||||
// Read the response data. We need to get the client token, access token, and the selected
|
|
||||||
// profile.
|
|
||||||
qDebug() << "Processing authentication response.";
|
|
||||||
// qDebug() << responseData;
|
|
||||||
// If we already have a client token, make sure the one the server gave us matches our
|
|
||||||
// existing one.
|
|
||||||
qDebug() << "Getting client token.";
|
|
||||||
QString clientToken = responseData.value("clientToken").toString("");
|
|
||||||
if (clientToken.isEmpty())
|
|
||||||
{
|
|
||||||
// Fail if the server gave us an empty client token
|
|
||||||
changeState(STATE_FAILED_HARD, tr("Authentication server didn't send a client token."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!m_account->m_clientToken.isEmpty() && clientToken != m_account->m_clientToken)
|
|
||||||
{
|
|
||||||
changeState(STATE_FAILED_HARD, tr("Authentication server attempted to change the client token. This isn't supported."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Set the client token.
|
|
||||||
m_account->m_clientToken = clientToken;
|
|
||||||
|
|
||||||
// Now, we set the access token.
|
|
||||||
qDebug() << "Getting access token.";
|
|
||||||
QString accessToken = responseData.value("accessToken").toString("");
|
|
||||||
if (accessToken.isEmpty())
|
|
||||||
{
|
|
||||||
// Fail if the server didn't give us an access token.
|
|
||||||
changeState(STATE_FAILED_HARD, tr("Authentication server didn't send an access token."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Set the access token.
|
|
||||||
m_account->m_accessToken = accessToken;
|
|
||||||
|
|
||||||
// Now we load the list of available profiles.
|
|
||||||
// Mojang hasn't yet implemented the profile system,
|
|
||||||
// but we might as well support what's there so we
|
|
||||||
// don't have trouble implementing it later.
|
|
||||||
qDebug() << "Loading profile list.";
|
|
||||||
QJsonArray availableProfiles = responseData.value("availableProfiles").toArray();
|
|
||||||
QList<AccountProfile> loadedProfiles;
|
|
||||||
for (auto iter : availableProfiles)
|
|
||||||
{
|
|
||||||
QJsonObject profile = iter.toObject();
|
|
||||||
// Profiles are easy, we just need their ID and name.
|
|
||||||
QString id = profile.value("id").toString("");
|
|
||||||
QString name = profile.value("name").toString("");
|
|
||||||
bool legacy = profile.value("legacy").toBool(false);
|
|
||||||
|
|
||||||
if (id.isEmpty() || name.isEmpty())
|
|
||||||
{
|
|
||||||
// This should never happen, but we might as well
|
|
||||||
// warn about it if it does so we can debug it easily.
|
|
||||||
// You never know when Mojang might do something truly derpy.
|
|
||||||
qWarning() << "Found entry in available profiles list with missing ID or name "
|
|
||||||
"field. Ignoring it.";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now, add a new AccountProfile entry to the list.
|
|
||||||
loadedProfiles.append({id, name, legacy});
|
|
||||||
}
|
|
||||||
// Put the list of profiles we loaded into the MojangAccount object.
|
|
||||||
m_account->m_profiles = loadedProfiles;
|
|
||||||
|
|
||||||
// Finally, we set the current profile to the correct value. This is pretty simple.
|
|
||||||
// We do need to make sure that the current profile that the server gave us
|
|
||||||
// is actually in the available profiles list.
|
|
||||||
// If it isn't, we'll just fail horribly (*shouldn't* ever happen, but you never know).
|
|
||||||
qDebug() << "Setting current profile.";
|
|
||||||
QJsonObject currentProfile = responseData.value("selectedProfile").toObject();
|
|
||||||
QString currentProfileId = currentProfile.value("id").toString("");
|
|
||||||
if (currentProfileId.isEmpty())
|
|
||||||
{
|
|
||||||
changeState(STATE_FAILED_HARD, tr("Authentication server didn't specify a currently selected profile. The account exists, but likely isn't premium."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!m_account->setCurrentProfile(currentProfileId))
|
|
||||||
{
|
|
||||||
changeState(STATE_FAILED_HARD, tr("Authentication server specified a selected profile that wasn't in the available profiles list."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// this is what the vanilla launcher passes to the userProperties launch param
|
|
||||||
if (responseData.contains("user"))
|
|
||||||
{
|
|
||||||
User u;
|
|
||||||
auto obj = responseData.value("user").toObject();
|
|
||||||
u.id = obj.value("id").toString();
|
|
||||||
auto propArray = obj.value("properties").toArray();
|
|
||||||
for (auto prop : propArray)
|
|
||||||
{
|
|
||||||
auto propTuple = prop.toObject();
|
|
||||||
auto name = propTuple.value("name").toString();
|
|
||||||
auto value = propTuple.value("value").toString();
|
|
||||||
u.properties.insert(name, value);
|
|
||||||
}
|
|
||||||
m_account->m_user = u;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We've made it through the minefield of possible errors. Return true to indicate that
|
|
||||||
// we've succeeded.
|
|
||||||
qDebug() << "Finished reading authentication response.";
|
|
||||||
changeState(STATE_SUCCEEDED);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString AuthenticateTask::getEndpoint() const
|
|
||||||
{
|
|
||||||
return "authenticate";
|
|
||||||
}
|
|
||||||
|
|
||||||
QString AuthenticateTask::getStateMessage() const
|
|
||||||
{
|
|
||||||
switch (m_state)
|
|
||||||
{
|
|
||||||
case STATE_SENDING_REQUEST:
|
|
||||||
return tr("Authenticating: Sending request...");
|
|
||||||
case STATE_PROCESSING_RESPONSE:
|
|
||||||
return tr("Authenticating: Processing response...");
|
|
||||||
default:
|
|
||||||
return YggdrasilTask::getStateMessage();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
/* Copyright 2013-2021 MultiMC Contributors
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "../YggdrasilTask.h"
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QString>
|
|
||||||
#include <QJsonObject>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The authenticate task takes a MojangAccount with no access token and password and attempts to
|
|
||||||
* authenticate with Mojang's servers.
|
|
||||||
* If successful, it will set the MojangAccount's access token.
|
|
||||||
*/
|
|
||||||
class AuthenticateTask : public YggdrasilTask
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
AuthenticateTask(MojangAccount *account, const QString &password, QObject *parent = 0);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
virtual QJsonObject getRequestContent() const override;
|
|
||||||
|
|
||||||
virtual QString getEndpoint() const override;
|
|
||||||
|
|
||||||
virtual void processResponse(QJsonObject responseData) override;
|
|
||||||
|
|
||||||
virtual QString getStateMessage() const override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
QString m_password;
|
|
||||||
};
|
|
51
launcher/minecraft/auth/flows/MSAHelper.txt
Normal file
51
launcher/minecraft/auth/flows/MSAHelper.txt
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
class Helper : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
Helper(MSAFlows * context) : QObject(), context_(context), msg_(QString()) {
|
||||||
|
QFile tokenCache("usercache.dat");
|
||||||
|
if(tokenCache.open(QIODevice::ReadOnly)) {
|
||||||
|
context_->resumeFromState(tokenCache.readAll());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void run() {
|
||||||
|
connect(context_, &MSAFlows::activityChanged, this, &Helper::onActivityChanged);
|
||||||
|
context_->silentSignIn();
|
||||||
|
}
|
||||||
|
|
||||||
|
void onFailed() {
|
||||||
|
qDebug() << "Login failed";
|
||||||
|
}
|
||||||
|
|
||||||
|
void onActivityChanged(Katabasis::Activity activity) {
|
||||||
|
if(activity == Katabasis::Activity::Idle) {
|
||||||
|
switch(context_->validity()) {
|
||||||
|
case Katabasis::Validity::None: {
|
||||||
|
// account is gone, remove it.
|
||||||
|
QFile::remove("usercache.dat");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Katabasis::Validity::Assumed: {
|
||||||
|
// this is basically a soft-failed refresh. do nothing.
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Katabasis::Validity::Certain: {
|
||||||
|
// stuff got refreshed / signed in. Save.
|
||||||
|
auto data = context_->saveState();
|
||||||
|
QSaveFile tokenCache("usercache.dat");
|
||||||
|
if(tokenCache.open(QIODevice::WriteOnly)) {
|
||||||
|
tokenCache.write(context_->saveState());
|
||||||
|
tokenCache.commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
MSAFlows *context_;
|
||||||
|
QString msg_;
|
||||||
|
};
|
20
launcher/minecraft/auth/flows/MSAInteractive.cpp
Normal file
20
launcher/minecraft/auth/flows/MSAInteractive.cpp
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
#include "MSAInteractive.h"
|
||||||
|
|
||||||
|
MSAInteractive::MSAInteractive(AccountData* data, QObject* parent) : AuthContext(data, parent) {}
|
||||||
|
|
||||||
|
void MSAInteractive::executeTask() {
|
||||||
|
m_requestsDone = 0;
|
||||||
|
m_xboxProfileSucceeded = false;
|
||||||
|
m_mcAuthSucceeded = false;
|
||||||
|
|
||||||
|
initMSA();
|
||||||
|
|
||||||
|
QVariantMap extraOpts;
|
||||||
|
extraOpts["prompt"] = "select_account";
|
||||||
|
m_oauth2->setExtraRequestParams(extraOpts);
|
||||||
|
|
||||||
|
beginActivity(Katabasis::Activity::LoggingIn);
|
||||||
|
m_oauth2->unlink();
|
||||||
|
*m_data = AccountData();
|
||||||
|
m_oauth2->link();
|
||||||
|
}
|
10
launcher/minecraft/auth/flows/MSAInteractive.h
Normal file
10
launcher/minecraft/auth/flows/MSAInteractive.h
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "AuthContext.h"
|
||||||
|
|
||||||
|
class MSAInteractive : public AuthContext
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit MSAInteractive(AccountData * data, QObject *parent = 0);
|
||||||
|
void executeTask() override;
|
||||||
|
};
|
16
launcher/minecraft/auth/flows/MSASilent.cpp
Normal file
16
launcher/minecraft/auth/flows/MSASilent.cpp
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
#include "MSASilent.h"
|
||||||
|
|
||||||
|
MSASilent::MSASilent(AccountData* data, QObject* parent) : AuthContext(data, parent) {}
|
||||||
|
|
||||||
|
void MSASilent::executeTask() {
|
||||||
|
m_requestsDone = 0;
|
||||||
|
m_xboxProfileSucceeded = false;
|
||||||
|
m_mcAuthSucceeded = false;
|
||||||
|
|
||||||
|
initMSA();
|
||||||
|
|
||||||
|
beginActivity(Katabasis::Activity::Refreshing);
|
||||||
|
if(!m_oauth2->refresh()) {
|
||||||
|
finishActivity();
|
||||||
|
}
|
||||||
|
}
|
10
launcher/minecraft/auth/flows/MSASilent.h
Normal file
10
launcher/minecraft/auth/flows/MSASilent.h
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "AuthContext.h"
|
||||||
|
|
||||||
|
class MSASilent : public AuthContext
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit MSASilent(AccountData * data, QObject *parent = 0);
|
||||||
|
void executeTask() override;
|
||||||
|
};
|
14
launcher/minecraft/auth/flows/MojangLogin.cpp
Normal file
14
launcher/minecraft/auth/flows/MojangLogin.cpp
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
#include "MojangLogin.h"
|
||||||
|
|
||||||
|
MojangLogin::MojangLogin(AccountData* data, QString password, QObject* parent) : AuthContext(data, parent), m_password(password) {}
|
||||||
|
|
||||||
|
void MojangLogin::executeTask() {
|
||||||
|
m_requestsDone = 0;
|
||||||
|
m_xboxProfileSucceeded = false;
|
||||||
|
m_mcAuthSucceeded = false;
|
||||||
|
|
||||||
|
initMojang();
|
||||||
|
|
||||||
|
beginActivity(Katabasis::Activity::LoggingIn);
|
||||||
|
m_yggdrasil->login(m_password);
|
||||||
|
}
|
13
launcher/minecraft/auth/flows/MojangLogin.h
Normal file
13
launcher/minecraft/auth/flows/MojangLogin.h
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "AuthContext.h"
|
||||||
|
|
||||||
|
class MojangLogin : public AuthContext
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit MojangLogin(AccountData * data, QString password, QObject *parent = 0);
|
||||||
|
void executeTask() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString m_password;
|
||||||
|
};
|
14
launcher/minecraft/auth/flows/MojangRefresh.cpp
Normal file
14
launcher/minecraft/auth/flows/MojangRefresh.cpp
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
#include "MojangRefresh.h"
|
||||||
|
|
||||||
|
MojangRefresh::MojangRefresh(AccountData* data, QObject* parent) : AuthContext(data, parent) {}
|
||||||
|
|
||||||
|
void MojangRefresh::executeTask() {
|
||||||
|
m_requestsDone = 0;
|
||||||
|
m_xboxProfileSucceeded = false;
|
||||||
|
m_mcAuthSucceeded = false;
|
||||||
|
|
||||||
|
initMojang();
|
||||||
|
|
||||||
|
beginActivity(Katabasis::Activity::Refreshing);
|
||||||
|
m_yggdrasil->refresh();
|
||||||
|
}
|
10
launcher/minecraft/auth/flows/MojangRefresh.h
Normal file
10
launcher/minecraft/auth/flows/MojangRefresh.h
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "AuthContext.h"
|
||||||
|
|
||||||
|
class MojangRefresh : public AuthContext
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit MojangRefresh(AccountData * data, QObject *parent = 0);
|
||||||
|
void executeTask() override;
|
||||||
|
};
|
@ -1,144 +0,0 @@
|
|||||||
/* Copyright 2013-2021 MultiMC Contributors
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "RefreshTask.h"
|
|
||||||
#include "../MojangAccount.h"
|
|
||||||
|
|
||||||
#include <QJsonDocument>
|
|
||||||
#include <QJsonObject>
|
|
||||||
#include <QJsonArray>
|
|
||||||
#include <QVariant>
|
|
||||||
|
|
||||||
#include <QDebug>
|
|
||||||
|
|
||||||
RefreshTask::RefreshTask(MojangAccount *account) : YggdrasilTask(account)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
QJsonObject RefreshTask::getRequestContent() const
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
* {
|
|
||||||
* "clientToken": "client identifier"
|
|
||||||
* "accessToken": "current access token to be refreshed"
|
|
||||||
* "selectedProfile": // specifying this causes errors
|
|
||||||
* {
|
|
||||||
* "id": "profile ID"
|
|
||||||
* "name": "profile name"
|
|
||||||
* }
|
|
||||||
* "requestUser": true/false // request the user structure
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
QJsonObject req;
|
|
||||||
req.insert("clientToken", m_account->m_clientToken);
|
|
||||||
req.insert("accessToken", m_account->m_accessToken);
|
|
||||||
/*
|
|
||||||
{
|
|
||||||
auto currentProfile = m_account->currentProfile();
|
|
||||||
QJsonObject profile;
|
|
||||||
profile.insert("id", currentProfile->id());
|
|
||||||
profile.insert("name", currentProfile->name());
|
|
||||||
req.insert("selectedProfile", profile);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
req.insert("requestUser", true);
|
|
||||||
|
|
||||||
return req;
|
|
||||||
}
|
|
||||||
|
|
||||||
void RefreshTask::processResponse(QJsonObject responseData)
|
|
||||||
{
|
|
||||||
// Read the response data. We need to get the client token, access token, and the selected
|
|
||||||
// profile.
|
|
||||||
qDebug() << "Processing authentication response.";
|
|
||||||
|
|
||||||
// qDebug() << responseData;
|
|
||||||
// If we already have a client token, make sure the one the server gave us matches our
|
|
||||||
// existing one.
|
|
||||||
QString clientToken = responseData.value("clientToken").toString("");
|
|
||||||
if (clientToken.isEmpty())
|
|
||||||
{
|
|
||||||
// Fail if the server gave us an empty client token
|
|
||||||
changeState(STATE_FAILED_HARD, tr("Authentication server didn't send a client token."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!m_account->m_clientToken.isEmpty() && clientToken != m_account->m_clientToken)
|
|
||||||
{
|
|
||||||
changeState(STATE_FAILED_HARD, tr("Authentication server attempted to change the client token. This isn't supported."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now, we set the access token.
|
|
||||||
qDebug() << "Getting new access token.";
|
|
||||||
QString accessToken = responseData.value("accessToken").toString("");
|
|
||||||
if (accessToken.isEmpty())
|
|
||||||
{
|
|
||||||
// Fail if the server didn't give us an access token.
|
|
||||||
changeState(STATE_FAILED_HARD, tr("Authentication server didn't send an access token."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// we validate that the server responded right. (our current profile = returned current
|
|
||||||
// profile)
|
|
||||||
QJsonObject currentProfile = responseData.value("selectedProfile").toObject();
|
|
||||||
QString currentProfileId = currentProfile.value("id").toString("");
|
|
||||||
if (m_account->currentProfile()->id != currentProfileId)
|
|
||||||
{
|
|
||||||
changeState(STATE_FAILED_HARD, tr("Authentication server didn't specify the same prefile as expected."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// this is what the vanilla launcher passes to the userProperties launch param
|
|
||||||
if (responseData.contains("user"))
|
|
||||||
{
|
|
||||||
User u;
|
|
||||||
auto obj = responseData.value("user").toObject();
|
|
||||||
u.id = obj.value("id").toString();
|
|
||||||
auto propArray = obj.value("properties").toArray();
|
|
||||||
for (auto prop : propArray)
|
|
||||||
{
|
|
||||||
auto propTuple = prop.toObject();
|
|
||||||
auto name = propTuple.value("name").toString();
|
|
||||||
auto value = propTuple.value("value").toString();
|
|
||||||
u.properties.insert(name, value);
|
|
||||||
}
|
|
||||||
m_account->m_user = u;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We've made it through the minefield of possible errors. Return true to indicate that
|
|
||||||
// we've succeeded.
|
|
||||||
qDebug() << "Finished reading refresh response.";
|
|
||||||
// Reset the access token.
|
|
||||||
m_account->m_accessToken = accessToken;
|
|
||||||
changeState(STATE_SUCCEEDED);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString RefreshTask::getEndpoint() const
|
|
||||||
{
|
|
||||||
return "refresh";
|
|
||||||
}
|
|
||||||
|
|
||||||
QString RefreshTask::getStateMessage() const
|
|
||||||
{
|
|
||||||
switch (m_state)
|
|
||||||
{
|
|
||||||
case STATE_SENDING_REQUEST:
|
|
||||||
return tr("Refreshing login token...");
|
|
||||||
case STATE_PROCESSING_RESPONSE:
|
|
||||||
return tr("Refreshing login token: Processing response...");
|
|
||||||
default:
|
|
||||||
return YggdrasilTask::getStateMessage();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
/* Copyright 2013-2021 MultiMC Contributors
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "../YggdrasilTask.h"
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QString>
|
|
||||||
#include <QJsonObject>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The authenticate task takes a MojangAccount with a possibly timed-out access token
|
|
||||||
* and attempts to authenticate with Mojang's servers.
|
|
||||||
* If successful, it will set the new access token. The token is considered validated.
|
|
||||||
*/
|
|
||||||
class RefreshTask : public YggdrasilTask
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
RefreshTask(MojangAccount * account);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
virtual QJsonObject getRequestContent() const override;
|
|
||||||
|
|
||||||
virtual QString getEndpoint() const override;
|
|
||||||
|
|
||||||
virtual void processResponse(QJsonObject responseData) override;
|
|
||||||
|
|
||||||
virtual QString getStateMessage() const override;
|
|
||||||
};
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
|||||||
|
|
||||||
/* Copyright 2013-2021 MultiMC Contributors
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "ValidateTask.h"
|
|
||||||
#include "../MojangAccount.h"
|
|
||||||
|
|
||||||
#include <QJsonDocument>
|
|
||||||
#include <QJsonObject>
|
|
||||||
#include <QJsonArray>
|
|
||||||
#include <QVariant>
|
|
||||||
|
|
||||||
#include <QDebug>
|
|
||||||
|
|
||||||
ValidateTask::ValidateTask(MojangAccount * account, QObject *parent)
|
|
||||||
: YggdrasilTask(account, parent)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
QJsonObject ValidateTask::getRequestContent() const
|
|
||||||
{
|
|
||||||
QJsonObject req;
|
|
||||||
req.insert("accessToken", m_account->m_accessToken);
|
|
||||||
return req;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ValidateTask::processResponse(QJsonObject responseData)
|
|
||||||
{
|
|
||||||
// Assume that if processError wasn't called, then the request was successful.
|
|
||||||
changeState(YggdrasilTask::STATE_SUCCEEDED);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ValidateTask::getEndpoint() const
|
|
||||||
{
|
|
||||||
return "validate";
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ValidateTask::getStateMessage() const
|
|
||||||
{
|
|
||||||
switch (m_state)
|
|
||||||
{
|
|
||||||
case YggdrasilTask::STATE_SENDING_REQUEST:
|
|
||||||
return tr("Validating access token: Sending request...");
|
|
||||||
case YggdrasilTask::STATE_PROCESSING_RESPONSE:
|
|
||||||
return tr("Validating access token: Processing response...");
|
|
||||||
default:
|
|
||||||
return YggdrasilTask::getStateMessage();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
/* Copyright 2013-2021 MultiMC Contributors
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* :FIXME: DEAD CODE, DEAD CODE, DEAD CODE! :FIXME:
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "../YggdrasilTask.h"
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QString>
|
|
||||||
#include <QJsonObject>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The validate task takes a MojangAccount and checks to make sure its access token is valid.
|
|
||||||
*/
|
|
||||||
class ValidateTask : public YggdrasilTask
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
ValidateTask(MojangAccount *account, QObject *parent = 0);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
virtual QJsonObject getRequestContent() const override;
|
|
||||||
|
|
||||||
virtual QString getEndpoint() const override;
|
|
||||||
|
|
||||||
virtual void processResponse(QJsonObject responseData) override;
|
|
||||||
|
|
||||||
virtual QString getStateMessage() const override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
};
|
|
@ -13,8 +13,8 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "YggdrasilTask.h"
|
#include "Yggdrasil.h"
|
||||||
#include "MojangAccount.h"
|
#include "../AccountData.h"
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
@ -29,68 +29,147 @@
|
|||||||
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
|
||||||
YggdrasilTask::YggdrasilTask(MojangAccount *account, QObject *parent)
|
Yggdrasil::Yggdrasil(AccountData *data, QObject *parent)
|
||||||
: Task(parent), m_account(account)
|
: AccountTask(data, parent)
|
||||||
{
|
{
|
||||||
changeState(STATE_CREATED);
|
changeState(STATE_CREATED);
|
||||||
}
|
}
|
||||||
|
|
||||||
void YggdrasilTask::executeTask()
|
void Yggdrasil::sendRequest(QUrl endpoint, QByteArray content) {
|
||||||
{
|
changeState(STATE_WORKING);
|
||||||
changeState(STATE_SENDING_REQUEST);
|
|
||||||
|
|
||||||
// Get the content of the request we're going to send to the server.
|
QNetworkRequest netRequest(endpoint);
|
||||||
QJsonDocument doc(getRequestContent());
|
|
||||||
|
|
||||||
QUrl reqUrl(BuildConfig.AUTH_BASE + getEndpoint());
|
|
||||||
QNetworkRequest netRequest(reqUrl);
|
|
||||||
netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
m_netReply = ENV.qnam().post(netRequest, content);
|
||||||
QByteArray requestData = doc.toJson();
|
connect(m_netReply, &QNetworkReply::finished, this, &Yggdrasil::processReply);
|
||||||
m_netReply = ENV.qnam().post(netRequest, requestData);
|
connect(m_netReply, &QNetworkReply::uploadProgress, this, &Yggdrasil::refreshTimers);
|
||||||
connect(m_netReply, &QNetworkReply::finished, this, &YggdrasilTask::processReply);
|
connect(m_netReply, &QNetworkReply::downloadProgress, this, &Yggdrasil::refreshTimers);
|
||||||
connect(m_netReply, &QNetworkReply::uploadProgress, this, &YggdrasilTask::refreshTimers);
|
connect(m_netReply, &QNetworkReply::sslErrors, this, &Yggdrasil::sslErrors);
|
||||||
connect(m_netReply, &QNetworkReply::downloadProgress, this, &YggdrasilTask::refreshTimers);
|
|
||||||
connect(m_netReply, &QNetworkReply::sslErrors, this, &YggdrasilTask::sslErrors);
|
|
||||||
timeout_keeper.setSingleShot(true);
|
timeout_keeper.setSingleShot(true);
|
||||||
timeout_keeper.start(timeout_max);
|
timeout_keeper.start(timeout_max);
|
||||||
counter.setSingleShot(false);
|
counter.setSingleShot(false);
|
||||||
counter.start(time_step);
|
counter.start(time_step);
|
||||||
progress(0, timeout_max);
|
progress(0, timeout_max);
|
||||||
connect(&timeout_keeper, &QTimer::timeout, this, &YggdrasilTask::abortByTimeout);
|
connect(&timeout_keeper, &QTimer::timeout, this, &Yggdrasil::abortByTimeout);
|
||||||
connect(&counter, &QTimer::timeout, this, &YggdrasilTask::heartbeat);
|
connect(&counter, &QTimer::timeout, this, &Yggdrasil::heartbeat);
|
||||||
}
|
}
|
||||||
|
|
||||||
void YggdrasilTask::refreshTimers(qint64, qint64)
|
void Yggdrasil::executeTask() {
|
||||||
|
}
|
||||||
|
|
||||||
|
void Yggdrasil::refresh() {
|
||||||
|
start();
|
||||||
|
/*
|
||||||
|
* {
|
||||||
|
* "clientToken": "client identifier"
|
||||||
|
* "accessToken": "current access token to be refreshed"
|
||||||
|
* "selectedProfile": // specifying this causes errors
|
||||||
|
* {
|
||||||
|
* "id": "profile ID"
|
||||||
|
* "name": "profile name"
|
||||||
|
* }
|
||||||
|
* "requestUser": true/false // request the user structure
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
QJsonObject req;
|
||||||
|
req.insert("clientToken", m_data->clientToken());
|
||||||
|
req.insert("accessToken", m_data->accessToken());
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
auto currentProfile = m_account->currentProfile();
|
||||||
|
QJsonObject profile;
|
||||||
|
profile.insert("id", currentProfile->id());
|
||||||
|
profile.insert("name", currentProfile->name());
|
||||||
|
req.insert("selectedProfile", profile);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
req.insert("requestUser", false);
|
||||||
|
QJsonDocument doc(req);
|
||||||
|
|
||||||
|
QUrl reqUrl(BuildConfig.AUTH_BASE + "refresh");
|
||||||
|
QByteArray requestData = doc.toJson();
|
||||||
|
|
||||||
|
sendRequest(reqUrl, requestData);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Yggdrasil::login(QString password) {
|
||||||
|
start();
|
||||||
|
/*
|
||||||
|
* {
|
||||||
|
* "agent": { // optional
|
||||||
|
* "name": "Minecraft", // So far this is the only encountered value
|
||||||
|
* "version": 1 // This number might be increased
|
||||||
|
* // by the vanilla client in the future
|
||||||
|
* },
|
||||||
|
* "username": "mojang account name", // Can be an email address or player name for
|
||||||
|
* // unmigrated accounts
|
||||||
|
* "password": "mojang account password",
|
||||||
|
* "clientToken": "client identifier", // optional
|
||||||
|
* "requestUser": true/false // request the user structure
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
QJsonObject req;
|
||||||
|
|
||||||
|
{
|
||||||
|
QJsonObject agent;
|
||||||
|
// C++ makes string literals void* for some stupid reason, so we have to tell it
|
||||||
|
// QString... Thanks Obama.
|
||||||
|
agent.insert("name", QString("Minecraft"));
|
||||||
|
agent.insert("version", 1);
|
||||||
|
req.insert("agent", agent);
|
||||||
|
}
|
||||||
|
|
||||||
|
req.insert("username", m_data->userName());
|
||||||
|
req.insert("password", password);
|
||||||
|
req.insert("requestUser", false);
|
||||||
|
|
||||||
|
// If we already have a client token, give it to the server.
|
||||||
|
// Otherwise, let the server give us one.
|
||||||
|
|
||||||
|
m_data->generateClientTokenIfMissing();
|
||||||
|
req.insert("clientToken", m_data->clientToken());
|
||||||
|
|
||||||
|
QJsonDocument doc(req);
|
||||||
|
|
||||||
|
QUrl reqUrl(BuildConfig.AUTH_BASE + "authenticate");
|
||||||
|
QNetworkRequest netRequest(reqUrl);
|
||||||
|
QByteArray requestData = doc.toJson();
|
||||||
|
|
||||||
|
sendRequest(reqUrl, requestData);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
void Yggdrasil::refreshTimers(qint64, qint64)
|
||||||
{
|
{
|
||||||
timeout_keeper.stop();
|
timeout_keeper.stop();
|
||||||
timeout_keeper.start(timeout_max);
|
timeout_keeper.start(timeout_max);
|
||||||
progress(count = 0, timeout_max);
|
progress(count = 0, timeout_max);
|
||||||
}
|
}
|
||||||
void YggdrasilTask::heartbeat()
|
void Yggdrasil::heartbeat()
|
||||||
{
|
{
|
||||||
count += time_step;
|
count += time_step;
|
||||||
progress(count, timeout_max);
|
progress(count, timeout_max);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool YggdrasilTask::abort()
|
bool Yggdrasil::abort()
|
||||||
{
|
{
|
||||||
progress(timeout_max, timeout_max);
|
progress(timeout_max, timeout_max);
|
||||||
// TODO: actually use this in a meaningful way
|
// TODO: actually use this in a meaningful way
|
||||||
m_aborted = YggdrasilTask::BY_USER;
|
m_aborted = Yggdrasil::BY_USER;
|
||||||
m_netReply->abort();
|
m_netReply->abort();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void YggdrasilTask::abortByTimeout()
|
void Yggdrasil::abortByTimeout()
|
||||||
{
|
{
|
||||||
progress(timeout_max, timeout_max);
|
progress(timeout_max, timeout_max);
|
||||||
// TODO: actually use this in a meaningful way
|
// TODO: actually use this in a meaningful way
|
||||||
m_aborted = YggdrasilTask::BY_TIMEOUT;
|
m_aborted = Yggdrasil::BY_TIMEOUT;
|
||||||
m_netReply->abort();
|
m_netReply->abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
void YggdrasilTask::sslErrors(QList<QSslError> errors)
|
void Yggdrasil::sslErrors(QList<QSslError> errors)
|
||||||
{
|
{
|
||||||
int i = 1;
|
int i = 1;
|
||||||
for (auto error : errors)
|
for (auto error : errors)
|
||||||
@ -102,9 +181,52 @@ void YggdrasilTask::sslErrors(QList<QSslError> errors)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void YggdrasilTask::processReply()
|
void Yggdrasil::processResponse(QJsonObject responseData)
|
||||||
{
|
{
|
||||||
changeState(STATE_PROCESSING_RESPONSE);
|
// Read the response data. We need to get the client token, access token, and the selected
|
||||||
|
// profile.
|
||||||
|
qDebug() << "Processing authentication response.";
|
||||||
|
|
||||||
|
// qDebug() << responseData;
|
||||||
|
// If we already have a client token, make sure the one the server gave us matches our
|
||||||
|
// existing one.
|
||||||
|
QString clientToken = responseData.value("clientToken").toString("");
|
||||||
|
if (clientToken.isEmpty())
|
||||||
|
{
|
||||||
|
// Fail if the server gave us an empty client token
|
||||||
|
changeState(STATE_FAILED_HARD, tr("Authentication server didn't send a client token."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(m_data->clientToken().isEmpty()) {
|
||||||
|
m_data->setClientToken(clientToken);
|
||||||
|
}
|
||||||
|
else if(clientToken != m_data->clientToken()) {
|
||||||
|
changeState(STATE_FAILED_HARD, tr("Authentication server attempted to change the client token. This isn't supported."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, we set the access token.
|
||||||
|
qDebug() << "Getting access token.";
|
||||||
|
QString accessToken = responseData.value("accessToken").toString("");
|
||||||
|
if (accessToken.isEmpty())
|
||||||
|
{
|
||||||
|
// Fail if the server didn't give us an access token.
|
||||||
|
changeState(STATE_FAILED_HARD, tr("Authentication server didn't send an access token."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Set the access token.
|
||||||
|
m_data->yggdrasilToken.token = accessToken;
|
||||||
|
m_data->yggdrasilToken.validity = Katabasis::Validity::Certain;
|
||||||
|
|
||||||
|
// We've made it through the minefield of possible errors. Return true to indicate that
|
||||||
|
// we've succeeded.
|
||||||
|
qDebug() << "Finished reading authentication response.";
|
||||||
|
changeState(STATE_SUCCEEDED);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Yggdrasil::processReply()
|
||||||
|
{
|
||||||
|
changeState(STATE_WORKING);
|
||||||
|
|
||||||
switch (m_netReply->error())
|
switch (m_netReply->error())
|
||||||
{
|
{
|
||||||
@ -195,7 +317,7 @@ void YggdrasilTask::processReply()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void YggdrasilTask::processError(QJsonObject responseData)
|
void Yggdrasil::processError(QJsonObject responseData)
|
||||||
{
|
{
|
||||||
QJsonValue errorVal = responseData.value("error");
|
QJsonValue errorVal = responseData.value("error");
|
||||||
QJsonValue errorMessageValue = responseData.value("errorMessage");
|
QJsonValue errorMessageValue = responseData.value("errorMessage");
|
||||||
@ -213,43 +335,3 @@ void YggdrasilTask::processError(QJsonObject responseData)
|
|||||||
changeState(STATE_FAILED_HARD, tr("An unknown Yggdrasil error occurred."));
|
changeState(STATE_FAILED_HARD, tr("An unknown Yggdrasil error occurred."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QString YggdrasilTask::getStateMessage() const
|
|
||||||
{
|
|
||||||
switch (m_state)
|
|
||||||
{
|
|
||||||
case STATE_CREATED:
|
|
||||||
return "Waiting...";
|
|
||||||
case STATE_SENDING_REQUEST:
|
|
||||||
return tr("Sending request to auth servers...");
|
|
||||||
case STATE_PROCESSING_RESPONSE:
|
|
||||||
return tr("Processing response from servers...");
|
|
||||||
case STATE_SUCCEEDED:
|
|
||||||
return tr("Authentication task succeeded.");
|
|
||||||
case STATE_FAILED_SOFT:
|
|
||||||
return tr("Failed to contact the authentication server.");
|
|
||||||
case STATE_FAILED_HARD:
|
|
||||||
return tr("Failed to authenticate.");
|
|
||||||
default:
|
|
||||||
return tr("...");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void YggdrasilTask::changeState(YggdrasilTask::State newState, QString reason)
|
|
||||||
{
|
|
||||||
m_state = newState;
|
|
||||||
setStatus(getStateMessage());
|
|
||||||
if (newState == STATE_SUCCEEDED)
|
|
||||||
{
|
|
||||||
emitSucceeded();
|
|
||||||
}
|
|
||||||
else if (newState == STATE_FAILED_HARD || newState == STATE_FAILED_SOFT)
|
|
||||||
{
|
|
||||||
emitFailed(reason);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
YggdrasilTask::State YggdrasilTask::state()
|
|
||||||
{
|
|
||||||
return m_state;
|
|
||||||
}
|
|
82
launcher/minecraft/auth/flows/Yggdrasil.h
Normal file
82
launcher/minecraft/auth/flows/Yggdrasil.h
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
/* Copyright 2013-2021 MultiMC Contributors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../AccountTask.h"
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <qsslerror.h>
|
||||||
|
|
||||||
|
#include "../MinecraftAccount.h"
|
||||||
|
|
||||||
|
class QNetworkReply;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Yggdrasil task is a task that performs an operation on a given mojang account.
|
||||||
|
*/
|
||||||
|
class Yggdrasil : public AccountTask
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit Yggdrasil(AccountData * data, QObject *parent = 0);
|
||||||
|
virtual ~Yggdrasil() {};
|
||||||
|
|
||||||
|
void refresh();
|
||||||
|
void login(QString password);
|
||||||
|
protected:
|
||||||
|
void executeTask() override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes the response received from the server.
|
||||||
|
* If an error occurred, this should emit a failed signal.
|
||||||
|
* If Yggdrasil gave an error response, it should call setError() first, and then return false.
|
||||||
|
* Otherwise, it should return true.
|
||||||
|
* Note: If the response from the server was blank, and the HTTP code was 200, this function is called with
|
||||||
|
* an empty QJsonObject.
|
||||||
|
*/
|
||||||
|
void processResponse(QJsonObject responseData);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes an error response received from the server.
|
||||||
|
* The default implementation will read data from Yggdrasil's standard error response format and set it as this task's Error.
|
||||||
|
* \returns a QString error message that will be passed to emitFailed.
|
||||||
|
*/
|
||||||
|
virtual void processError(QJsonObject responseData);
|
||||||
|
|
||||||
|
protected slots:
|
||||||
|
void processReply();
|
||||||
|
void refreshTimers(qint64, qint64);
|
||||||
|
void heartbeat();
|
||||||
|
void sslErrors(QList<QSslError>);
|
||||||
|
void abortByTimeout();
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
virtual bool abort() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void sendRequest(QUrl endpoint, QByteArray content);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
QNetworkReply *m_netReply = nullptr;
|
||||||
|
QTimer timeout_keeper;
|
||||||
|
QTimer counter;
|
||||||
|
int count = 0; // num msec since time reset
|
||||||
|
|
||||||
|
const int timeout_max = 30000;
|
||||||
|
const int time_step = 50;
|
||||||
|
};
|
@ -16,7 +16,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <launch/LaunchStep.h>
|
#include <launch/LaunchStep.h>
|
||||||
#include <minecraft/auth/MojangAccount.h>
|
#include <minecraft/auth/MinecraftAccount.h>
|
||||||
|
|
||||||
class ClaimAccount: public LaunchStep
|
class ClaimAccount: public LaunchStep
|
||||||
{
|
{
|
||||||
@ -33,5 +33,5 @@ public:
|
|||||||
}
|
}
|
||||||
private:
|
private:
|
||||||
std::unique_ptr<UseLock> lock;
|
std::unique_ptr<UseLock> lock;
|
||||||
MojangAccountPtr m_account;
|
MinecraftAccountPtr m_account;
|
||||||
};
|
};
|
||||||
|
@ -29,12 +29,13 @@
|
|||||||
#include "dialogs/CustomMessageBox.h"
|
#include "dialogs/CustomMessageBox.h"
|
||||||
#include "dialogs/SkinUploadDialog.h"
|
#include "dialogs/SkinUploadDialog.h"
|
||||||
#include "tasks/Task.h"
|
#include "tasks/Task.h"
|
||||||
#include "minecraft/auth/YggdrasilTask.h"
|
#include "minecraft/auth/AccountTask.h"
|
||||||
#include "minecraft/services/SkinDelete.h"
|
#include "minecraft/services/SkinDelete.h"
|
||||||
|
|
||||||
#include "MultiMC.h"
|
#include "MultiMC.h"
|
||||||
|
|
||||||
#include "BuildConfig.h"
|
#include "BuildConfig.h"
|
||||||
|
#include <dialogs/MSALoginDialog.h>
|
||||||
|
|
||||||
AccountListPage::AccountListPage(QWidget *parent)
|
AccountListPage::AccountListPage(QWidget *parent)
|
||||||
: QMainWindow(parent), ui(new Ui::AccountListPage)
|
: QMainWindow(parent), ui(new Ui::AccountListPage)
|
||||||
@ -50,11 +51,12 @@ AccountListPage::AccountListPage(QWidget *parent)
|
|||||||
m_accounts = MMC->accounts();
|
m_accounts = MMC->accounts();
|
||||||
|
|
||||||
ui->listView->setModel(m_accounts.get());
|
ui->listView->setModel(m_accounts.get());
|
||||||
ui->listView->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
ui->listView->header()->setSectionResizeMode(0, QHeaderView::Stretch);
|
||||||
|
ui->listView->header()->setSectionResizeMode(1, QHeaderView::Stretch);
|
||||||
|
ui->listView->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
|
||||||
ui->listView->setSelectionMode(QAbstractItemView::SingleSelection);
|
ui->listView->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||||
|
|
||||||
// Expand the account column
|
// Expand the account column
|
||||||
ui->listView->header()->setSectionResizeMode(1, QHeaderView::Stretch);
|
|
||||||
|
|
||||||
QItemSelectionModel *selectionModel = ui->listView->selectionModel();
|
QItemSelectionModel *selectionModel = ui->listView->selectionModel();
|
||||||
|
|
||||||
@ -63,8 +65,8 @@ AccountListPage::AccountListPage(QWidget *parent)
|
|||||||
});
|
});
|
||||||
connect(ui->listView, &VersionListView::customContextMenuRequested, this, &AccountListPage::ShowContextMenu);
|
connect(ui->listView, &VersionListView::customContextMenuRequested, this, &AccountListPage::ShowContextMenu);
|
||||||
|
|
||||||
connect(m_accounts.get(), SIGNAL(listChanged()), SLOT(listChanged()));
|
connect(m_accounts.get(), &AccountList::listChanged, this, &AccountListPage::listChanged);
|
||||||
connect(m_accounts.get(), SIGNAL(activeAccountChanged()), SLOT(listChanged()));
|
connect(m_accounts.get(), &AccountList::activeAccountChanged, this, &AccountListPage::listChanged);
|
||||||
|
|
||||||
updateButtonStates();
|
updateButtonStates();
|
||||||
}
|
}
|
||||||
@ -103,9 +105,36 @@ void AccountListPage::listChanged()
|
|||||||
updateButtonStates();
|
updateButtonStates();
|
||||||
}
|
}
|
||||||
|
|
||||||
void AccountListPage::on_actionAdd_triggered()
|
void AccountListPage::on_actionAddMojang_triggered()
|
||||||
{
|
{
|
||||||
addAccount(tr("Please enter your Minecraft account email and password to add your account."));
|
MinecraftAccountPtr account = LoginDialog::newAccount(
|
||||||
|
this,
|
||||||
|
tr("Please enter your Mojang account email and password to add your account.")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (account != nullptr)
|
||||||
|
{
|
||||||
|
m_accounts->addAccount(account);
|
||||||
|
if (m_accounts->count() == 1) {
|
||||||
|
m_accounts->setActiveAccount(account->profileId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AccountListPage::on_actionAddMicrosoft_triggered()
|
||||||
|
{
|
||||||
|
MinecraftAccountPtr account = MSALoginDialog::newAccount(
|
||||||
|
this,
|
||||||
|
tr("Please enter your Mojang account email and password to add your account.")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (account != nullptr)
|
||||||
|
{
|
||||||
|
m_accounts->addAccount(account);
|
||||||
|
if (m_accounts->count() == 1) {
|
||||||
|
m_accounts->setActiveAccount(account->profileId());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AccountListPage::on_actionRemove_triggered()
|
void AccountListPage::on_actionRemove_triggered()
|
||||||
@ -124,9 +153,8 @@ void AccountListPage::on_actionSetDefault_triggered()
|
|||||||
if (selection.size() > 0)
|
if (selection.size() > 0)
|
||||||
{
|
{
|
||||||
QModelIndex selected = selection.first();
|
QModelIndex selected = selection.first();
|
||||||
MojangAccountPtr account =
|
MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value<MinecraftAccountPtr>();
|
||||||
selected.data(MojangAccountList::PointerRole).value<MojangAccountPtr>();
|
m_accounts->setActiveAccount(account->profileId());
|
||||||
m_accounts->setActiveAccount(account->username());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,39 +184,13 @@ void AccountListPage::updateButtonStates()
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void AccountListPage::addAccount(const QString &errMsg)
|
|
||||||
{
|
|
||||||
// TODO: The login dialog isn't quite done yet
|
|
||||||
MojangAccountPtr account = LoginDialog::newAccount(this, errMsg);
|
|
||||||
|
|
||||||
if (account != nullptr)
|
|
||||||
{
|
|
||||||
m_accounts->addAccount(account);
|
|
||||||
if (m_accounts->count() == 1)
|
|
||||||
m_accounts->setActiveAccount(account->username());
|
|
||||||
|
|
||||||
// Grab associated player skins
|
|
||||||
auto job = new NetJob("Player skins: " + account->username());
|
|
||||||
|
|
||||||
for (AccountProfile profile : account->profiles())
|
|
||||||
{
|
|
||||||
auto meta = Env::getInstance().metacache()->resolveEntry("skins", profile.id + ".png");
|
|
||||||
auto action = Net::Download::makeCached(QUrl(BuildConfig.SKINS_BASE + profile.id + ".png"), meta);
|
|
||||||
job->addNetAction(action);
|
|
||||||
meta->setStale(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
job->start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void AccountListPage::on_actionUploadSkin_triggered()
|
void AccountListPage::on_actionUploadSkin_triggered()
|
||||||
{
|
{
|
||||||
QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes();
|
QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes();
|
||||||
if (selection.size() > 0)
|
if (selection.size() > 0)
|
||||||
{
|
{
|
||||||
QModelIndex selected = selection.first();
|
QModelIndex selected = selection.first();
|
||||||
MojangAccountPtr account = selected.data(MojangAccountList::PointerRole).value<MojangAccountPtr>();
|
MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value<MinecraftAccountPtr>();
|
||||||
SkinUploadDialog dialog(account, this);
|
SkinUploadDialog dialog(account, this);
|
||||||
dialog.exec();
|
dialog.exec();
|
||||||
}
|
}
|
||||||
@ -202,8 +204,8 @@ void AccountListPage::on_actionDeleteSkin_triggered()
|
|||||||
|
|
||||||
QModelIndex selected = selection.first();
|
QModelIndex selected = selection.first();
|
||||||
AuthSessionPtr session = std::make_shared<AuthSession>();
|
AuthSessionPtr session = std::make_shared<AuthSession>();
|
||||||
MojangAccountPtr account = selected.data(MojangAccountList::PointerRole).value<MojangAccountPtr>();
|
MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value<MinecraftAccountPtr>();
|
||||||
auto login = account->login(session);
|
auto login = account->refresh(session);
|
||||||
ProgressDialog prog(this);
|
ProgressDialog prog(this);
|
||||||
if (prog.execWithTask((Task*)login.get()) != QDialog::Accepted) {
|
if (prog.execWithTask((Task*)login.get()) != QDialog::Accepted) {
|
||||||
CustomMessageBox::selectable(this, tr("Skin Delete"), tr("Failed to login!"), QMessageBox::Warning)->exec();
|
CustomMessageBox::selectable(this, tr("Skin Delete"), tr("Failed to login!"), QMessageBox::Warning)->exec();
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
#include "pages/BasePage.h"
|
#include "pages/BasePage.h"
|
||||||
|
|
||||||
#include "minecraft/auth/MojangAccountList.h"
|
#include "minecraft/auth/AccountList.h"
|
||||||
#include "MultiMC.h"
|
#include "MultiMC.h"
|
||||||
|
|
||||||
namespace Ui
|
namespace Ui
|
||||||
@ -60,7 +60,8 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void on_actionAdd_triggered();
|
void on_actionAddMojang_triggered();
|
||||||
|
void on_actionAddMicrosoft_triggered();
|
||||||
void on_actionRemove_triggered();
|
void on_actionRemove_triggered();
|
||||||
void on_actionSetDefault_triggered();
|
void on_actionSetDefault_triggered();
|
||||||
void on_actionNoDefault_triggered();
|
void on_actionNoDefault_triggered();
|
||||||
@ -74,11 +75,10 @@ public slots:
|
|||||||
|
|
||||||
protected slots:
|
protected slots:
|
||||||
void ShowContextMenu(const QPoint &pos);
|
void ShowContextMenu(const QPoint &pos);
|
||||||
void addAccount(const QString& errMsg="");
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void changeEvent(QEvent * event) override;
|
void changeEvent(QEvent * event) override;
|
||||||
QMenu * createPopupMenu() override;
|
QMenu * createPopupMenu() override;
|
||||||
std::shared_ptr<MojangAccountList> m_accounts;
|
std::shared_ptr<AccountList> m_accounts;
|
||||||
Ui::AccountListPage *ui;
|
Ui::AccountListPage *ui;
|
||||||
};
|
};
|
||||||
|
@ -25,7 +25,23 @@
|
|||||||
<number>0</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
<item>
|
<item>
|
||||||
<widget class="VersionListView" name="listView"/>
|
<widget class="VersionListView" name="listView">
|
||||||
|
<property name="alternatingRowColors">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="rootIsDecorated">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="itemsExpandable">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="allColumnsShowFocus">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<attribute name="headerStretchLastSection">
|
||||||
|
<bool>false</bool>
|
||||||
|
</attribute>
|
||||||
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
@ -36,7 +52,8 @@
|
|||||||
<attribute name="toolBarBreak">
|
<attribute name="toolBarBreak">
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</attribute>
|
</attribute>
|
||||||
<addaction name="actionAdd"/>
|
<addaction name="actionAddMicrosoft"/>
|
||||||
|
<addaction name="actionAddMojang"/>
|
||||||
<addaction name="actionRemove"/>
|
<addaction name="actionRemove"/>
|
||||||
<addaction name="actionSetDefault"/>
|
<addaction name="actionSetDefault"/>
|
||||||
<addaction name="actionNoDefault"/>
|
<addaction name="actionNoDefault"/>
|
||||||
@ -44,9 +61,9 @@
|
|||||||
<addaction name="actionUploadSkin"/>
|
<addaction name="actionUploadSkin"/>
|
||||||
<addaction name="actionDeleteSkin"/>
|
<addaction name="actionDeleteSkin"/>
|
||||||
</widget>
|
</widget>
|
||||||
<action name="actionAdd">
|
<action name="actionAddMojang">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Add</string>
|
<string>Add Mojang</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="actionRemove">
|
<action name="actionRemove">
|
||||||
@ -80,6 +97,11 @@
|
|||||||
<string>Delete the currently active skin and go back to the default one</string>
|
<string>Delete the currently active skin and go back to the default one</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
|
<action name="actionAddMicrosoft">
|
||||||
|
<property name="text">
|
||||||
|
<string>Add Microsoft</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
</widget>
|
</widget>
|
||||||
<customwidgets>
|
<customwidgets>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
|
@ -38,7 +38,7 @@
|
|||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
|
||||||
#include "minecraft/PackProfile.h"
|
#include "minecraft/PackProfile.h"
|
||||||
#include "minecraft/auth/MojangAccountList.h"
|
#include "minecraft/auth/AccountList.h"
|
||||||
#include "minecraft/mod/Mod.h"
|
#include "minecraft/mod/Mod.h"
|
||||||
#include "icons/IconList.h"
|
#include "icons/IconList.h"
|
||||||
#include "Exception.h"
|
#include "Exception.h"
|
||||||
|
Loading…
Reference in New Issue
Block a user