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:
Petr Mrázek 2021-07-26 21:44:11 +02:00
parent fca2e9e44c
commit 3a53349e33
66 changed files with 2342 additions and 2477 deletions

View File

@ -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)

View File

@ -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@";

View File

@ -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/";

View File

@ -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"

View File

@ -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

View File

@ -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());

View File

@ -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;

View File

@ -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"));

View File

@ -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 &current, const QModelIndex &previous); void instanceChanged(const QModelIndex &current, 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;

View File

@ -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();

View File

@ -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;

View File

@ -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())
{ {

View File

@ -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);

View File

@ -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;
}; };

View File

@ -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">

View 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;
}

View 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;
};

View 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>

View File

@ -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();
} }

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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;
} }

View File

@ -1,9 +0,0 @@
#include "BuildConfig.h"
#include <QObject>
const Config BuildConfig;
Config::Config()
{
CLIENT_ID = "@MOJANGDEMO_CLIENT_ID@";
}

View File

@ -1,11 +0,0 @@
#pragma once
#include <QString>
class Config
{
public:
Config();
QString CLIENT_ID;
};
extern const Config BuildConfig;

View File

@ -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)

View File

@ -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"

View File

@ -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();
}

View File

@ -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;
};

View File

@ -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>

View 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";
}
}
}

View 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;
};

View File

@ -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)
{ {

View 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;
};

View 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);
}
}

View File

@ -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;
}; };

View File

@ -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);

View File

@ -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;

View 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.";
}
}

View File

@ -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;
}; };

View File

@ -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();
}

View File

@ -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);
};

View File

@ -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);
} }

View File

@ -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;
}; };

View File

@ -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();
}
}

View File

@ -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;
};

View 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_;
};

View 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();
}

View 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;
};

View 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();
}
}

View 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;
};

View 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);
}

View 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;
};

View 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();
}

View 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;
};

View File

@ -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();
}
}

View File

@ -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;
};

View File

@ -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();
}
}

View File

@ -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:
};

View File

@ -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;
}

View 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;
};

View File

@ -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;
}; };

View File

@ -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();

View File

@ -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;
}; };

View File

@ -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>

View File

@ -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"