From 3a53349e332599221bc325f7fac9dc7927194bc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Mon, 26 Jul 2021 21:44:11 +0200 Subject: [PATCH] GH-3392 dirty initial MSA support that shares logic with Mojang flows Both act as the first step of AuthContext. --- CMakeLists.txt | 3 + buildconfig/BuildConfig.cpp.in | 1 + buildconfig/BuildConfig.h | 6 +- launcher/BaseInstance.h | 2 +- launcher/CMakeLists.txt | 39 +- launcher/Env.cpp | 1 - launcher/LaunchController.cpp | 177 +++--- launcher/MainWindow.cpp | 158 +++--- launcher/MainWindow.h | 5 +- launcher/MultiMC.cpp | 4 +- launcher/MultiMC.h | 6 +- launcher/SkinUtils.cpp | 4 +- launcher/dialogs/LoginDialog.cpp | 9 +- launcher/dialogs/LoginDialog.h | 6 +- launcher/dialogs/LoginDialog.ui | 12 +- launcher/dialogs/MSALoginDialog.cpp | 96 ++++ launcher/dialogs/MSALoginDialog.h | 55 ++ launcher/dialogs/MSALoginDialog.ui | 60 ++ launcher/dialogs/ProfileSelectDialog.cpp | 32 +- launcher/dialogs/ProfileSelectDialog.h | 8 +- launcher/dialogs/SkinUploadDialog.cpp | 2 +- launcher/dialogs/SkinUploadDialog.h | 6 +- launcher/minecraft/MinecraftInstance.cpp | 16 +- .../minecraft/auth-msa/BuildConfig.cpp.in | 9 - launcher/minecraft/auth-msa/BuildConfig.h | 11 - launcher/minecraft/auth-msa/CMakeLists.txt | 28 - launcher/minecraft/auth-msa/main.cpp | 100 ---- launcher/minecraft/auth-msa/mainwindow.cpp | 97 ---- launcher/minecraft/auth-msa/mainwindow.h | 34 -- launcher/minecraft/auth-msa/mainwindow.ui | 72 --- launcher/minecraft/auth/AccountData.cpp | 387 +++++++++++++ launcher/minecraft/auth/AccountData.h | 73 +++ ...{MojangAccountList.cpp => AccountList.cpp} | 327 ++++++----- launcher/minecraft/auth/AccountList.h | 118 ++++ launcher/minecraft/auth/AccountTask.cpp | 69 +++ .../auth/{YggdrasilTask.h => AccountTask.h} | 78 +-- launcher/minecraft/auth/AuthSession.cpp | 2 + launcher/minecraft/auth/AuthSession.h | 13 +- launcher/minecraft/auth/MinecraftAccount.cpp | 303 ++++++++++ .../{MojangAccount.h => MinecraftAccount.h} | 120 ++-- launcher/minecraft/auth/MojangAccount.cpp | 315 ----------- launcher/minecraft/auth/MojangAccountList.h | 199 ------- .../flows/AuthContext.cpp} | 518 ++++++------------ .../context.h => auth/flows/AuthContext.h} | 106 ++-- .../minecraft/auth/flows/AuthenticateTask.cpp | 202 ------- .../minecraft/auth/flows/AuthenticateTask.h | 46 -- launcher/minecraft/auth/flows/MSAHelper.txt | 51 ++ .../minecraft/auth/flows/MSAInteractive.cpp | 20 + .../minecraft/auth/flows/MSAInteractive.h | 10 + launcher/minecraft/auth/flows/MSASilent.cpp | 16 + launcher/minecraft/auth/flows/MSASilent.h | 10 + launcher/minecraft/auth/flows/MojangLogin.cpp | 14 + launcher/minecraft/auth/flows/MojangLogin.h | 13 + .../minecraft/auth/flows/MojangRefresh.cpp | 14 + launcher/minecraft/auth/flows/MojangRefresh.h | 10 + launcher/minecraft/auth/flows/RefreshTask.cpp | 144 ----- launcher/minecraft/auth/flows/RefreshTask.h | 44 -- .../minecraft/auth/flows/ValidateTask.cpp | 61 --- launcher/minecraft/auth/flows/ValidateTask.h | 47 -- .../Yggdrasil.cpp} | 224 +++++--- launcher/minecraft/auth/flows/Yggdrasil.h | 82 +++ launcher/minecraft/launch/ClaimAccount.h | 4 +- launcher/pages/global/AccountListPage.cpp | 80 +-- launcher/pages/global/AccountListPage.h | 8 +- launcher/pages/global/AccountListPage.ui | 30 +- launcher/pages/instance/VersionPage.cpp | 2 +- 66 files changed, 2342 insertions(+), 2477 deletions(-) create mode 100644 launcher/dialogs/MSALoginDialog.cpp create mode 100644 launcher/dialogs/MSALoginDialog.h create mode 100644 launcher/dialogs/MSALoginDialog.ui delete mode 100644 launcher/minecraft/auth-msa/BuildConfig.cpp.in delete mode 100644 launcher/minecraft/auth-msa/BuildConfig.h delete mode 100644 launcher/minecraft/auth-msa/CMakeLists.txt delete mode 100644 launcher/minecraft/auth-msa/main.cpp delete mode 100644 launcher/minecraft/auth-msa/mainwindow.cpp delete mode 100644 launcher/minecraft/auth-msa/mainwindow.h delete mode 100644 launcher/minecraft/auth-msa/mainwindow.ui create mode 100644 launcher/minecraft/auth/AccountData.cpp create mode 100644 launcher/minecraft/auth/AccountData.h rename launcher/minecraft/auth/{MojangAccountList.cpp => AccountList.cpp} (52%) create mode 100644 launcher/minecraft/auth/AccountList.h create mode 100644 launcher/minecraft/auth/AccountTask.cpp rename launcher/minecraft/auth/{YggdrasilTask.h => AccountTask.h} (52%) create mode 100644 launcher/minecraft/auth/MinecraftAccount.cpp rename launcher/minecraft/auth/{MojangAccount.h => MinecraftAccount.h} (51%) delete mode 100644 launcher/minecraft/auth/MojangAccount.cpp delete mode 100644 launcher/minecraft/auth/MojangAccountList.h rename launcher/minecraft/{auth-msa/context.cpp => auth/flows/AuthContext.cpp} (55%) rename launcher/minecraft/{auth-msa/context.h => auth/flows/AuthContext.h} (52%) delete mode 100644 launcher/minecraft/auth/flows/AuthenticateTask.cpp delete mode 100644 launcher/minecraft/auth/flows/AuthenticateTask.h create mode 100644 launcher/minecraft/auth/flows/MSAHelper.txt create mode 100644 launcher/minecraft/auth/flows/MSAInteractive.cpp create mode 100644 launcher/minecraft/auth/flows/MSAInteractive.h create mode 100644 launcher/minecraft/auth/flows/MSASilent.cpp create mode 100644 launcher/minecraft/auth/flows/MSASilent.h create mode 100644 launcher/minecraft/auth/flows/MojangLogin.cpp create mode 100644 launcher/minecraft/auth/flows/MojangLogin.h create mode 100644 launcher/minecraft/auth/flows/MojangRefresh.cpp create mode 100644 launcher/minecraft/auth/flows/MojangRefresh.h delete mode 100644 launcher/minecraft/auth/flows/RefreshTask.cpp delete mode 100644 launcher/minecraft/auth/flows/RefreshTask.h delete mode 100644 launcher/minecraft/auth/flows/ValidateTask.cpp delete mode 100644 launcher/minecraft/auth/flows/ValidateTask.h rename launcher/minecraft/auth/{YggdrasilTask.cpp => flows/Yggdrasil.cpp} (56%) create mode 100644 launcher/minecraft/auth/flows/Yggdrasil.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 521fdaad..817b4cfc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -90,6 +90,9 @@ set(MultiMC_DISCORD_URL "" CACHE STRING "URL for the Discord guild.") # Subreddit URL 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 include(GetGitRevisionDescription) get_git_head_revision(MultiMC_GIT_REFSPEC MultiMC_GIT_COMMIT) diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in index 60d417a6..9d4771b4 100644 --- a/buildconfig/BuildConfig.cpp.in +++ b/buildconfig/BuildConfig.cpp.in @@ -35,6 +35,7 @@ Config::Config() PASTE_EE_KEY = "@MultiMC_PASTE_EE_API_KEY@"; IMGUR_CLIENT_ID = "@MultiMC_IMGUR_CLIENT_ID@"; META_URL = "@MultiMC_META_URL@"; + MSA_CLIENT_ID = "@MultiMC_MSA_CLIENT_ID@"; BUG_TRACKER_URL = "@MultiMC_BUG_TRACKER_URL@"; DISCORD_URL = "@MultiMC_DISCORD_URL@"; diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index 185bebad..71880109 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -75,13 +75,17 @@ public: */ 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 DISCORD_URL; QString SUBREDDIT_URL; QString RESOURCE_BASE = "https://resources.download.minecraft.net/"; QString LIBRARY_BASE = "https://libraries.minecraft.net/"; - QString SKINS_BASE = "https://crafatar.com/skins/"; QString AUTH_BASE = "https://authserver.mojang.com/"; QString MOJANG_STATUS_URL = "https://status.mojang.com/check"; QString IMGUR_BASE_URL = "https://api.imgur.com/3/"; diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h index 833646c0..8c08dc05 100644 --- a/launcher/BaseInstance.h +++ b/launcher/BaseInstance.h @@ -26,7 +26,7 @@ #include "settings/INIFile.h" #include "BaseVersionList.h" -#include "minecraft/auth/MojangAccount.h" +#include "minecraft/auth/MinecraftAccount.h" #include "MessageLevel.h" #include "pathmatcher/IPathMatcher.h" diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 37f5d3a1..3c140ede 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -203,20 +203,31 @@ set(STATUS_SOURCES # Support for Minecraft instances and launch set(MINECRAFT_SOURCES # 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.cpp - minecraft/auth/MojangAccountList.h - minecraft/auth/MojangAccountList.cpp - minecraft/auth/MojangAccount.h - minecraft/auth/MojangAccount.cpp - minecraft/auth/YggdrasilTask.h - minecraft/auth/YggdrasilTask.cpp - minecraft/auth/flows/AuthenticateTask.h - minecraft/auth/flows/AuthenticateTask.cpp - minecraft/auth/flows/RefreshTask.cpp - minecraft/auth/flows/RefreshTask.cpp - minecraft/auth/flows/ValidateTask.h - minecraft/auth/flows/ValidateTask.cpp + minecraft/auth/AccountList.h + minecraft/auth/AccountList.cpp + minecraft/auth/MinecraftAccount.h + minecraft/auth/MinecraftAccount.cpp + minecraft/auth/flows/AuthContext.h + minecraft/auth/flows/AuthContext.cpp + + minecraft/auth/flows/MSAInteractive.h + minecraft/auth/flows/MSAInteractive.cpp + minecraft/auth/flows/MSASilent.h + minecraft/auth/flows/MSASilent.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.cpp @@ -732,6 +743,8 @@ SET(MULTIMC_SOURCES dialogs/IconPickerDialog.h dialogs/LoginDialog.cpp dialogs/LoginDialog.h + dialogs/MSALoginDialog.cpp + dialogs/MSALoginDialog.h dialogs/NewComponentDialog.cpp dialogs/NewComponentDialog.h dialogs/NewInstanceDialog.cpp @@ -850,6 +863,7 @@ SET(MULTIMC_UIS dialogs/EditAccountDialog.ui dialogs/ExportInstanceDialog.ui dialogs/LoginDialog.ui + dialogs/MSALoginDialog.ui dialogs/UpdateDialog.ui dialogs/NotificationDialog.ui dialogs/SkinUploadDialog.ui @@ -892,6 +906,7 @@ target_link_libraries(MultiMC_logic optional-bare tomlc99 BuildConfig + Katabasis ) target_link_libraries(MultiMC_logic Qt5::Core diff --git a/launcher/Env.cpp b/launcher/Env.cpp index 71b49d95..abf9f58c 100644 --- a/launcher/Env.cpp +++ b/launcher/Env.cpp @@ -101,7 +101,6 @@ void Env::initHttpMetaCache() m_metacache->addBase("ModpacksCHPacks", QDir("cache/ModpacksCHPacks").absolutePath()); m_metacache->addBase("TechnicPacks", QDir("cache/TechnicPacks").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("translations", QDir("translations").absolutePath()); m_metacache->addBase("icons", QDir("cache/icons").absolutePath()); diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index ee764082..11780625 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -1,6 +1,6 @@ #include "LaunchController.h" #include "MainWindow.h" -#include +#include #include "MultiMC.h" #include "dialogs/CustomMessageBox.h" #include "dialogs/ProfileSelectDialog.h" @@ -12,7 +12,7 @@ #include #include #include -#include +#include #include #include #include @@ -35,22 +35,23 @@ void LaunchController::executeTask() } // FIXME: minecraft specific -void LaunchController::login() -{ +void LaunchController::login() { JavaCommon::checkJVMArgs(m_instance->settings()->get("JvmArgs").toString(), m_parentWidget); // Find an account to use. - std::shared_ptr accounts = MMC->accounts(); - MojangAccountPtr account = accounts->activeAccount(); + std::shared_ptr accounts = MMC->accounts(); if (accounts->count() <= 0) { // Tell the user they need to log in at least one account in order to play. 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 " "account logged in to MultiMC." "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) { @@ -58,11 +59,16 @@ void LaunchController::login() 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. - ProfileSelectDialog selectDialog(tr("Which profile would you like to use?"), - ProfileSelectDialog::GlobalDefaultCheckbox, m_parentWidget); + ProfileSelectDialog selectDialog( + tr("Which account would you like to use?"), + ProfileSelectDialog::GlobalDefaultCheckbox, + m_parentWidget + ); selectDialog.exec(); @@ -70,8 +76,9 @@ void LaunchController::login() account = selectDialog.selectedAccount(); // If the user said to use the account as default, do that. - if (selectDialog.useAsGlobalDefault() && account.get() != nullptr) - accounts->setActiveAccount(account->username()); + if (selectDialog.useAsGlobalDefault() && account.get() != nullptr) { + accounts->setActiveAccount(account->profileId()); + } } // if no account is selected, we bail @@ -93,7 +100,13 @@ void LaunchController::login() { m_session = std::make_shared(); m_session->wants_online = m_online; - auto task = account->login(m_session, password); + std::shared_ptr task; + if(!password.isNull()) { + task = account->login(m_session, password); + } + else { + task = account->refresh(m_session); + } if (task) { // We'll need to validate the access token to make sure the account @@ -107,9 +120,9 @@ void LaunchController::login() if (!task->wasSuccessful()) { auto failReasonNew = task->failReason(); - if(failReasonNew == "Invalid token.") + if(failReasonNew == "Invalid token." || failReasonNew == "Invalid Signature") { - account->invalidateClientToken(); + // account->invalidateClientToken(); failReason = needLoginAgain; } else failReason = failReasonNew; @@ -117,72 +130,82 @@ void LaunchController::login() } switch (m_session->status) { - case AuthSession::Undetermined: - { - qCritical() << "Received undetermined session status during login. Bye."; - tryagain = false; - emitFailed(tr("Received undetermined session status during login.")); - break; - } - case AuthSession::RequiresPassword: - { - EditAccountDialog passDialog(failReason, m_parentWidget, EditAccountDialog::PasswordField); - auto username = m_session->username; - auto chopN = [](QString toChop, int N) -> QString - { - if(toChop.size() > N) + case AuthSession::Undetermined: { + qCritical() << "Received undetermined session status during login. Bye."; + tryagain = false; + emitFailed(tr("Received undetermined session status during login.")); + return; + } + case AuthSession::RequiresPassword: { + // FIXME: this needs to understand MSA + EditAccountDialog passDialog(failReason, m_parentWidget, EditAccountDialog::PasswordField); + auto username = m_session->username; + auto chopN = [](QString toChop, int N) -> QString { - auto left = toChop.left(N); - left += QString("\u25CF").repeated(toChop.size() - N); - return left; - } - return toChop; - }; + if(toChop.size() > N) + { + auto left = toChop.left(N); + left += QString("\u25CF").repeated(toChop.size() - N); + return left; + } + return toChop; + }; - if(username.contains('@')) - { - auto parts = username.split('@'); - auto mailbox = chopN(parts[0],3); - QString domain = chopN(parts[1], 3); - username = mailbox + '@' + domain; - } - passDialog.setUsername(username); - if (passDialog.exec() == QDialog::Accepted) - { - password = passDialog.password(); - } - else - { - tryagain = false; - } - break; - } - case AuthSession::PlayableOffline: - { - // we ask the user for a player name - bool ok = false; - QString usedname = m_session->player_name; - QString name = QInputDialog::getText(m_parentWidget, tr("Player name"), - tr("Choose your offline mode player name."), - QLineEdit::Normal, m_session->player_name, &ok); - if (!ok) - { - tryagain = false; + if(username.contains('@')) + { + auto parts = username.split('@'); + auto mailbox = chopN(parts[0],3); + QString domain = chopN(parts[1], 3); + username = mailbox + '@' + domain; + } + passDialog.setUsername(username); + if (passDialog.exec() == QDialog::Accepted) + { + password = passDialog.password(); + } + else + { + tryagain = false; + emitFailed(tr("Received undetermined session status during login.")); + } break; } - if (name.length()) - { - usedname = name; + 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 + bool ok = false; + QString usedname = m_session->player_name; + QString name = QInputDialog::getText( + m_parentWidget, + tr("Player name"), + tr("Choose your offline mode player name."), + QLineEdit::Normal, + m_session->player_name, + &ok + ); + if (!ok) + { + tryagain = false; + break; + } + if (name.length()) + { + usedname = name; + } + m_session->MakeOffline(usedname); + // offline flavored game from here :3 + } + case AuthSession::PlayableOnline: + { + launchInstance(); + tryagain = false; + return; } - m_session->MakeOffline(usedname); - // offline flavored game from here :3 - } - case AuthSession::PlayableOnline: - { - launchInstance(); - tryagain = false; - return; - } } } emitFailed(tr("Failed to launch.")); diff --git a/launcher/MainWindow.cpp b/launcher/MainWindow.cpp index 9225193e..182b22e9 100644 --- a/launcher/MainWindow.cpp +++ b/launcher/MainWindow.cpp @@ -54,7 +54,7 @@ #include #include #include -#include +#include #include #include #include @@ -90,6 +90,20 @@ #include "KonamiCode.h" #include +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 template class Translated @@ -753,49 +767,27 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow // 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. // Template hell sucks... - connect(MMC->accounts().get(), &MojangAccountList::activeAccountChanged, [this] - { - activeAccountChanged(); - }); - connect(MMC->accounts().get(), &MojangAccountList::listChanged, [this] - { - repopulateAccountsMenu(); - }); + connect( + MMC->accounts().get(), + &AccountList::activeAccountChanged, + [this] { + activeAccountChanged(); + } + ); + connect( + MMC->accounts().get(), + &AccountList::listChanged, + [this] + { + repopulateAccountsMenu(); + } + ); // Show initial account activeAccountChanged(); - auto accounts = MMC->accounts(); - - QList 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(); - } + // TODO: refresh accounts here? + // auto accounts = MMC->accounts(); // load the news { @@ -844,7 +836,15 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow void MainWindow::retranslateUi() { - accountMenuButton->setText(tr("Profiles")); + std::shared_ptr 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")); + } if (m_selectedInstance) { m_statusLeft->setText(m_selectedInstance->getStatusbarDescription()); @@ -872,12 +872,6 @@ void MainWindow::konamiTriggered() qDebug() << "Super Secret Mode ACTIVATED!"; } -void MainWindow::skinJobFinished() -{ - activeAccountChanged(); - skin_download_job.reset(); -} - void MainWindow::showInstanceContextMenu(const QPoint &pos) { QList actions; @@ -1018,34 +1012,21 @@ void MainWindow::updateToolsMenu() 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() { accountMenu->clear(); - std::shared_ptr accounts = MMC->accounts(); - MojangAccountPtr active_account = accounts->activeAccount(); + std::shared_ptr accounts = MMC->accounts(); + MinecraftAccountPtr active_account = accounts->activeAccount(); - QString active_username = ""; + QString active_profileId = ""; if (active_account != nullptr) { - active_username = active_account->username(); - const AccountProfile *profile = active_account->currentProfile(); + active_profileId = active_account->profileId(); // 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); } } @@ -1061,22 +1042,19 @@ void MainWindow::repopulateAccountsMenu() // TODO: Nicer way to iterate? for (int i = 0; i < accounts->count(); i++) { - MojangAccountPtr account = accounts->at(i); - for (auto profile : account->profiles()) + MinecraftAccountPtr account = accounts->at(i); + auto profileLabel = profileInUseFilter(account->profileName(), account->isInUse()); + QAction *action = new QAction(profileLabel, this); + action->setData(account->profileId()); + action->setCheckable(true); + if (active_profileId == account->profileId()) { - auto profileLabel = profileInUseFilter(profile.name, account->isInUse()); - QAction *action = new QAction(profileLabel, this); - action->setData(account->username()); - action->setCheckable(true); - if (active_username == account->username()) - { - action->setChecked(true); - } - - action->setIcon(SkinUtils::getFaceFromCache(profile.id)); - accountMenu->addAction(action); - connect(action, SIGNAL(triggered(bool)), SLOT(changeActiveAccount())); + action->setChecked(true); } + + action->setIcon(account->getFace()); + accountMenu->addAction(action); + connect(action, SIGNAL(triggered(bool)), SLOT(changeActiveAccount())); } } @@ -1086,8 +1064,7 @@ void MainWindow::repopulateAccountsMenu() action->setCheckable(true); action->setIcon(MMC->getThemedIcon("noaccount")); action->setData(""); - if (active_username.isEmpty()) - { + if (active_profileId.isEmpty()) { action->setChecked(true); } @@ -1134,18 +1111,15 @@ void MainWindow::activeAccountChanged() { 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(); - if (profile != nullptr) - { - auto profileLabel = profileInUseFilter(profile->name, account->isInUse()); - accountMenuButton->setIcon(SkinUtils::getFaceFromCache(profile->id)); - accountMenuButton->setText(profileLabel); - return; - } + auto profileLabel = profileInUseFilter(account->profileName(), account->isInUse()); + accountMenuButton->setText(profileLabel); + accountMenuButton->setIcon(account->getFace()); + return; } // Set the icon to the "no account" icon. diff --git a/launcher/MainWindow.h b/launcher/MainWindow.h index c992ab94..67dec8cf 100644 --- a/launcher/MainWindow.h +++ b/launcher/MainWindow.h @@ -22,7 +22,7 @@ #include #include "BaseInstance.h" -#include "minecraft/auth/MojangAccount.h" +#include "minecraft/auth/MinecraftAccount.h" #include "net/NetJob.h" #include "updater/GoUpdate.h" @@ -149,8 +149,6 @@ private slots: void updateToolsMenu(); - void skinJobFinished(); - void instanceActivated(QModelIndex); void instanceChanged(const QModelIndex ¤t, const QModelIndex &previous); @@ -214,7 +212,6 @@ private: QToolButton *accountMenuButton = nullptr; KonamiCode * secretEventFilter = nullptr; - unique_qobject_ptr skin_download_job; unique_qobject_ptr m_newsChecker; unique_qobject_ptr m_notificationChecker; diff --git a/launcher/MultiMC.cpp b/launcher/MultiMC.cpp index 932c7a76..5961a45d 100644 --- a/launcher/MultiMC.cpp +++ b/launcher/MultiMC.cpp @@ -42,7 +42,7 @@ #include "dialogs/CustomMessageBox.h" #include "InstanceList.h" -#include +#include #include "icons/IconList.h" #include "net/HttpMetaCache.h" #include "Env.h" @@ -745,7 +745,7 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv) // and accounts { - m_accounts.reset(new MojangAccountList(this)); + m_accounts.reset(new AccountList(this)); qDebug() << "Loading accounts..."; m_accounts->setListFilePath("accounts.json", true); m_accounts->loadList(); diff --git a/launcher/MultiMC.h b/launcher/MultiMC.h index af2b41c1..59fd7345 100644 --- a/launcher/MultiMC.h +++ b/launcher/MultiMC.h @@ -24,7 +24,7 @@ class QFile; class HttpMetaCache; class SettingsObject; class InstanceList; -class MojangAccountList; +class AccountList; class IconList; class QNetworkAccessManager; class JavaInstallList; @@ -111,7 +111,7 @@ public: return m_mcedit.get(); } - std::shared_ptr accounts() const + std::shared_ptr accounts() const { return m_accounts; } @@ -188,7 +188,7 @@ private: FolderInstanceProvider * m_instanceFolder = nullptr; std::shared_ptr m_icons; std::shared_ptr m_updateChecker; - std::shared_ptr m_accounts; + std::shared_ptr m_accounts; std::shared_ptr m_javalist; std::shared_ptr m_translations; std::shared_ptr m_globalSettingsProvider; diff --git a/launcher/SkinUtils.cpp b/launcher/SkinUtils.cpp index ec969889..a196173e 100644 --- a/launcher/SkinUtils.cpp +++ b/launcher/SkinUtils.cpp @@ -30,9 +30,7 @@ namespace SkinUtils */ QPixmap getFaceFromCache(QString username, int height, int width) { - QFile fskin(ENV.metacache() - ->resolveEntry("skins", username + ".png") - ->getFullPath()); + QFile fskin(ENV.metacache()->resolveEntry("skins", username + ".png")->getFullPath()); if (fskin.exists()) { diff --git a/launcher/dialogs/LoginDialog.cpp b/launcher/dialogs/LoginDialog.cpp index 32f8a48f..1dee9920 100644 --- a/launcher/dialogs/LoginDialog.cpp +++ b/launcher/dialogs/LoginDialog.cpp @@ -16,7 +16,7 @@ #include "LoginDialog.h" #include "ui_LoginDialog.h" -#include "minecraft/auth/YggdrasilTask.h" +#include "minecraft/auth/AccountTask.h" #include @@ -42,11 +42,10 @@ void LoginDialog::accept() ui->progressBar->setVisible(true); // 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()); connect(m_loginTask.get(), &Task::failed, this, &LoginDialog::onTaskFailed); - connect(m_loginTask.get(), &Task::succeeded, this, - &LoginDialog::onTaskSucceeded); + connect(m_loginTask.get(), &Task::succeeded, this, &LoginDialog::onTaskSucceeded); connect(m_loginTask.get(), &Task::status, this, &LoginDialog::onTaskStatus); connect(m_loginTask.get(), &Task::progress, this, &LoginDialog::onTaskProgress); m_loginTask->start(); @@ -98,7 +97,7 @@ void LoginDialog::onTaskProgress(qint64 current, qint64 total) } // Public interface -MojangAccountPtr LoginDialog::newAccount(QWidget *parent, QString msg) +MinecraftAccountPtr LoginDialog::newAccount(QWidget *parent, QString msg) { LoginDialog dlg(parent); dlg.ui->label->setText(msg); diff --git a/launcher/dialogs/LoginDialog.h b/launcher/dialogs/LoginDialog.h index 16bdddfb..13463640 100644 --- a/launcher/dialogs/LoginDialog.h +++ b/launcher/dialogs/LoginDialog.h @@ -18,7 +18,7 @@ #include #include -#include "minecraft/auth/MojangAccount.h" +#include "minecraft/auth/MinecraftAccount.h" namespace Ui { @@ -32,7 +32,7 @@ class LoginDialog : public QDialog public: ~LoginDialog(); - static MojangAccountPtr newAccount(QWidget *parent, QString message); + static MinecraftAccountPtr newAccount(QWidget *parent, QString message); private: explicit LoginDialog(QWidget *parent = 0); @@ -53,6 +53,6 @@ slots: private: Ui::LoginDialog *ui; - MojangAccountPtr m_account; + MinecraftAccountPtr m_account; std::shared_ptr m_loginTask; }; diff --git a/launcher/dialogs/LoginDialog.ui b/launcher/dialogs/LoginDialog.ui index dbdb3b93..8fa4a45d 100644 --- a/launcher/dialogs/LoginDialog.ui +++ b/launcher/dialogs/LoginDialog.ui @@ -7,7 +7,7 @@ 0 0 421 - 238 + 198 @@ -20,16 +20,6 @@ Add Account - - - - NOTICE: MultiMC does not currently support Microsoft accounts. This means that accounts created from December 2020 onwards cannot be used. - - - true - - - diff --git a/launcher/dialogs/MSALoginDialog.cpp b/launcher/dialogs/MSALoginDialog.cpp new file mode 100644 index 00000000..778b379d --- /dev/null +++ b/launcher/dialogs/MSALoginDialog.cpp @@ -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 + +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("" + reason + ""); + + // 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; +} diff --git a/launcher/dialogs/MSALoginDialog.h b/launcher/dialogs/MSALoginDialog.h new file mode 100644 index 00000000..402180ee --- /dev/null +++ b/launcher/dialogs/MSALoginDialog.h @@ -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 +#include + +#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 m_loginTask; +}; + diff --git a/launcher/dialogs/MSALoginDialog.ui b/launcher/dialogs/MSALoginDialog.ui new file mode 100644 index 00000000..4ae8085a --- /dev/null +++ b/launcher/dialogs/MSALoginDialog.ui @@ -0,0 +1,60 @@ + + + MSALoginDialog + + + + 0 + 0 + 421 + 114 + + + + + 0 + 0 + + + + Add Microsoft Account + + + + + + Message label placeholder. + + + Qt::RichText + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + 24 + + + false + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel + + + + + + + + diff --git a/launcher/dialogs/ProfileSelectDialog.cpp b/launcher/dialogs/ProfileSelectDialog.cpp index ae34709f..e2ad73e4 100644 --- a/launcher/dialogs/ProfileSelectDialog.cpp +++ b/launcher/dialogs/ProfileSelectDialog.cpp @@ -33,9 +33,10 @@ ProfileSelectDialog::ProfileSelectDialog(const QString &message, int flags, QWid m_accounts = MMC->accounts(); auto view = ui->listView; //view->setModel(m_accounts.get()); - //view->hideColumn(MojangAccountList::ActiveColumn); + //view->hideColumn(AccountList::ActiveColumn); view->setColumnCount(1); view->setRootIsDecorated(false); + // FIXME: use a real model, not this if(QTreeWidgetItem* header = view->headerItem()) { header->setText(0, tr("Name")); @@ -47,20 +48,19 @@ ProfileSelectDialog::ProfileSelectDialog(const QString &message, int flags, QWid QList items; for (int i = 0; i < m_accounts->count(); i++) { - MojangAccountPtr account = m_accounts->at(i); - for (auto profile : account->profiles()) - { - auto profileLabel = profile.name; - if(account->isInUse()) - { - profileLabel += tr(" (in use)"); - } - auto item = new QTreeWidgetItem(view); - item->setText(0, profileLabel); - item->setIcon(0, SkinUtils::getFaceFromCache(profile.id)); - item->setData(0, MojangAccountList::PointerRole, QVariant::fromValue(account)); - items.append(item); + MinecraftAccountPtr account = m_accounts->at(i); + QString profileLabel; + if(account->isInUse()) { + profileLabel = tr("%1 (in use)").arg(account->profileName()); } + else { + profileLabel = account->profileName(); + } + auto item = new QTreeWidgetItem(view); + item->setText(0, profileLabel); + item->setIcon(0, account->getFace()); + item->setData(0, AccountList::PointerRole, QVariant::fromValue(account)); + items.append(item); } view->addTopLevelItems(items); @@ -84,7 +84,7 @@ ProfileSelectDialog::~ProfileSelectDialog() delete ui; } -MojangAccountPtr ProfileSelectDialog::selectedAccount() const +MinecraftAccountPtr ProfileSelectDialog::selectedAccount() const { return m_selected; } @@ -105,7 +105,7 @@ void ProfileSelectDialog::on_buttonBox_accepted() if (selection.size() > 0) { QModelIndex selected = selection.first(); - m_selected = selected.data(MojangAccountList::PointerRole).value(); + m_selected = selected.data(AccountList::PointerRole).value(); } close(); } diff --git a/launcher/dialogs/ProfileSelectDialog.h b/launcher/dialogs/ProfileSelectDialog.h index 9f95830c..a4acd9a1 100644 --- a/launcher/dialogs/ProfileSelectDialog.h +++ b/launcher/dialogs/ProfileSelectDialog.h @@ -19,7 +19,7 @@ #include -#include "minecraft/auth/MojangAccountList.h" +#include "minecraft/auth/AccountList.h" namespace Ui { @@ -59,7 +59,7 @@ public: * Gets a pointer to the account that the user selected. * 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. @@ -80,10 +80,10 @@ slots: void on_buttonBox_rejected(); protected: - std::shared_ptr m_accounts; + std::shared_ptr m_accounts; //! The account that was selected when the user clicked OK. - MojangAccountPtr m_selected; + MinecraftAccountPtr m_selected; private: Ui::ProfileSelectDialog *ui; diff --git a/launcher/dialogs/SkinUploadDialog.cpp b/launcher/dialogs/SkinUploadDialog.cpp index 56133529..3c62edac 100644 --- a/launcher/dialogs/SkinUploadDialog.cpp +++ b/launcher/dialogs/SkinUploadDialog.cpp @@ -107,7 +107,7 @@ void SkinUploadDialog::on_skinBrowseBtn_clicked() 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) { ui->setupUi(this); diff --git a/launcher/dialogs/SkinUploadDialog.h b/launcher/dialogs/SkinUploadDialog.h index deb44eac..84d17dc6 100644 --- a/launcher/dialogs/SkinUploadDialog.h +++ b/launcher/dialogs/SkinUploadDialog.h @@ -1,7 +1,7 @@ #pragma once #include -#include +#include namespace Ui { @@ -11,7 +11,7 @@ namespace Ui class SkinUploadDialog : public QDialog { Q_OBJECT public: - explicit SkinUploadDialog(MojangAccountPtr acct, QWidget *parent = 0); + explicit SkinUploadDialog(MinecraftAccountPtr acct, QWidget *parent = 0); virtual ~SkinUploadDialog() {}; public slots: @@ -22,7 +22,7 @@ public slots: void on_skinBrowseBtn_clicked(); protected: - MojangAccountPtr m_acct; + MinecraftAccountPtr m_acct; private: Ui::SkinUploadDialog *ui; diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index dbf9f816..5f3c7244 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -423,7 +423,7 @@ QStringList MinecraftInstance::processMinecraftArgs( // yggdrasil! if(session) { - token_mapping["auth_username"] = session->username; + // token_mapping["auth_username"] = session->username; token_mapping["auth_session"] = session->session; token_mapping["auth_access_token"] = session->access_token; token_mapping["auth_player_name"] = session->player_name; @@ -691,19 +691,11 @@ QMap MinecraftInstance::createCensorFilterFromSession(AuthSess addToFilter(sessionRef.session, tr("")); } addToFilter(sessionRef.access_token, tr("")); - addToFilter(sessionRef.client_token, tr("")); + if(sessionRef.client_token.size()) { + addToFilter(sessionRef.client_token, tr("")); + } addToFilter(sessionRef.uuid, tr("")); - 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; } diff --git a/launcher/minecraft/auth-msa/BuildConfig.cpp.in b/launcher/minecraft/auth-msa/BuildConfig.cpp.in deleted file mode 100644 index 8f470e25..00000000 --- a/launcher/minecraft/auth-msa/BuildConfig.cpp.in +++ /dev/null @@ -1,9 +0,0 @@ -#include "BuildConfig.h" -#include - -const Config BuildConfig; - -Config::Config() -{ - CLIENT_ID = "@MOJANGDEMO_CLIENT_ID@"; -} diff --git a/launcher/minecraft/auth-msa/BuildConfig.h b/launcher/minecraft/auth-msa/BuildConfig.h deleted file mode 100644 index 7a01d704..00000000 --- a/launcher/minecraft/auth-msa/BuildConfig.h +++ /dev/null @@ -1,11 +0,0 @@ -#pragma once -#include - -class Config -{ -public: - Config(); - QString CLIENT_ID; -}; - -extern const Config BuildConfig; diff --git a/launcher/minecraft/auth-msa/CMakeLists.txt b/launcher/minecraft/auth-msa/CMakeLists.txt deleted file mode 100644 index 22777d1b..00000000 --- a/launcher/minecraft/auth-msa/CMakeLists.txt +++ /dev/null @@ -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) diff --git a/launcher/minecraft/auth-msa/main.cpp b/launcher/minecraft/auth-msa/main.cpp deleted file mode 100644 index 481e0126..00000000 --- a/launcher/minecraft/auth-msa/main.cpp +++ /dev/null @@ -1,100 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#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" diff --git a/launcher/minecraft/auth-msa/mainwindow.cpp b/launcher/minecraft/auth-msa/mainwindow.cpp deleted file mode 100644 index d4e18dc0..00000000 --- a/launcher/minecraft/auth-msa/mainwindow.cpp +++ /dev/null @@ -1,97 +0,0 @@ -#include "mainwindow.h" -#include "ui_mainwindow.h" -#include - -#include - -#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(); -} diff --git a/launcher/minecraft/auth-msa/mainwindow.h b/launcher/minecraft/auth-msa/mainwindow.h deleted file mode 100644 index abde52d8..00000000 --- a/launcher/minecraft/auth-msa/mainwindow.h +++ /dev/null @@ -1,34 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#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 m_ui; -}; - diff --git a/launcher/minecraft/auth-msa/mainwindow.ui b/launcher/minecraft/auth-msa/mainwindow.ui deleted file mode 100644 index 32b34128..00000000 --- a/launcher/minecraft/auth-msa/mainwindow.ui +++ /dev/null @@ -1,72 +0,0 @@ - - - MainWindow - - - - 0 - 0 - 1037 - 511 - - - - SmartMapsClient - - - true - - - - - - - SignIn Mojang - - - - - - - Qt::Horizontal - - - - - - - - - - Refresh - - - - - - - SignIn MSA - - - - - - - SignOut - - - - - - - Make Active - - - - - - - - - - diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp new file mode 100644 index 00000000..77c73c1b --- /dev/null +++ b/launcher/minecraft/auth/AccountData.cpp @@ -0,0 +1,387 @@ +#include "AccountData.h" +#include +#include +#include +#include +#include + +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 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"; + } + } +} diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h new file mode 100644 index 00000000..b2d09cb0 --- /dev/null +++ b/launcher/minecraft/auth/AccountData.h @@ -0,0 +1,73 @@ +#pragma once +#include +#include +#include +#include +#include + +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 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; +}; diff --git a/launcher/minecraft/auth/MojangAccountList.cpp b/launcher/minecraft/auth/AccountList.cpp similarity index 52% rename from launcher/minecraft/auth/MojangAccountList.cpp rename to launcher/minecraft/auth/AccountList.cpp index e584cb3b..59028b60 100644 --- a/launcher/minecraft/auth/MojangAccountList.cpp +++ b/launcher/minecraft/auth/AccountList.cpp @@ -13,8 +13,8 @@ * limitations under the License. */ -#include "MojangAccountList.h" -#include "MojangAccount.h" +#include "AccountList.h" +#include "AccountData.h" #include #include @@ -28,31 +28,49 @@ #include #include +#include -#define ACCOUNT_LIST_FORMAT_VERSION 2 +enum AccountListVersion { + MojangOnly = 2, + MojangMSA = 3 +}; -MojangAccountList::MojangAccountList(QObject *parent) : QAbstractListModel(parent) -{ -} +AccountList::AccountList(QObject *parent) : QAbstractListModel(parent) { } -MojangAccountPtr MojangAccountList::findAccount(const QString &username) const -{ - for (int i = 0; i < count(); i++) - { - MojangAccountPtr account = at(i); - if (account->username() == username) - return account; +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 nullptr; + return -1; } -const MojangAccountPtr MojangAccountList::at(int i) const +const MinecraftAccountPtr AccountList::at(int i) const { - return MojangAccountPtr(m_accounts.at(i)); + return MinecraftAccountPtr(m_accounts.at(i)); } -void MojangAccountList::addAccount(const MojangAccountPtr account) +void AccountList::addAccount(const MinecraftAccountPtr account) { + // 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; + } + + // 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(); beginInsertRows(QModelIndex(), row, row); connect(account.get(), SIGNAL(changed()), SLOT(accountChanged())); @@ -61,24 +79,7 @@ void MojangAccountList::addAccount(const MojangAccountPtr account) onListChanged(); } -void MojangAccountList::removeAccount(const QString &username) -{ - 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) +void AccountList::removeAccount(QModelIndex index) { int row = index.row(); 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; } -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; auto prevActiveAcc = m_activeAccount; m_activeAccount = nullptr; - for (MojangAccountPtr account : m_accounts) + for (MinecraftAccountPtr account : m_accounts) { if (account == prevActiveAcc) { @@ -125,9 +126,9 @@ void MojangAccountList::setActiveAccount(const QString &username) auto newActiveAccount = m_activeAccount; int newActiveAccountIdx = -1; int idx = 0; - for (MojangAccountPtr account : m_accounts) + for (MinecraftAccountPtr account : m_accounts) { - if (account->username() == username) + if (account->profileId() == profileId) { newActiveAccount = account; 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. onListChanged(); } -void MojangAccountList::onListChanged() +void AccountList::onListChanged() { if (m_autosave) // TODO: Alert the user if this fails. @@ -163,7 +164,7 @@ void MojangAccountList::onListChanged() emit listChanged(); } -void MojangAccountList::onActiveChanged() +void AccountList::onActiveChanged() { if (m_autosave) saveList(); @@ -171,12 +172,12 @@ void MojangAccountList::onActiveChanged() emit activeAccountChanged(); } -int MojangAccountList::count() const +int AccountList::count() const { 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()) return QVariant(); @@ -184,51 +185,61 @@ QVariant MojangAccountList::data(const QModelIndex &index, int role) const if (index.row() > count()) return QVariant(); - MojangAccountPtr account = at(index.row()); + MinecraftAccountPtr account = at(index.row()); switch (role) { - case Qt::DisplayRole: - switch (index.column()) - { - case NameColumn: - return account->username(); + case Qt::DisplayRole: + switch (index.column()) + { + case NameColumn: + return account->accountDisplayString(); + + case TypeColumn: { + auto typeStr = account->typeString(); + typeStr[0] = typeStr[0].toUpper(); + return typeStr; + } + + case ProfileNameColumn: { + return account->profileName(); + } + + default: + return QVariant(); + } + + case Qt::ToolTipRole: + return account->accountDisplayString(); + + case PointerRole: + return qVariantFromValue(account); + + case Qt::CheckStateRole: + switch (index.column()) + { + case NameColumn: + return account == m_activeAccount ? Qt::Checked : Qt::Unchecked; + } default: return QVariant(); - } - - case Qt::ToolTipRole: - return account->username(); - - case PointerRole: - return qVariantFromValue(account); - - case Qt::CheckStateRole: - switch (index.column()) - { - case ActiveColumn: - return account == m_activeAccount ? Qt::Checked : Qt::Unchecked; - } - - default: - return QVariant(); } } -QVariant MojangAccountList::headerData(int section, Qt::Orientation orientation, int role) const +QVariant AccountList::headerData(int section, Qt::Orientation orientation, int role) const { switch (role) { case Qt::DisplayRole: switch (section) { - case ActiveColumn: - return tr("Active?"); - case NameColumn: - return tr("Name"); - + return tr("Account"); + case TypeColumn: + return tr("Type"); + case ProfileNameColumn: + return tr("Profile"); default: return QVariant(); } @@ -237,8 +248,11 @@ QVariant MojangAccountList::headerData(int section, Qt::Orientation orientation, switch (section) { 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: 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(); } -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()) { @@ -269,7 +283,7 @@ Qt::ItemFlags MojangAccountList::flags(const QModelIndex &index) const 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()) { @@ -280,8 +294,8 @@ bool MojangAccountList::setData(const QModelIndex &index, const QVariant &value, { if(value == Qt::Checked) { - MojangAccountPtr account = this->at(index.row()); - this->setActiveAccount(account->username()); + MinecraftAccountPtr account = at(index.row()); + setActiveAccount(account->profileId()); } } @@ -289,31 +303,21 @@ bool MojangAccountList::setData(const QModelIndex &index, const QVariant &value, return true; } -void MojangAccountList::updateListData(QList versions) +bool AccountList::loadList() { - beginResetModel(); - m_accounts = versions; - endResetModel(); -} - -bool MojangAccountList::loadList(const QString &filePath) -{ - QString path = filePath; - if (path.isEmpty()) - path = m_listFilePath; - if (path.isEmpty()) + if (m_listFilePath.isEmpty()) { qCritical() << "Can't load Mojang account list. No file path given and no default set."; return false; } - QFile file(path); + QFile file(m_listFilePath); // Try to open the file and fail if we can't. // TODO: We should probably report this error to the user. 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; } @@ -343,121 +347,168 @@ bool MojangAccountList::loadList(const QString &filePath) QJsonObject root = jsonDoc.object(); // Make sure the format version matches. - if (root.value("formatVersion").toVariant().toInt() != ACCOUNT_LIST_FORMAT_VERSION) - { - QString newName = "accounts-old.json"; - qWarning() << "Format version mismatch when loading account list. Existing one will be renamed to" - << newName; - - // Attempt to rename the old version. - file.rename(newName); - return false; + 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"; + qWarning() << "Unknown format version when loading account list. Existing one will be renamed to" << newName; + // Attempt to rename the old version. + file.rename(newName); + return false; + } } +} - // Now, load the accounts array. +bool AccountList::loadV2(QJsonObject& root) { beginResetModel(); + auto activeUserName = root.value("activeAccount").toString(""); QJsonArray accounts = root.value("accounts").toArray(); for (QJsonValue accountVal : accounts) { QJsonObject accountObj = accountVal.toObject(); - MojangAccountPtr account = MojangAccount::loadFromJson(accountObj); + MinecraftAccountPtr account = MinecraftAccount::loadFromJsonV2(accountObj); 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); + if (activeUserName.size() && account->mojangUserName() == activeUserName) { + m_activeAccount = account; + } } else { qWarning() << "Failed to load an account."; } } - // Load the active account. - m_activeAccount = findAccount(root.value("activeAccount").toString("")); endResetModel(); return true; } -bool MojangAccountList::saveList(const QString &filePath) +bool AccountList::loadV3(QJsonObject& root) { + beginResetModel(); + QJsonArray accounts = root.value("accounts").toArray(); + for (QJsonValue accountVal : accounts) + { + QJsonObject accountObj = accountVal.toObject(); + MinecraftAccountPtr account = MinecraftAccount::loadFromJsonV3(accountObj); + if (account.get() != nullptr) + { + 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() { - QString path(filePath); - if (path.isEmpty()) - path = m_listFilePath; - if (path.isEmpty()) + if (m_listFilePath.isEmpty()) { qCritical() << "Can't save Mojang account list. No file path given and no default set."; return false; } // make sure the parent folder exists - if(!FS::ensureFilePathExists(path)) + if(!FS::ensureFilePathExists(m_listFilePath)) return false; // make sure the file wasn't overwritten with a folder before (fixes a bug) - QFileInfo finfo(path); + QFileInfo finfo(m_listFilePath); if(finfo.isDir()) { - QDir badDir(path); + QDir badDir(m_listFilePath); badDir.removeRecursively(); } - qDebug() << "Writing account list to" << path; + qDebug() << "Writing account list to" << m_listFilePath; qDebug() << "Building JSON data structure."; // Build the JSON document to write to the list file. QJsonObject root; - root.insert("formatVersion", ACCOUNT_LIST_FORMAT_VERSION); + root.insert("formatVersion", AccountListVersion::MojangMSA); // Build a list of accounts. qDebug() << "Building account array."; QJsonArray accounts; - for (MojangAccountPtr account : m_accounts) + for (MinecraftAccountPtr account : m_accounts) { QJsonObject accountObj = account->saveToJson(); + if(m_activeAccount == account) { + accountObj["active"] = true; + } accounts.append(accountObj); } // Insert the account list into the root object. 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. QJsonDocument doc(root); // Now that we're done building the JSON object, we can write it to the 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. // TODO: We should probably report this error to the user. 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; } // Write the JSON to the file. file.write(doc.toJson()); file.setPermissions(QFile::ReadOwner|QFile::WriteOwner|QFile::ReadUser|QFile::WriteUser); - file.close(); - - qDebug() << "Saved account list to" << path; - - return true; + if(file.commit()) { + qDebug() << "Saved account list to" << m_listFilePath; + 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_autosave = autosave; } -bool MojangAccountList::anyAccountIsValid() +bool AccountList::anyAccountIsValid() { for(auto account:m_accounts) { diff --git a/launcher/minecraft/auth/AccountList.h b/launcher/minecraft/auth/AccountList.h new file mode 100644 index 00000000..ac3684ee --- /dev/null +++ b/launcher/minecraft/auth/AccountList.h @@ -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 +#include +#include +#include + +/*! + * 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 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; +}; diff --git a/launcher/minecraft/auth/AccountTask.cpp b/launcher/minecraft/auth/AccountTask.cpp new file mode 100644 index 00000000..c06be42b --- /dev/null +++ b/launcher/minecraft/auth/AccountTask.cpp @@ -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 +#include +#include +#include +#include +#include + +#include + +#include + +#include + +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); + } +} diff --git a/launcher/minecraft/auth/YggdrasilTask.h b/launcher/minecraft/auth/AccountTask.h similarity index 52% rename from launcher/minecraft/auth/YggdrasilTask.h rename to launcher/minecraft/auth/AccountTask.h index 8af2e132..3f08096f 100644 --- a/launcher/minecraft/auth/YggdrasilTask.h +++ b/launcher/minecraft/auth/AccountTask.h @@ -22,19 +22,17 @@ #include #include -#include "MojangAccount.h" +#include "MinecraftAccount.h" class QNetworkReply; -/** - * A Yggdrasil task is a task that performs an operation on a given mojang account. - */ -class YggdrasilTask : public Task +class AccountTask : public Task { + friend class AuthContext; Q_OBJECT public: - explicit YggdrasilTask(MojangAccount * account, QObject *parent = 0); - virtual ~YggdrasilTask() {}; + explicit AccountTask(AccountData * data, QObject *parent = 0); + virtual ~AccountTask() {}; /** * 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 { @@ -75,46 +73,18 @@ public: enum State { STATE_CREATED, - STATE_SENDING_REQUEST, - STATE_PROCESSING_RESPONSE, + STATE_WORKING, 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_SUCCEEDED - } m_state = STATE_CREATED; + } m_accountState = STATE_CREATED; + + State accountState() { + return m_accountState; + } 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. * Used to set the status message for the task. @@ -122,30 +92,12 @@ protected: */ virtual QString getStateMessage() const; -protected -slots: - void processReply(); - void refreshTimers(qint64, qint64); - void heartbeat(); - void sslErrors(QList); - +protected slots: void changeState(State newState, QString reason=QString()); -public -slots: - virtual bool abort() override; - void abortByTimeout(); - State state(); + protected: // FIXME: segfault disaster waiting to happen - MojangAccount *m_account = nullptr; - QNetworkReply *m_netReply = nullptr; + AccountData *m_data = nullptr; std::shared_ptr 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; }; diff --git a/launcher/minecraft/auth/AuthSession.cpp b/launcher/minecraft/auth/AuthSession.cpp index 4e858796..d44f9098 100644 --- a/launcher/minecraft/auth/AuthSession.cpp +++ b/launcher/minecraft/auth/AuthSession.cpp @@ -7,11 +7,13 @@ QString AuthSession::serializeUserProperties() { QJsonObject userAttrs; + /* for (auto key : u.properties.keys()) { auto array = QJsonArray::fromStringList(u.properties.values(key)); userAttrs.insert(key, array); } + */ QJsonDocument value(userAttrs); return value.toJson(QJsonDocument::Compact); diff --git a/launcher/minecraft/auth/AuthSession.h b/launcher/minecraft/auth/AuthSession.h index 29958597..d77435b8 100644 --- a/launcher/minecraft/auth/AuthSession.h +++ b/launcher/minecraft/auth/AuthSession.h @@ -4,13 +4,7 @@ #include #include -class MojangAccount; - -struct User -{ - QString id; - QMultiMap properties; -}; +class MinecraftAccount; struct AuthSession { @@ -21,13 +15,12 @@ struct AuthSession enum Status { Undetermined, + RequiresOAuth, RequiresPassword, PlayableOffline, PlayableOnline } status = Undetermined; - User u; - // client token QString client_token; // account user name @@ -46,7 +39,7 @@ struct AuthSession bool auth_server_online = false; // Did the user request online mode? bool wants_online = true; - std::shared_ptr m_accountPtr; + std::shared_ptr m_accountPtr; }; typedef std::shared_ptr AuthSessionPtr; diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp new file mode 100644 index 00000000..671f9c38 --- /dev/null +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -0,0 +1,303 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Authors: Orochimarufan + * + * 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 +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include +#include + +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 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 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 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."; + } +} diff --git a/launcher/minecraft/auth/MojangAccount.h b/launcher/minecraft/auth/MinecraftAccount.h similarity index 51% rename from launcher/minecraft/auth/MojangAccount.h rename to launcher/minecraft/auth/MinecraftAccount.h index 3f6cbedd..72bb6bd4 100644 --- a/launcher/minecraft/auth/MojangAccount.h +++ b/launcher/minecraft/auth/MinecraftAccount.h @@ -21,17 +21,19 @@ #include #include #include +#include #include #include "AuthSession.h" #include "Usable.h" +#include "AccountData.h" class Task; -class YggdrasilTask; -class MojangAccount; +class AccountTask; +class MinecraftAccount; -typedef std::shared_ptr MojangAccountPtr; -Q_DECLARE_METATYPE(MojangAccountPtr) +typedef std::shared_ptr MinecraftAccountPtr; +Q_DECLARE_METATYPE(MinecraftAccountPtr) /** * 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 * token if the user chose to stay logged in. */ -class MojangAccount : +class MinecraftAccount : public QObject, public Usable, - public std::enable_shared_from_this + public std::enable_shared_from_this { Q_OBJECT public: /* construction */ //! Do not copy accounts. ever. - explicit MojangAccount(const MojangAccount &other, QObject *parent) = delete; + explicit MinecraftAccount(const MinecraftAccount &other, QObject *parent) = delete; //! 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 MojangAccountPtr createFromUsername(const QString &username); + static MinecraftAccountPtr createFromUsername(const QString &username); - //! Loads a MojangAccount from the given JSON object. - static MojangAccountPtr loadFromJson(const QJsonObject &json); + static MinecraftAccountPtr createBlankMSA(); - //! 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; 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. * If the attempt fails because we already are performing some task, it returns false. */ - std::shared_ptr login(AuthSessionPtr session, QString password = QString()); - void invalidateClientToken(); + std::shared_ptr login(AuthSessionPtr session, QString password = QString()); + + std::shared_ptr loginMSA(AuthSessionPtr session); + + std::shared_ptr refresh(AuthSessionPtr session); public: /* queries */ - const QString &username() const - { - return m_username; + QString accountDisplayString() const { + return data.accountDisplayString(); } - const QString &clientToken() const - { - return m_clientToken; + QString mojangUserName() const { + return data.userName(); } - const QString &accessToken() const - { - return m_accessToken; + QString accessToken() const { + return data.accessToken(); } - const QList &profiles() const - { - return m_profiles; + QString profileId() const { + return data.profileId(); } - const User &user() - { - return m_user; + QString profileName() const { + return data.profileName(); } - //! Returns the currently selected profile (if none, returns nullptr) - const AccountProfile *currentProfile() const; + QString typeString() 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 AccountStatus accountStatus() const; + AccountData * accountData() { + return &data; + } + signals: /** * This signal is emitted when the account changes @@ -137,27 +154,10 @@ signals: // TODO: better signalling for the various possible state changes - especially errors protected: /* variables */ - QString m_username; - - // 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 m_profiles; - - // the user structure, whatever it is. - User m_user; + AccountData data; // current task we are executing here - std::shared_ptr m_currentTask; + std::shared_ptr m_currentTask; protected: /* methods */ @@ -171,10 +171,4 @@ slots: private: void fillSession(AuthSessionPtr session); - -public: - friend class YggdrasilTask; - friend class AuthenticateTask; - friend class ValidateTask; - friend class RefreshTask; }; diff --git a/launcher/minecraft/auth/MojangAccount.cpp b/launcher/minecraft/auth/MojangAccount.cpp deleted file mode 100644 index f5853fe3..00000000 --- a/launcher/minecraft/auth/MojangAccount.cpp +++ /dev/null @@ -1,315 +0,0 @@ -/* Copyright 2013-2021 MultiMC Contributors - * - * Authors: Orochimarufan - * - * 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 -#include -#include -#include -#include -#include - -#include - -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 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 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(); -} diff --git a/launcher/minecraft/auth/MojangAccountList.h b/launcher/minecraft/auth/MojangAccountList.h deleted file mode 100644 index 99d2988e..00000000 --- a/launcher/minecraft/auth/MojangAccountList.h +++ /dev/null @@ -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 -#include -#include -#include - -/*! - * \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 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 versions); -}; diff --git a/launcher/minecraft/auth-msa/context.cpp b/launcher/minecraft/auth/flows/AuthContext.cpp similarity index 55% rename from launcher/minecraft/auth-msa/context.cpp rename to launcher/minecraft/auth/flows/AuthContext.cpp index d7ecda30..9aa58ac3 100644 --- a/launcher/minecraft/auth-msa/context.cpp +++ b/launcher/minecraft/auth/flows/AuthContext.cpp @@ -14,9 +14,8 @@ #include #include -#include "context.h" +#include "AuthContext.h" #include "katabasis/Globals.h" -#include "katabasis/StoreQSettings.h" #include "katabasis/Requestor.h" #include "BuildConfig.h" @@ -24,117 +23,107 @@ using OAuth2 = Katabasis::OAuth2; using Requestor = Katabasis::Requestor; using Activity = Katabasis::Activity; -Context::Context(QObject *parent) : - QObject(parent) +AuthContext::AuthContext(AccountData * data, QObject *parent) : + AccountTask(data, parent) { 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; 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.accessTokenUrl = "https://login.live.com/oauth20_token.srf"; 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(oauth2, &OAuth2::linkingSucceeded, this, &Context::onLinkingSucceeded); - connect(oauth2, &OAuth2::openBrowser, this, &Context::onOpenBrowser); - connect(oauth2, &OAuth2::closeBrowser, this, &Context::onCloseBrowser); - connect(oauth2, &OAuth2::activityChanged, this, &Context::onOAuthActivityChanged); + connect(m_oauth2, &OAuth2::linkingFailed, this, &AuthContext::onOAuthLinkingFailed); + connect(m_oauth2, &OAuth2::linkingSucceeded, this, &AuthContext::onOAuthLinkingSucceeded); + connect(m_oauth2, &OAuth2::openBrowser, this, &AuthContext::onOpenBrowser); + connect(m_oauth2, &OAuth2::closeBrowser, this, &AuthContext::onCloseBrowser); + connect(m_oauth2, &OAuth2::activityChanged, this, &AuthContext::onOAuthActivityChanged); } -void Context::beginActivity(Activity activity) { - if(isBusy()) { - throw 0; +void AuthContext::initMojang() { + if(m_yggdrasil) { + return; } - activity_ = activity; - emit activityChanged(activity_); + m_yggdrasil = new Yggdrasil(m_data, this); + + connect(m_yggdrasil, &Task::failed, this, &AuthContext::onMojangFailed); + connect(m_yggdrasil, &Task::succeeded, this, &AuthContext::onMojangSucceeded); } -void Context::finishActivity() { - if(!isBusy()) { - throw 0; - } - activity_ = Katabasis::Activity::Idle; - m_account.validity_ = m_account.minecraftProfile.validity; - emit activityChanged(activity_); +void AuthContext::onMojangSucceeded() { + doMinecraftProfile(); } -QString Context::gameToken() { - return m_account.minecraftToken.token; + +void AuthContext::onMojangFailed() { + finishActivity(); + m_error = m_yggdrasil->m_error; + m_aborted = m_yggdrasil->m_aborted; + changeState(m_yggdrasil->accountState(), "Microsoft user authentication failed."); } -QString Context::userId() { - 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(); - return false; - } - - requestsDone = 0; - xboxProfileSucceeded = false; - mcAuthSucceeded = false; - - return true; -} - -bool Context::signIn() { +/* +bool AuthContext::signOut() { if(isBusy()) { return false; } - requestsDone = 0; - xboxProfileSucceeded = false; - mcAuthSucceeded = false; + start(); - beginActivity(Activity::LoggingIn); - oauth2->unlink(); - m_account = AccountData(); - oauth2->link(); - return true; -} - -bool Context::signOut() { - if(isBusy()) { - return false; - } beginActivity(Activity::LoggingOut); - oauth2->unlink(); + m_oauth2->unlink(); m_account = AccountData(); finishActivity(); return true; } +*/ - -void Context::onOpenBrowser(const QUrl &url) { +void AuthContext::onOpenBrowser(const QUrl &url) { QDesktopServices::openUrl(url); } -void Context::onCloseBrowser() { +void AuthContext::onCloseBrowser() { } -void Context::onLinkingFailed() { +void AuthContext::onOAuthLinkingFailed() { finishActivity(); + changeState(STATE_FAILED_HARD, "Microsoft user authentication failed."); } -void Context::onLinkingSucceeded() { +void AuthContext::onOAuthLinkingSucceeded() { auto *o2t = qobject_cast(sender()); if (!o2t->linked()) { finishActivity(); + changeState(STATE_FAILED_HARD, "Microsoft user authentication ended with an impossible state (succeeded, but not succeeded at the same time)."); return; } QVariantMap extraTokens = o2t->extraTokens(); @@ -147,11 +136,14 @@ void Context::onLinkingSucceeded() { doUserAuth(); } -void Context::onOAuthActivityChanged(Katabasis::Activity activity) { +void AuthContext::onOAuthActivityChanged(Katabasis::Activity activity) { // 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( { "Properties": { @@ -163,15 +155,15 @@ void Context::doUserAuth() { "TokenType": "JWT" } )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")); request.setHeader(QNetworkRequest::ContentTypeHeader, "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); - connect(requestor, &Requestor::finished, this, &Context::onUserAuthDone); + connect(requestor, &Requestor::finished, this, &AuthContext::onUserAuthDone); requestor->post(request, xbox_auth_data.toUtf8()); qDebug() << "First layer of XBox auth ... commencing."; } @@ -181,7 +173,7 @@ bool getDateTime(QJsonValue value, QDateTime & out) { if(!value.isString()) { return false; } - out = QDateTime::fromString(value.toString(), Qt::ISODateWithMs); + out = QDateTime::fromString(value.toString(), Qt::ISODate); return out.isValid(); } @@ -294,7 +286,7 @@ bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output) { } -void Context::onUserAuthDone( +void AuthContext::onUserAuthDone( int requestId, QNetworkReply::NetworkError error, QByteArray replyData, @@ -303,6 +295,7 @@ void Context::onUserAuthDone( if (error != QNetworkReply::NoError) { qWarning() << "Reply error:" << error; finishActivity(); + changeState(STATE_FAILED_HARD, "XBox user authentication failed."); return; } @@ -310,9 +303,13 @@ void Context::onUserAuthDone( if(!parseXTokenResponse(replyData, temp)) { qWarning() << "Could not parse user authentication response..."; finishActivity(); + changeState(STATE_FAILED_HARD, "XBox user authentication response could not be understood."); return; } - m_account.userToken = temp; + m_data->userToken = temp; + + m_stage = MSAStage::XboxAuth; + changeState(STATE_WORKING, "Starting XBox authentication"); doSTSAuthMinecraft(); doSTSAuthGeneric(); @@ -329,7 +326,7 @@ void Context::onUserAuthDone( }, } */ -void Context::doSTSAuthMinecraft() { +void AuthContext::doSTSAuthMinecraft() { QString xbox_auth_template = R"XXX( { "Properties": { @@ -342,20 +339,20 @@ void Context::doSTSAuthMinecraft() { "TokenType": "JWT" } )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")); request.setHeader(QNetworkRequest::ContentTypeHeader, "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); - connect(requestor, &Requestor::finished, this, &Context::onSTSAuthMinecraftDone); + connect(requestor, &Requestor::finished, this, &AuthContext::onSTSAuthMinecraftDone); requestor->post(request, xbox_auth_data.toUtf8()); qDebug() << "Second layer of XBox auth ... commencing."; } -void Context::onSTSAuthMinecraftDone( +void AuthContext::onSTSAuthMinecraftDone( int requestId, QNetworkReply::NetworkError error, QByteArray replyData, @@ -363,29 +360,29 @@ void Context::onSTSAuthMinecraftDone( ) { if (error != QNetworkReply::NoError) { qWarning() << "Reply error:" << error; - finishActivity(); + m_requestsDone ++; return; } Katabasis::Token temp; if(!parseXTokenResponse(replyData, temp)) { qWarning() << "Could not parse authorization response for access to mojang services..."; - finishActivity(); + m_requestsDone ++; 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"; qDebug() << replyData; - finishActivity(); + m_requestsDone ++; return; } - m_account.mojangservicesToken = temp; + m_data->mojangservicesToken = temp; doMinecraftAuth(); } -void Context::doSTSAuthGeneric() { +void AuthContext::doSTSAuthGeneric() { QString xbox_auth_template = R"XXX( { "Properties": { @@ -398,20 +395,20 @@ void Context::doSTSAuthGeneric() { "TokenType": "JWT" } )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")); request.setHeader(QNetworkRequest::ContentTypeHeader, "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); - connect(requestor, &Requestor::finished, this, &Context::onSTSAuthGenericDone); + connect(requestor, &Requestor::finished, this, &AuthContext::onSTSAuthGenericDone); requestor->post(request, xbox_auth_data.toUtf8()); qDebug() << "Second layer of XBox auth ... commencing."; } -void Context::onSTSAuthGenericDone( +void AuthContext::onSTSAuthGenericDone( int requestId, QNetworkReply::NetworkError error, QByteArray replyData, @@ -419,44 +416,44 @@ void Context::onSTSAuthGenericDone( ) { if (error != QNetworkReply::NoError) { qWarning() << "Reply error:" << error; - finishActivity(); + m_requestsDone ++; return; } Katabasis::Token temp; if(!parseXTokenResponse(replyData, temp)) { qWarning() << "Could not parse authorization response for access to xbox API..."; - finishActivity(); + m_requestsDone ++; 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"; qDebug() << replyData; - finishActivity(); + m_requestsDone ++; return; } - m_account.xboxApiToken = temp; + m_data->xboxApiToken = temp; doXBoxProfile(); } -void Context::doMinecraftAuth() { +void AuthContext::doMinecraftAuth() { QString mc_auth_template = R"XXX( { "identityToken": "XBL3.0 x=%1;%2" } )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")); request.setHeader(QNetworkRequest::ContentTypeHeader, "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); - connect(requestor, &Requestor::finished, this, &Context::onMinecraftAuthDone); + connect(requestor, &Requestor::finished, this, &AuthContext::onMinecraftAuthDone); requestor->post(request, data.toUtf8()); qDebug() << "Getting Minecraft access token..."; } @@ -501,33 +498,31 @@ bool parseMojangResponse(QByteArray & data, Katabasis::Token &output) { } } -void Context::onMinecraftAuthDone( +void AuthContext::onMinecraftAuthDone( int requestId, QNetworkReply::NetworkError error, QByteArray replyData, QList headers ) { - requestsDone++; + m_requestsDone ++; if (error != QNetworkReply::NoError) { qWarning() << "Reply error:" << error; qDebug() << replyData; - finishActivity(); return; } - if(!parseMojangResponse(replyData, m_account.minecraftToken)) { + if(!parseMojangResponse(replyData, m_data->yggdrasilToken)) { qWarning() << "Could not parse login_with_xbox response..."; qDebug() << replyData; - finishActivity(); return; } - mcAuthSucceeded = true; + m_mcAuthSucceeded = true; checkResult(); } -void Context::doXBoxProfile() { +void AuthContext::doXBoxProfile() { auto url = QUrl("https://profile.xboxlive.com/users/me/profile/settings"); QUrlQuery q; q.addQueryItem( @@ -544,45 +539,45 @@ void Context::doXBoxProfile() { request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setRawHeader("Accept", "application/json"); 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()); - Requestor *requestor = new Requestor(mgr, oauth2, this); + 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, m_oauth2, this); requestor->setAddAccessTokenInQuery(false); - connect(requestor, &Requestor::finished, this, &Context::onXBoxProfileDone); + connect(requestor, &Requestor::finished, this, &AuthContext::onXBoxProfileDone); requestor->get(request); qDebug() << "Getting Xbox profile..."; } -void Context::onXBoxProfileDone( +void AuthContext::onXBoxProfileDone( int requestId, QNetworkReply::NetworkError error, QByteArray replyData, QList headers ) { - requestsDone ++; + m_requestsDone ++; if (error != QNetworkReply::NoError) { qWarning() << "Reply error:" << error; qDebug() << replyData; - finishActivity(); return; } qDebug() << "XBox profile: " << replyData; - xboxProfileSucceeded = true; + m_xboxProfileSucceeded = true; checkResult(); } -void Context::checkResult() { - if(requestsDone != 2) { +void AuthContext::checkResult() { + if(m_requestsDone != 2) { return; } - if(mcAuthSucceeded && xboxProfileSucceeded) { + if(m_mcAuthSucceeded && m_xboxProfileSucceeded) { doMinecraftProfile(); } else { 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"); QNetworkRequest request = QNetworkRequest(url); request.setHeader(QNetworkRequest::ContentTypeHeader, "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); - connect(requestor, &Requestor::finished, this, &Context::onMinecraftProfileDone); + connect(requestor, &Requestor::finished, this, &AuthContext::onMinecraftProfileDone); requestor->get(request); } -void Context::onMinecraftProfileDone(int, QNetworkReply::NetworkError error, QByteArray data, QList headers) { +void AuthContext::onMinecraftProfileDone(int, QNetworkReply::NetworkError error, QByteArray data, QList headers) { qDebug() << data; if (error == QNetworkReply::ContentNotFoundError) { - m_account.minecraftProfile = MinecraftProfile(); + m_data->minecraftProfile = MinecraftProfile(); finishActivity(); + changeState(STATE_FAILED_HARD, "Account is missing a profile"); return; } if (error != QNetworkReply::NoError) { finishActivity(); + changeState(STATE_FAILED_HARD, "Profile acquisition failed"); return; } - if(!parseMinecraftProfile(data, m_account.minecraftProfile)) { - m_account.minecraftProfile = MinecraftProfile(); + if(!parseMinecraftProfile(data, m_data->minecraftProfile)) { + m_data->minecraftProfile = MinecraftProfile(); finishActivity(); + changeState(STATE_FAILED_HARD, "Profile response could not be parsed"); return; } doGetSkin(); } -void Context::doGetSkin() { - auto url = QUrl(m_account.minecraftProfile.skin.url); +void AuthContext::doGetSkin() { + m_stage = MSAStage::Skin; + changeState(STATE_WORKING, "Starting skin acquisition"); + + auto url = QUrl(m_data->minecraftProfile.skin.url); QNetworkRequest request = QNetworkRequest(url); - Requestor *requestor = new Requestor(mgr, oauth2, this); + Requestor *requestor = new Requestor(mgr, m_oauth2, this); requestor->setAddAccessTokenInQuery(false); - connect(requestor, &Requestor::finished, this, &Context::onSkinDone); + connect(requestor, &Requestor::finished, this, &AuthContext::onSkinDone); requestor->get(request); } -void Context::onSkinDone(int, QNetworkReply::NetworkError error, QByteArray data, QList) { +void AuthContext::onSkinDone(int, QNetworkReply::NetworkError error, QByteArray data, QList) { if (error == QNetworkReply::NoError) { - m_account.minecraftProfile.skin.data = data; + m_data->minecraftProfile.skin.data = data; } + m_data->validity_ = Katabasis::Validity::Certain; finishActivity(); + changeState(STATE_SUCCEEDED, "Finished whole chain"); } -namespace { -void tokenToJSON(QJsonObject &parent, Katabasis::Token t, const char * tokenName) { - 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; - } - +QString AuthContext::getStateMessage() const { + switch (m_accountState) { - 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; + case STATE_WORKING: + switch(m_stage) { + case MSAStage::Idle: { + QString loginMessage = tr("Logging in as %1 user"); + if(m_data->type == AccountType::MSA) { + return loginMessage.arg("Microsoft"); + } + else { + return loginMessage.arg("Mojang"); + } + } + case MSAStage::UserAuth: + return tr("Logging in as XBox user"); + case MSAStage::XboxAuth: + return tr("Logging in with XBox and Mojang services"); + case MSAStage::MinecraftProfile: + return tr("Getting Minecraft profile"); + case MSAStage::Skin: + return tr("Getting Minecraft skin"); + default: + break; + } + default: + return AccountTask::getStateMessage(); } - - 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(); - } - } - - 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 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); } diff --git a/launcher/minecraft/auth-msa/context.h b/launcher/minecraft/auth/flows/AuthContext.h similarity index 52% rename from launcher/minecraft/auth-msa/context.h rename to launcher/minecraft/auth/flows/AuthContext.h index f1ac99b8..5f99dba3 100644 --- a/launcher/minecraft/auth-msa/context.h +++ b/launcher/minecraft/auth/flows/AuthContext.h @@ -7,87 +7,47 @@ #include #include +#include "Yggdrasil.h" +#include "../AccountData.h" +#include "../AccountTask.h" -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 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 +class AuthContext : public AccountTask { Q_OBJECT public: - explicit Context(QObject *parent = 0); - - QByteArray saveState(); - bool resumeFromState(QByteArray data); + explicit AuthContext(AccountData * data, QObject *parent = 0); bool isBusy() { - return activity_ != Katabasis::Activity::Idle; + return m_activity != Katabasis::Activity::Idle; }; Katabasis::Validity validity() { - return m_account.validity_; + return m_data->validity_; }; - bool signIn(); - bool silentSignIn(); - bool signOut(); + //bool signOut(); + + QString getStateMessage() const override; - QString userName(); - QString userId(); - QString gameToken(); signals: - void succeeded(); - void failed(); void activityChanged(Katabasis::Activity activity); private slots: - void onLinkingSucceeded(); - void onLinkingFailed(); +// OAuth-specific callbacks + void onOAuthLinkingSucceeded(); + void onOAuthLinkingFailed(); void onOpenBrowser(const QUrl &url); void onCloseBrowser(); void onOAuthActivityChanged(Katabasis::Activity activity); -private: +// Yggdrasil specific callbacks + void onMojangSucceeded(); + void onMojangFailed(); + +protected: + void initMSA(); + void initMojang(); + void doUserAuth(); Q_SLOT void onUserAuthDone(int, QNetworkReply::NetworkError, QByteArray, QList); @@ -109,20 +69,26 @@ private: void checkResult(); -private: +protected: void beginActivity(Katabasis::Activity activity); void finishActivity(); void clearTokens(); -private: - Katabasis::OAuth2 *oauth2 = nullptr; +protected: + Katabasis::OAuth2 *m_oauth2 = nullptr; + Yggdrasil *m_yggdrasil = nullptr; - int requestsDone = 0; - bool xboxProfileSucceeded = false; - bool mcAuthSucceeded = false; - Katabasis::Activity activity_ = Katabasis::Activity::Idle; - - AccountData m_account; + int m_requestsDone = 0; + bool m_xboxProfileSucceeded = false; + bool m_mcAuthSucceeded = false; + Katabasis::Activity m_activity = Katabasis::Activity::Idle; + enum class MSAStage { + Idle, + UserAuth, + XboxAuth, + MinecraftProfile, + Skin + } m_stage = MSAStage::Idle; QNetworkAccessManager *mgr = nullptr; }; diff --git a/launcher/minecraft/auth/flows/AuthenticateTask.cpp b/launcher/minecraft/auth/flows/AuthenticateTask.cpp deleted file mode 100644 index 2e8dc859..00000000 --- a/launcher/minecraft/auth/flows/AuthenticateTask.cpp +++ /dev/null @@ -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 -#include -#include -#include - -#include -#include - -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 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(); - } -} diff --git a/launcher/minecraft/auth/flows/AuthenticateTask.h b/launcher/minecraft/auth/flows/AuthenticateTask.h deleted file mode 100644 index 4c14eec7..00000000 --- a/launcher/minecraft/auth/flows/AuthenticateTask.h +++ /dev/null @@ -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 -#include -#include - -/** - * 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; -}; diff --git a/launcher/minecraft/auth/flows/MSAHelper.txt b/launcher/minecraft/auth/flows/MSAHelper.txt new file mode 100644 index 00000000..dfaec374 --- /dev/null +++ b/launcher/minecraft/auth/flows/MSAHelper.txt @@ -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_; +}; diff --git a/launcher/minecraft/auth/flows/MSAInteractive.cpp b/launcher/minecraft/auth/flows/MSAInteractive.cpp new file mode 100644 index 00000000..03beb279 --- /dev/null +++ b/launcher/minecraft/auth/flows/MSAInteractive.cpp @@ -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(); +} diff --git a/launcher/minecraft/auth/flows/MSAInteractive.h b/launcher/minecraft/auth/flows/MSAInteractive.h new file mode 100644 index 00000000..9556f254 --- /dev/null +++ b/launcher/minecraft/auth/flows/MSAInteractive.h @@ -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; +}; diff --git a/launcher/minecraft/auth/flows/MSASilent.cpp b/launcher/minecraft/auth/flows/MSASilent.cpp new file mode 100644 index 00000000..8ce43c1f --- /dev/null +++ b/launcher/minecraft/auth/flows/MSASilent.cpp @@ -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(); + } +} diff --git a/launcher/minecraft/auth/flows/MSASilent.h b/launcher/minecraft/auth/flows/MSASilent.h new file mode 100644 index 00000000..e1b3d43d --- /dev/null +++ b/launcher/minecraft/auth/flows/MSASilent.h @@ -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; +}; diff --git a/launcher/minecraft/auth/flows/MojangLogin.cpp b/launcher/minecraft/auth/flows/MojangLogin.cpp new file mode 100644 index 00000000..cca911b5 --- /dev/null +++ b/launcher/minecraft/auth/flows/MojangLogin.cpp @@ -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); +} diff --git a/launcher/minecraft/auth/flows/MojangLogin.h b/launcher/minecraft/auth/flows/MojangLogin.h new file mode 100644 index 00000000..2e765ae8 --- /dev/null +++ b/launcher/minecraft/auth/flows/MojangLogin.h @@ -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; +}; diff --git a/launcher/minecraft/auth/flows/MojangRefresh.cpp b/launcher/minecraft/auth/flows/MojangRefresh.cpp new file mode 100644 index 00000000..af99175c --- /dev/null +++ b/launcher/minecraft/auth/flows/MojangRefresh.cpp @@ -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(); +} diff --git a/launcher/minecraft/auth/flows/MojangRefresh.h b/launcher/minecraft/auth/flows/MojangRefresh.h new file mode 100644 index 00000000..fb4facd5 --- /dev/null +++ b/launcher/minecraft/auth/flows/MojangRefresh.h @@ -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; +}; diff --git a/launcher/minecraft/auth/flows/RefreshTask.cpp b/launcher/minecraft/auth/flows/RefreshTask.cpp deleted file mode 100644 index ecba178d..00000000 --- a/launcher/minecraft/auth/flows/RefreshTask.cpp +++ /dev/null @@ -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 -#include -#include -#include - -#include - -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(); - } -} diff --git a/launcher/minecraft/auth/flows/RefreshTask.h b/launcher/minecraft/auth/flows/RefreshTask.h deleted file mode 100644 index f0840dda..00000000 --- a/launcher/minecraft/auth/flows/RefreshTask.h +++ /dev/null @@ -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 -#include -#include - -/** - * 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; -}; - diff --git a/launcher/minecraft/auth/flows/ValidateTask.cpp b/launcher/minecraft/auth/flows/ValidateTask.cpp deleted file mode 100644 index 6b3f0a65..00000000 --- a/launcher/minecraft/auth/flows/ValidateTask.cpp +++ /dev/null @@ -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 -#include -#include -#include - -#include - -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(); - } -} diff --git a/launcher/minecraft/auth/flows/ValidateTask.h b/launcher/minecraft/auth/flows/ValidateTask.h deleted file mode 100644 index 986c2e9f..00000000 --- a/launcher/minecraft/auth/flows/ValidateTask.h +++ /dev/null @@ -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 -#include -#include - -/** - * 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: -}; diff --git a/launcher/minecraft/auth/YggdrasilTask.cpp b/launcher/minecraft/auth/flows/Yggdrasil.cpp similarity index 56% rename from launcher/minecraft/auth/YggdrasilTask.cpp rename to launcher/minecraft/auth/flows/Yggdrasil.cpp index 0857b46b..7cea059c 100644 --- a/launcher/minecraft/auth/YggdrasilTask.cpp +++ b/launcher/minecraft/auth/flows/Yggdrasil.cpp @@ -13,8 +13,8 @@ * limitations under the License. */ -#include "YggdrasilTask.h" -#include "MojangAccount.h" +#include "Yggdrasil.h" +#include "../AccountData.h" #include #include @@ -29,68 +29,147 @@ #include -YggdrasilTask::YggdrasilTask(MojangAccount *account, QObject *parent) - : Task(parent), m_account(account) +Yggdrasil::Yggdrasil(AccountData *data, QObject *parent) + : AccountTask(data, parent) { changeState(STATE_CREATED); } -void YggdrasilTask::executeTask() -{ - changeState(STATE_SENDING_REQUEST); +void Yggdrasil::sendRequest(QUrl endpoint, QByteArray content) { + changeState(STATE_WORKING); - // Get the content of the request we're going to send to the server. - QJsonDocument doc(getRequestContent()); - - QUrl reqUrl(BuildConfig.AUTH_BASE + getEndpoint()); - QNetworkRequest netRequest(reqUrl); + QNetworkRequest netRequest(endpoint); netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - - QByteArray requestData = doc.toJson(); - m_netReply = ENV.qnam().post(netRequest, requestData); - connect(m_netReply, &QNetworkReply::finished, this, &YggdrasilTask::processReply); - connect(m_netReply, &QNetworkReply::uploadProgress, this, &YggdrasilTask::refreshTimers); - connect(m_netReply, &QNetworkReply::downloadProgress, this, &YggdrasilTask::refreshTimers); - connect(m_netReply, &QNetworkReply::sslErrors, this, &YggdrasilTask::sslErrors); + m_netReply = ENV.qnam().post(netRequest, content); + connect(m_netReply, &QNetworkReply::finished, this, &Yggdrasil::processReply); + connect(m_netReply, &QNetworkReply::uploadProgress, this, &Yggdrasil::refreshTimers); + connect(m_netReply, &QNetworkReply::downloadProgress, this, &Yggdrasil::refreshTimers); + connect(m_netReply, &QNetworkReply::sslErrors, this, &Yggdrasil::sslErrors); timeout_keeper.setSingleShot(true); timeout_keeper.start(timeout_max); counter.setSingleShot(false); counter.start(time_step); progress(0, timeout_max); - connect(&timeout_keeper, &QTimer::timeout, this, &YggdrasilTask::abortByTimeout); - connect(&counter, &QTimer::timeout, this, &YggdrasilTask::heartbeat); + connect(&timeout_keeper, &QTimer::timeout, this, &Yggdrasil::abortByTimeout); + 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.start(timeout_max); progress(count = 0, timeout_max); } -void YggdrasilTask::heartbeat() +void Yggdrasil::heartbeat() { count += time_step; progress(count, timeout_max); } -bool YggdrasilTask::abort() +bool Yggdrasil::abort() { progress(timeout_max, timeout_max); // TODO: actually use this in a meaningful way - m_aborted = YggdrasilTask::BY_USER; + m_aborted = Yggdrasil::BY_USER; m_netReply->abort(); return true; } -void YggdrasilTask::abortByTimeout() +void Yggdrasil::abortByTimeout() { progress(timeout_max, timeout_max); // TODO: actually use this in a meaningful way - m_aborted = YggdrasilTask::BY_TIMEOUT; + m_aborted = Yggdrasil::BY_TIMEOUT; m_netReply->abort(); } -void YggdrasilTask::sslErrors(QList errors) +void Yggdrasil::sslErrors(QList errors) { int i = 1; for (auto error : errors) @@ -102,9 +181,52 @@ void YggdrasilTask::sslErrors(QList 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()) { @@ -195,7 +317,7 @@ void YggdrasilTask::processReply() } } -void YggdrasilTask::processError(QJsonObject responseData) +void Yggdrasil::processError(QJsonObject responseData) { QJsonValue errorVal = responseData.value("error"); QJsonValue errorMessageValue = responseData.value("errorMessage"); @@ -213,43 +335,3 @@ void YggdrasilTask::processError(QJsonObject responseData) 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; -} diff --git a/launcher/minecraft/auth/flows/Yggdrasil.h b/launcher/minecraft/auth/flows/Yggdrasil.h new file mode 100644 index 00000000..e709cb9f --- /dev/null +++ b/launcher/minecraft/auth/flows/Yggdrasil.h @@ -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 +#include +#include +#include + +#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); + 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; +}; diff --git a/launcher/minecraft/launch/ClaimAccount.h b/launcher/minecraft/launch/ClaimAccount.h index c5bd75f3..cb4de23f 100644 --- a/launcher/minecraft/launch/ClaimAccount.h +++ b/launcher/minecraft/launch/ClaimAccount.h @@ -16,7 +16,7 @@ #pragma once #include -#include +#include class ClaimAccount: public LaunchStep { @@ -33,5 +33,5 @@ public: } private: std::unique_ptr lock; - MojangAccountPtr m_account; + MinecraftAccountPtr m_account; }; diff --git a/launcher/pages/global/AccountListPage.cpp b/launcher/pages/global/AccountListPage.cpp index ff3736ed..a3cd86a4 100644 --- a/launcher/pages/global/AccountListPage.cpp +++ b/launcher/pages/global/AccountListPage.cpp @@ -29,12 +29,13 @@ #include "dialogs/CustomMessageBox.h" #include "dialogs/SkinUploadDialog.h" #include "tasks/Task.h" -#include "minecraft/auth/YggdrasilTask.h" +#include "minecraft/auth/AccountTask.h" #include "minecraft/services/SkinDelete.h" #include "MultiMC.h" #include "BuildConfig.h" +#include AccountListPage::AccountListPage(QWidget *parent) : QMainWindow(parent), ui(new Ui::AccountListPage) @@ -50,11 +51,12 @@ AccountListPage::AccountListPage(QWidget *parent) m_accounts = MMC->accounts(); 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); // Expand the account column - ui->listView->header()->setSectionResizeMode(1, QHeaderView::Stretch); QItemSelectionModel *selectionModel = ui->listView->selectionModel(); @@ -63,8 +65,8 @@ AccountListPage::AccountListPage(QWidget *parent) }); connect(ui->listView, &VersionListView::customContextMenuRequested, this, &AccountListPage::ShowContextMenu); - connect(m_accounts.get(), SIGNAL(listChanged()), SLOT(listChanged())); - connect(m_accounts.get(), SIGNAL(activeAccountChanged()), SLOT(listChanged())); + connect(m_accounts.get(), &AccountList::listChanged, this, &AccountListPage::listChanged); + connect(m_accounts.get(), &AccountList::activeAccountChanged, this, &AccountListPage::listChanged); updateButtonStates(); } @@ -103,9 +105,36 @@ void AccountListPage::listChanged() 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() @@ -124,9 +153,8 @@ void AccountListPage::on_actionSetDefault_triggered() if (selection.size() > 0) { QModelIndex selected = selection.first(); - MojangAccountPtr account = - selected.data(MojangAccountList::PointerRole).value(); - m_accounts->setActiveAccount(account->username()); + MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value(); + m_accounts->setActiveAccount(account->profileId()); } } @@ -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() { QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); if (selection.size() > 0) { QModelIndex selected = selection.first(); - MojangAccountPtr account = selected.data(MojangAccountList::PointerRole).value(); + MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value(); SkinUploadDialog dialog(account, this); dialog.exec(); } @@ -202,8 +204,8 @@ void AccountListPage::on_actionDeleteSkin_triggered() QModelIndex selected = selection.first(); AuthSessionPtr session = std::make_shared(); - MojangAccountPtr account = selected.data(MojangAccountList::PointerRole).value(); - auto login = account->login(session); + MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value(); + auto login = account->refresh(session); ProgressDialog prog(this); if (prog.execWithTask((Task*)login.get()) != QDialog::Accepted) { CustomMessageBox::selectable(this, tr("Skin Delete"), tr("Failed to login!"), QMessageBox::Warning)->exec(); diff --git a/launcher/pages/global/AccountListPage.h b/launcher/pages/global/AccountListPage.h index fba1833f..24bb96da 100644 --- a/launcher/pages/global/AccountListPage.h +++ b/launcher/pages/global/AccountListPage.h @@ -20,7 +20,7 @@ #include "pages/BasePage.h" -#include "minecraft/auth/MojangAccountList.h" +#include "minecraft/auth/AccountList.h" #include "MultiMC.h" namespace Ui @@ -60,7 +60,8 @@ public: } public slots: - void on_actionAdd_triggered(); + void on_actionAddMojang_triggered(); + void on_actionAddMicrosoft_triggered(); void on_actionRemove_triggered(); void on_actionSetDefault_triggered(); void on_actionNoDefault_triggered(); @@ -74,11 +75,10 @@ public slots: protected slots: void ShowContextMenu(const QPoint &pos); - void addAccount(const QString& errMsg=""); private: void changeEvent(QEvent * event) override; QMenu * createPopupMenu() override; - std::shared_ptr m_accounts; + std::shared_ptr m_accounts; Ui::AccountListPage *ui; }; diff --git a/launcher/pages/global/AccountListPage.ui b/launcher/pages/global/AccountListPage.ui index 71647db3..887c3d48 100644 --- a/launcher/pages/global/AccountListPage.ui +++ b/launcher/pages/global/AccountListPage.ui @@ -25,7 +25,23 @@ 0 - + + + true + + + false + + + false + + + true + + + false + + @@ -36,7 +52,8 @@ false - + + @@ -44,9 +61,9 @@ - + - Add + Add Mojang @@ -80,6 +97,11 @@ Delete the currently active skin and go back to the default one + + + Add Microsoft + + diff --git a/launcher/pages/instance/VersionPage.cpp b/launcher/pages/instance/VersionPage.cpp index a98bfb7d..20cb2c9f 100644 --- a/launcher/pages/instance/VersionPage.cpp +++ b/launcher/pages/instance/VersionPage.cpp @@ -38,7 +38,7 @@ #include #include "minecraft/PackProfile.h" -#include "minecraft/auth/MojangAccountList.h" +#include "minecraft/auth/AccountList.h" #include "minecraft/mod/Mod.h" #include "icons/IconList.h" #include "Exception.h"