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 01/35] 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" From 44d634f564b0e0c0626cca71e8ecd19c99b838b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Sun, 15 Aug 2021 23:40:37 +0200 Subject: [PATCH 02/35] GH-3392 Fix strings in AuthContext and make them translateable --- launcher/minecraft/auth/flows/AuthContext.cpp | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/launcher/minecraft/auth/flows/AuthContext.cpp b/launcher/minecraft/auth/flows/AuthContext.cpp index 9aa58ac3..859060d6 100644 --- a/launcher/minecraft/auth/flows/AuthContext.cpp +++ b/launcher/minecraft/auth/flows/AuthContext.cpp @@ -87,7 +87,7 @@ void AuthContext::onMojangFailed() { finishActivity(); m_error = m_yggdrasil->m_error; m_aborted = m_yggdrasil->m_aborted; - changeState(m_yggdrasil->accountState(), "Microsoft user authentication failed."); + changeState(m_yggdrasil->accountState(), tr("Mojang user authentication failed.")); } /* @@ -116,14 +116,14 @@ void AuthContext::onCloseBrowser() { void AuthContext::onOAuthLinkingFailed() { finishActivity(); - changeState(STATE_FAILED_HARD, "Microsoft user authentication failed."); + changeState(STATE_FAILED_HARD, tr("Microsoft user authentication failed.")); } 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)."); + changeState(STATE_FAILED_HARD, tr("Microsoft user authentication ended with an impossible state (succeeded, but not succeeded at the same time).")); return; } QVariantMap extraTokens = o2t->extraTokens(); @@ -142,7 +142,7 @@ void AuthContext::onOAuthActivityChanged(Katabasis::Activity activity) { void AuthContext::doUserAuth() { m_stage = MSAStage::UserAuth; - changeState(STATE_WORKING, "Starting user authentication"); + changeState(STATE_WORKING, tr("Starting user authentication")); QString xbox_auth_template = R"XXX( { @@ -295,7 +295,7 @@ void AuthContext::onUserAuthDone( if (error != QNetworkReply::NoError) { qWarning() << "Reply error:" << error; finishActivity(); - changeState(STATE_FAILED_HARD, "XBox user authentication failed."); + changeState(STATE_FAILED_HARD, tr("XBox user authentication failed.")); return; } @@ -303,13 +303,13 @@ void AuthContext::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."); + changeState(STATE_FAILED_HARD, tr("XBox user authentication response could not be understood.")); return; } m_data->userToken = temp; m_stage = MSAStage::XboxAuth; - changeState(STATE_WORKING, "Starting XBox authentication"); + changeState(STATE_WORKING, tr("Starting XBox authentication")); doSTSAuthMinecraft(); doSTSAuthGeneric(); @@ -577,7 +577,7 @@ void AuthContext::checkResult() { } else { finishActivity(); - changeState(STATE_FAILED_HARD, "XBox and/or Mojang authentication steps did not succeed"); + changeState(STATE_FAILED_HARD, tr("XBox and/or Mojang authentication steps did not succeed")); } } @@ -663,7 +663,7 @@ bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) { void AuthContext::doMinecraftProfile() { m_stage = MSAStage::MinecraftProfile; - changeState(STATE_WORKING, "Starting minecraft profile acquisition"); + changeState(STATE_WORKING, tr("Starting minecraft profile acquisition")); auto url = QUrl("https://api.minecraftservices.com/minecraft/profile"); QNetworkRequest request = QNetworkRequest(url); @@ -683,18 +683,18 @@ void AuthContext::onMinecraftProfileDone(int, QNetworkReply::NetworkError error, if (error == QNetworkReply::ContentNotFoundError) { m_data->minecraftProfile = MinecraftProfile(); finishActivity(); - changeState(STATE_FAILED_HARD, "Account is missing a profile"); + changeState(STATE_FAILED_HARD, tr("Account is missing a profile")); return; } if (error != QNetworkReply::NoError) { finishActivity(); - changeState(STATE_FAILED_HARD, "Profile acquisition failed"); + changeState(STATE_FAILED_HARD, tr("Profile acquisition failed")); return; } if(!parseMinecraftProfile(data, m_data->minecraftProfile)) { m_data->minecraftProfile = MinecraftProfile(); finishActivity(); - changeState(STATE_FAILED_HARD, "Profile response could not be parsed"); + changeState(STATE_FAILED_HARD, tr("Profile response could not be parsed")); return; } doGetSkin(); @@ -702,7 +702,7 @@ void AuthContext::onMinecraftProfileDone(int, QNetworkReply::NetworkError error, void AuthContext::doGetSkin() { m_stage = MSAStage::Skin; - changeState(STATE_WORKING, "Starting skin acquisition"); + changeState(STATE_WORKING, tr("Fetching player skin")); auto url = QUrl(m_data->minecraftProfile.skin.url); QNetworkRequest request = QNetworkRequest(url); @@ -718,7 +718,7 @@ void AuthContext::onSkinDone(int, QNetworkReply::NetworkError error, QByteArray } m_data->validity_ = Katabasis::Validity::Certain; finishActivity(); - changeState(STATE_SUCCEEDED, "Finished whole chain"); + changeState(STATE_SUCCEEDED, tr("Finished all authentication steps")); } QString AuthContext::getStateMessage() const { From 4ea52f4758c2dd8e24b296cfaae349e60a5bf3e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Sun, 15 Aug 2021 23:46:12 +0200 Subject: [PATCH 03/35] GH-3392 make sure skin upload at least doesn't fail completely --- launcher/dialogs/SkinUploadDialog.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/dialogs/SkinUploadDialog.cpp b/launcher/dialogs/SkinUploadDialog.cpp index 3c62edac..19bfac4d 100644 --- a/launcher/dialogs/SkinUploadDialog.cpp +++ b/launcher/dialogs/SkinUploadDialog.cpp @@ -15,7 +15,7 @@ void SkinUploadDialog::on_buttonBox_rejected() void SkinUploadDialog::on_buttonBox_accepted() { AuthSessionPtr session = std::make_shared(); - auto login = m_acct->login(session); + auto login = m_acct->refresh(session); ProgressDialog prog(this); if (prog.execWithTask((Task*)login.get()) != QDialog::Accepted) { From 2a21e28ffd6d7a0a72c2acfda6df0e31d3b1c6bd Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Tue, 17 Aug 2021 13:19:04 +0100 Subject: [PATCH 04/35] GH-4012 Disable Xbox login if no MS client token is specified --- launcher/pages/global/AccountListPage.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/launcher/pages/global/AccountListPage.cpp b/launcher/pages/global/AccountListPage.cpp index a3cd86a4..286bbeec 100644 --- a/launcher/pages/global/AccountListPage.cpp +++ b/launcher/pages/global/AccountListPage.cpp @@ -69,6 +69,12 @@ AccountListPage::AccountListPage(QWidget *parent) connect(m_accounts.get(), &AccountList::activeAccountChanged, this, &AccountListPage::listChanged); updateButtonStates(); + + // Xbox authentication won't work without a client identifier, so disable the button + // if the build didn't specify one (GH-4012) + if (BuildConfig.MSA_CLIENT_ID.isEmpty()) { + ui->actionAddMicrosoft->setEnabled(false); + } } AccountListPage::~AccountListPage() From f1a5f7bc4d27fb4f86b61c24b9502d9f8ff79be2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Wed, 18 Aug 2021 03:43:55 +0200 Subject: [PATCH 05/35] NOISSUE add ssl error logging to Requestor --- .../katabasis/include/katabasis/Requestor.h | 3 +++ libraries/katabasis/src/Requestor.cpp | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/libraries/katabasis/include/katabasis/Requestor.h b/libraries/katabasis/include/katabasis/Requestor.h index 61437f76..4bc0256a 100644 --- a/libraries/katabasis/include/katabasis/Requestor.h +++ b/libraries/katabasis/include/katabasis/Requestor.h @@ -80,6 +80,9 @@ protected slots: /// Handle request error. void onRequestError(QNetworkReply::NetworkError error); + /// Handle ssl errors. + void onSslErrors(QList errors); + /// Re-try request (after successful token refresh). void retry(); diff --git a/libraries/katabasis/src/Requestor.cpp b/libraries/katabasis/src/Requestor.cpp index 7b6d2679..e53d4108 100644 --- a/libraries/katabasis/src/Requestor.cpp +++ b/libraries/katabasis/src/Requestor.cpp @@ -40,6 +40,7 @@ int Requestor::get(const QNetworkRequest &req, int timeout/* = 60*1000*/) { timedReplies_.add(new Reply(reply_, timeout)); connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection); connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection); + connect(reply_, &QNetworkReply::sslErrors, this, &Requestor::onSslErrors); return id_; } @@ -53,6 +54,7 @@ int Requestor::post(const QNetworkRequest &req, const QByteArray &data, int time timedReplies_.add(new Reply(reply_, timeout)); connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection); connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection); + connect(reply_, &QNetworkReply::sslErrors, this, &Requestor::onSslErrors); connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64))); return id_; } @@ -69,6 +71,7 @@ int Requestor::post(const QNetworkRequest & req, QHttpMultiPart* data, int timeo timedReplies_.add(new Reply(reply_, timeout)); connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection); connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection); + connect(reply_, &QNetworkReply::sslErrors, this, &Requestor::onSslErrors); connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64))); return id_; } @@ -83,6 +86,7 @@ int Requestor::put(const QNetworkRequest &req, const QByteArray &data, int timeo timedReplies_.add(new Reply(reply_, timeout)); connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection); connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection); + connect(reply_, &QNetworkReply::sslErrors, this, &Requestor::onSslErrors); connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64))); return id_; } @@ -99,6 +103,7 @@ int Requestor::put(const QNetworkRequest & req, QHttpMultiPart* data, int timeou timedReplies_.add(new Reply(reply_, timeout)); connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection); connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection); + connect(reply_, &QNetworkReply::sslErrors, this, &Requestor::onSslErrors); connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64))); return id_; } @@ -118,6 +123,7 @@ int Requestor::customRequest(const QNetworkRequest &req, const QByteArray &verb, timedReplies_.add(new Reply(reply_)); connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection); connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection); + connect(reply_, &QNetworkReply::sslErrors, this, &Requestor::onSslErrors); connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64))); return id_; } @@ -131,6 +137,7 @@ int Requestor::head(const QNetworkRequest &req, int timeout/* = 60*1000*/) timedReplies_.add(new Reply(reply_, timeout)); connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection); connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection); + connect(reply_, &QNetworkReply::sslErrors, this, &Requestor::onSslErrors); return id_; } @@ -180,6 +187,16 @@ void Requestor::onRequestError(QNetworkReply::NetworkError error) { QTimer::singleShot(10, this, SLOT(finish())); } +void Requestor::onSslErrors(QList errors) { + int i = 1; + for (auto error : errors) { + qCritical() << "LOGIN SSL Error #" << i << " : " << error.errorString(); + auto cert = error.certificate(); + qCritical() << "Certificate in question:\n" << cert.toText(); + i++; + } +} + void Requestor::onUploadProgress(qint64 uploaded, qint64 total) { if (status_ == Idle) { qWarning() << "O2Requestor::onUploadProgress: No pending request"; From 4a283fe4c13bda6f2ab9ffe6d32dacb2b61f622c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Wed, 18 Aug 2021 04:18:59 +0200 Subject: [PATCH 06/35] NOISSUE print errorString in Requestor --- libraries/katabasis/src/Requestor.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/katabasis/src/Requestor.cpp b/libraries/katabasis/src/Requestor.cpp index e53d4108..917f2c07 100644 --- a/libraries/katabasis/src/Requestor.cpp +++ b/libraries/katabasis/src/Requestor.cpp @@ -174,6 +174,7 @@ void Requestor::onRequestError(QNetworkReply::NetworkError error) { if (reply_ != qobject_cast(sender())) { return; } + qWarning() << "O2Requestor::onRequestError: Error string: " << reply_->errorString(); int httpStatus = reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); qWarning() << "O2Requestor::onRequestError: HTTP status" << httpStatus << reply_->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); if ((status_ == Requesting) && (httpStatus == 401)) { From 345641f7d2fd896c5e521336049b45d7bf682b23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Thu, 19 Aug 2021 00:43:19 +0200 Subject: [PATCH 07/35] NOISSUE sanitize some MSA auth logging --- launcher/minecraft/auth/flows/AuthContext.cpp | 59 +++++++++++-------- libraries/katabasis/src/OAuth2.cpp | 4 +- 2 files changed, 37 insertions(+), 26 deletions(-) diff --git a/launcher/minecraft/auth/flows/AuthContext.cpp b/launcher/minecraft/auth/flows/AuthContext.cpp index 859060d6..a46f99ca 100644 --- a/launcher/minecraft/auth/flows/AuthContext.cpp +++ b/launcher/minecraft/auth/flows/AuthContext.cpp @@ -127,12 +127,14 @@ void AuthContext::onOAuthLinkingSucceeded() { return; } QVariantMap extraTokens = o2t->extraTokens(); +#ifndef NDEBUG if (!extraTokens.isEmpty()) { qDebug() << "Extra tokens in response:"; foreach (QString key, extraTokens.keys()) { qDebug() << "\t" << key << ":" << extraTokens.value(key); } } +#endif doUserAuth(); } @@ -219,35 +221,34 @@ bool getNumber(QJsonValue value, double & out) { // 2148916238 = child account not linked to a family */ -bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output) { +bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output, const char * name) { + qInfo() << "Parsing" << name <<":"; +#ifndef NDEBUG + qDebug() << data; +#endif QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); if(jsonError.error) { qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString(); - qDebug() << data; return false; } auto obj = doc.object(); if(!getDateTime(obj.value("IssueInstant"), output.issueInstant)) { qWarning() << "User IssueInstant is not a timestamp"; - qDebug() << data; return false; } if(!getDateTime(obj.value("NotAfter"), output.notAfter)) { qWarning() << "User NotAfter is not a timestamp"; - qDebug() << data; return false; } if(!getString(obj.value("Token"), output.token)) { qWarning() << "User Token is not a timestamp"; - qDebug() << data; return false; } auto arrayVal = obj.value("DisplayClaims").toObject().value("xui"); if(!arrayVal.isArray()) { qWarning() << "Missing xui claims array"; - qDebug() << data; return false; } bool foundUHS = false; @@ -266,7 +267,6 @@ bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output) { QString claim; if(!getString(obj.value(iter.key()), claim)) { qWarning() << "display claim " << iter.key() << " is not a string..."; - qDebug() << data; return false; } output.extra[iter.key()] = claim; @@ -276,11 +276,10 @@ bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output) { } if(!foundUHS) { qWarning() << "Missing uhs"; - qDebug() << data; return false; } output.validity = Katabasis::Validity::Certain; - qDebug() << data; + qInfo() << name << "is valid."; return true; } @@ -300,7 +299,7 @@ void AuthContext::onUserAuthDone( } Katabasis::Token temp; - if(!parseXTokenResponse(replyData, temp)) { + if(!parseXTokenResponse(replyData, temp, "UToken")) { qWarning() << "Could not parse user authentication response..."; finishActivity(); changeState(STATE_FAILED_HARD, tr("XBox user authentication response could not be understood.")); @@ -349,7 +348,7 @@ void AuthContext::doSTSAuthMinecraft() { connect(requestor, &Requestor::finished, this, &AuthContext::onSTSAuthMinecraftDone); requestor->post(request, xbox_auth_data.toUtf8()); - qDebug() << "Second layer of XBox auth ... commencing."; + qDebug() << "Getting Minecraft services STS token..."; } void AuthContext::onSTSAuthMinecraftDone( @@ -365,7 +364,7 @@ void AuthContext::onSTSAuthMinecraftDone( } Katabasis::Token temp; - if(!parseXTokenResponse(replyData, temp)) { + if(!parseXTokenResponse(replyData, temp, "STSAuthMinecraft")) { qWarning() << "Could not parse authorization response for access to mojang services..."; m_requestsDone ++; return; @@ -405,7 +404,7 @@ void AuthContext::doSTSAuthGeneric() { connect(requestor, &Requestor::finished, this, &AuthContext::onSTSAuthGenericDone); requestor->post(request, xbox_auth_data.toUtf8()); - qDebug() << "Second layer of XBox auth ... commencing."; + qDebug() << "Getting generic STS token..."; } void AuthContext::onSTSAuthGenericDone( @@ -421,7 +420,7 @@ void AuthContext::onSTSAuthGenericDone( } Katabasis::Token temp; - if(!parseXTokenResponse(replyData, temp)) { + if(!parseXTokenResponse(replyData, temp, "STSAuthGaneric")) { qWarning() << "Could not parse authorization response for access to xbox API..."; m_requestsDone ++; return; @@ -461,10 +460,13 @@ void AuthContext::doMinecraftAuth() { namespace { bool parseMojangResponse(QByteArray & data, Katabasis::Token &output) { QJsonParseError jsonError; + qDebug() << "Parsing Mojang response..."; +#ifndef NDEBUG + qDebug() << data; +#endif QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); if(jsonError.error) { - qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString(); - qDebug() << data; + qWarning() << "Failed to parse response from api.minecraftservices.com/authentication/login_with_xbox as JSON: " << jsonError.errorString(); return false; } @@ -472,7 +474,6 @@ bool parseMojangResponse(QByteArray & data, Katabasis::Token &output) { double expires_in = 0; if(!getNumber(obj.value("expires_in"), expires_in)) { qWarning() << "expires_in is not a valid number"; - qDebug() << data; return false; } auto currentTime = QDateTime::currentDateTimeUtc(); @@ -482,18 +483,16 @@ bool parseMojangResponse(QByteArray & data, Katabasis::Token &output) { QString username; if(!getString(obj.value("username"), username)) { qWarning() << "username is not valid"; - qDebug() << data; return false; } // TODO: it's a JWT... validate it? if(!getString(obj.value("access_token"), output.token)) { qWarning() << "access_token is not valid"; - qDebug() << data; return false; } output.validity = Katabasis::Validity::Certain; - qDebug() << data; + qDebug() << "Mojang response is valid."; return true; } } @@ -508,13 +507,17 @@ void AuthContext::onMinecraftAuthDone( if (error != QNetworkReply::NoError) { qWarning() << "Reply error:" << error; +#ifndef NDEBUG qDebug() << replyData; +#endif return; } if(!parseMojangResponse(replyData, m_data->yggdrasilToken)) { qWarning() << "Could not parse login_with_xbox response..."; +#ifndef NDEBUG qDebug() << replyData; +#endif return; } m_mcAuthSucceeded = true; @@ -558,11 +561,15 @@ void AuthContext::onXBoxProfileDone( if (error != QNetworkReply::NoError) { qWarning() << "Reply error:" << error; +#ifndef NDEBUG qDebug() << replyData; +#endif return; } +#ifndef NDEBUG qDebug() << "XBox profile: " << replyData; +#endif m_xboxProfileSucceeded = true; checkResult(); @@ -583,24 +590,26 @@ void AuthContext::checkResult() { namespace { bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) { + qDebug() << "Parsing Minecraft profile..."; +#ifndef NDEBUG + qDebug() << data; +#endif + QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); if(jsonError.error) { qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString(); - qDebug() << data; return false; } auto obj = doc.object(); if(!getString(obj.value("id"), output.id)) { - qWarning() << "minecraft profile id is not a string"; - qDebug() << data; + qWarning() << "Minecraft profile id is not a string"; return false; } if(!getString(obj.value("name"), output.name)) { - qWarning() << "minecraft profile name is not a string"; - qDebug() << data; + qWarning() << "Minecraft profile name is not a string"; return false; } diff --git a/libraries/katabasis/src/OAuth2.cpp b/libraries/katabasis/src/OAuth2.cpp index 6cc03a0d..3c07420c 100644 --- a/libraries/katabasis/src/OAuth2.cpp +++ b/libraries/katabasis/src/OAuth2.cpp @@ -516,7 +516,9 @@ QString OAuth2::refreshToken() { return token_.refresh_token; } void OAuth2::setRefreshToken(const QString &v) { +#ifndef NDEBUG qDebug() << "OAuth2::setRefreshToken" << v << "..."; +#endif token_.refresh_token = v; } @@ -573,7 +575,7 @@ void OAuth2::onRefreshFinished() { setLinked(true); emit linkingSucceeded(); emit refreshFinished(QNetworkReply::NoError); - qDebug() << " New token expires in" << expires() << "seconds"; + qDebug() << "New token expires in" << expires() << "seconds"; } else { qDebug() << "OAuth2::onRefreshFinished: Error" << (int)refreshReply->error() << refreshReply->errorString(); } From 94fd9a3535ae9a55c228720858292ed2bb69ff98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Thu, 19 Aug 2021 10:27:22 +0200 Subject: [PATCH 08/35] NOISSUE fix linux builds --- launcher/minecraft/auth/flows/AuthContext.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/launcher/minecraft/auth/flows/AuthContext.cpp b/launcher/minecraft/auth/flows/AuthContext.cpp index a46f99ca..d6a72208 100644 --- a/launcher/minecraft/auth/flows/AuthContext.cpp +++ b/launcher/minecraft/auth/flows/AuthContext.cpp @@ -222,7 +222,7 @@ bool getNumber(QJsonValue value, double & out) { */ bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output, const char * name) { - qInfo() << "Parsing" << name <<":"; + qDebug() << "Parsing" << name <<":"; #ifndef NDEBUG qDebug() << data; #endif @@ -279,7 +279,7 @@ bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output, const char return false; } output.validity = Katabasis::Validity::Certain; - qInfo() << name << "is valid."; + qDebug() << name << "is valid."; return true; } From 1b68d51da634ddab39fe872fcc28a4f491c0c8a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Fri, 20 Aug 2021 01:34:32 +0200 Subject: [PATCH 09/35] NOISSUE add setting capes, tweak missing profile message, fix cape IDs --- launcher/CMakeLists.txt | 2 + launcher/dialogs/LoginDialog.cpp | 12 +- launcher/dialogs/MSALoginDialog.cpp | 12 +- launcher/dialogs/MSALoginDialog.ui | 10 +- launcher/dialogs/SkinUploadDialog.cpp | 44 ++++- launcher/dialogs/SkinUploadDialog.ui | 170 ++++++++++-------- launcher/minecraft/auth/AccountData.cpp | 76 ++++---- launcher/minecraft/auth/AccountData.h | 4 +- launcher/minecraft/auth/flows/AuthContext.cpp | 18 +- launcher/minecraft/services/CapeChange.cpp | 67 +++++++ launcher/minecraft/services/CapeChange.h | 32 ++++ 11 files changed, 317 insertions(+), 130 deletions(-) create mode 100644 launcher/minecraft/services/CapeChange.cpp create mode 100644 launcher/minecraft/services/CapeChange.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 3c140ede..84a03895 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -329,6 +329,8 @@ set(MINECRAFT_SOURCES minecraft/AssetsUtils.cpp # Minecraft services + minecraft/services/CapeChange.cpp + minecraft/services/CapeChange.h minecraft/services/SkinUpload.cpp minecraft/services/SkinUpload.h minecraft/services/SkinDelete.cpp diff --git a/launcher/dialogs/LoginDialog.cpp b/launcher/dialogs/LoginDialog.cpp index 1dee9920..b1ca2c88 100644 --- a/launcher/dialogs/LoginDialog.cpp +++ b/launcher/dialogs/LoginDialog.cpp @@ -73,7 +73,17 @@ void LoginDialog::on_passTextBox_textEdited(const QString &newText) void LoginDialog::onTaskFailed(const QString &reason) { // Set message - ui->label->setText("" + reason + ""); + auto lines = reason.split('\n'); + QString processed; + for(auto line: lines) { + if(line.size()) { + processed += "" + line + "\n"; + } + else { + processed += '\n'; + } + } + ui->label->setText(processed); // Re-enable user-interaction setUserInputsEnabled(true); diff --git a/launcher/dialogs/MSALoginDialog.cpp b/launcher/dialogs/MSALoginDialog.cpp index 778b379d..86ebdf91 100644 --- a/launcher/dialogs/MSALoginDialog.cpp +++ b/launcher/dialogs/MSALoginDialog.cpp @@ -60,7 +60,17 @@ void MSALoginDialog::setUserInputsEnabled(bool enable) void MSALoginDialog::onTaskFailed(const QString &reason) { // Set message - ui->label->setText("" + reason + ""); + auto lines = reason.split('\n'); + QString processed; + for(auto line: lines) { + if(line.size()) { + processed += "" + line + "\n"; + } + else { + processed += '\n'; + } + } + ui->label->setText(processed); // Re-enable user-interaction setUserInputsEnabled(true); diff --git a/launcher/dialogs/MSALoginDialog.ui b/launcher/dialogs/MSALoginDialog.ui index 4ae8085a..5479a726 100644 --- a/launcher/dialogs/MSALoginDialog.ui +++ b/launcher/dialogs/MSALoginDialog.ui @@ -6,8 +6,8 @@ 0 0 - 421 - 114 + 491 + 143 @@ -23,10 +23,12 @@ - Message label placeholder. + Message label placeholder. + +aaaaa - Qt::RichText + Qt::MarkdownText Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse diff --git a/launcher/dialogs/SkinUploadDialog.cpp b/launcher/dialogs/SkinUploadDialog.cpp index 19bfac4d..97478f4b 100644 --- a/launcher/dialogs/SkinUploadDialog.cpp +++ b/launcher/dialogs/SkinUploadDialog.cpp @@ -1,11 +1,16 @@ #include #include +#include + #include #include +#include + #include "SkinUploadDialog.h" #include "ui_SkinUploadDialog.h" #include "ProgressDialog.h" #include "CustomMessageBox.h" +#include void SkinUploadDialog::on_buttonBox_rejected() { @@ -85,8 +90,13 @@ void SkinUploadDialog::on_buttonBox_accepted() { model = SkinUpload::ALEX; } - SkinUploadPtr upload = std::make_shared(this, session, FS::read(fileName), model); - if (prog.execWithTask((Task*)upload.get()) != QDialog::Accepted) + SequentialTask skinUpload; + skinUpload.addTask(std::make_shared(this, session, FS::read(fileName), model)); + auto selectedCape = ui->capeCombo->currentData().toString(); + if(selectedCape != session->m_accountPtr->accountData()->minecraftProfile.currentCape) { + skinUpload.addTask(std::make_shared(this, session, selectedCape)); + } + if (prog.execWithTask(&skinUpload) != QDialog::Accepted) { CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Failed to upload skin!"), QMessageBox::Warning)->exec(); close(); @@ -111,4 +121,34 @@ SkinUploadDialog::SkinUploadDialog(MinecraftAccountPtr acct, QWidget *parent) :QDialog(parent), m_acct(acct), ui(new Ui::SkinUploadDialog) { ui->setupUi(this); + + // FIXME: add a model for this, download/refresh the capes on demand + auto &data = *acct->accountData(); + int index = 0; + ui->capeCombo->addItem(tr("No Cape"), QVariant()); + auto currentCape = data.minecraftProfile.currentCape; + if(currentCape.isEmpty()) { + ui->capeCombo->setCurrentIndex(index); + } + + for(auto & cape: data.minecraftProfile.capes) { + index++; + if(cape.data.size()) { + QPixmap capeImage; + if(capeImage.loadFromData(cape.data, "PNG")) { + QPixmap preview = QPixmap(10, 16); + QPainter painter(&preview); + painter.drawPixmap(0, 0, capeImage.copy(1, 1, 10, 16)); + ui->capeCombo->addItem(capeImage, cape.alias, cape.id); + if(currentCape == cape.id) { + ui->capeCombo->setCurrentIndex(index); + } + continue; + } + } + ui->capeCombo->addItem(cape.alias, cape.id); + if(currentCape == cape.id) { + ui->capeCombo->setCurrentIndex(index); + } + } } diff --git a/launcher/dialogs/SkinUploadDialog.ui b/launcher/dialogs/SkinUploadDialog.ui index 6f5307e3..f4b0ed0a 100644 --- a/launcher/dialogs/SkinUploadDialog.ui +++ b/launcher/dialogs/SkinUploadDialog.ui @@ -1,85 +1,97 @@ - SkinUploadDialog - - - - 0 - 0 - 413 - 300 - + SkinUploadDialog + + + + 0 + 0 + 394 + 360 + + + + Skin Upload + + + + + + Skin File + + + + + + + + + + 0 + 0 + - - Skin Upload + + + 28 + 16777215 + - - - - - Skin File - - - - - - - - - - 0 - 0 - - - - - 28 - 16777215 - - - - ... - - - - - - - - - - Player Model - - - - - - Steve Model - - - true - - - - - - - Alex Model - - - - - - - - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - + + ... + + + + - - + + + + + Player Model + + + + + + Steve Model + + + true + + + + + + + Alex Model + + + + + + + + + + Cape + + + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp index 77c73c1b..5c6de9df 100644 --- a/launcher/minecraft/auth/AccountData.cpp +++ b/launcher/minecraft/auth/AccountData.cpp @@ -78,8 +78,8 @@ void profileToJSONV3(QJsonObject &parent, MinecraftProfile p, const char * token QJsonObject out; out["id"] = QJsonValue(p.id); out["name"] = QJsonValue(p.name); - if(p.currentCape != -1) { - out["cape"] = p.capes[p.currentCape].id; + if(!p.currentCape.isEmpty()) { + out["cape"] = p.currentCape; } { @@ -155,41 +155,53 @@ MinecraftProfile profileFromJSONV3(const QJsonObject &parent, const char * token } } - 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!"; + { + auto capesV = tokenObject.value("capes"); + if(!capesV.isArray()) { + qWarning() << "capes is not an array!"; 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(); + 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()); + // 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[cape.id] = cape; } - else if (!dataV.isUndefined()) { - qWarning() << "cape data is something unexpected"; - return MinecraftProfile(); + } + // current cape + { + auto capeV = tokenObject.value("cape"); + if(capeV.isString()) { + auto currentCape = capeV.toString(); + if(out.capes.contains(currentCape)) { + out.currentCape = currentCape; + } } - out.capes.push_back(cape); } out.validity = Katabasis::Validity::Assumed; return out; diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h index b2d09cb0..cf58fb76 100644 --- a/launcher/minecraft/auth/AccountData.h +++ b/launcher/minecraft/auth/AccountData.h @@ -25,8 +25,8 @@ struct MinecraftProfile { QString id; QString name; Skin skin; - int currentCape = -1; - QVector capes; + QString currentCape; + QMap capes; Katabasis::Validity validity = Katabasis::Validity::None; }; diff --git a/launcher/minecraft/auth/flows/AuthContext.cpp b/launcher/minecraft/auth/flows/AuthContext.cpp index d6a72208..9754d1a9 100644 --- a/launcher/minecraft/auth/flows/AuthContext.cpp +++ b/launcher/minecraft/auth/flows/AuthContext.cpp @@ -576,7 +576,9 @@ void AuthContext::onXBoxProfileDone( } void AuthContext::checkResult() { + qDebug() << "AuthContext::checkResult called"; if(m_requestsDone != 2) { + qDebug() << "Number of ready results:" << m_requestsDone; return; } if(m_mcAuthSucceeded && m_xboxProfileSucceeded) { @@ -638,10 +640,9 @@ bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) { break; } auto capesArray = obj.value("capes").toArray(); - int i = -1; - int currentCape = -1; + + QString currentCape; for(auto cape: capesArray) { - i++; auto capeObj = cape.toObject(); Cape capeOut; if(!getString(capeObj.value("id"), capeOut.id)) { @@ -652,7 +653,7 @@ bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) { continue; } if(state == "ACTIVE") { - currentCape = i; + currentCape = capeOut.id; } if(!getString(capeObj.value("url"), capeOut.url)) { continue; @@ -661,8 +662,7 @@ bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) { continue; } - // we deal with only the active skin - output.capes.push_back(capeOut); + output.capes[capeOut.id] = capeOut; } output.currentCape = currentCape; output.validity = Katabasis::Validity::Certain; @@ -692,18 +692,18 @@ void AuthContext::onMinecraftProfileDone(int, QNetworkReply::NetworkError error, if (error == QNetworkReply::ContentNotFoundError) { m_data->minecraftProfile = MinecraftProfile(); finishActivity(); - changeState(STATE_FAILED_HARD, tr("Account is missing a profile")); + changeState(STATE_FAILED_HARD, tr("Account is missing a Minecraft Java profile.\n\nWhile the Microsoft account is valid, it does not own the game.\n\nYou might own Bedrock on this account, but that does not give you access to Java currently.")); return; } if (error != QNetworkReply::NoError) { finishActivity(); - changeState(STATE_FAILED_HARD, tr("Profile acquisition failed")); + changeState(STATE_FAILED_HARD, tr("Minecraft Java profile acquisition failed.")); return; } if(!parseMinecraftProfile(data, m_data->minecraftProfile)) { m_data->minecraftProfile = MinecraftProfile(); finishActivity(); - changeState(STATE_FAILED_HARD, tr("Profile response could not be parsed")); + changeState(STATE_FAILED_HARD, tr("Minecraft Java profile response could not be parsed")); return; } doGetSkin(); diff --git a/launcher/minecraft/services/CapeChange.cpp b/launcher/minecraft/services/CapeChange.cpp new file mode 100644 index 00000000..c1d88d14 --- /dev/null +++ b/launcher/minecraft/services/CapeChange.cpp @@ -0,0 +1,67 @@ +#include "CapeChange.h" +#include +#include +#include + +CapeChange::CapeChange(QObject *parent, AuthSessionPtr session, QString cape) + : Task(parent), m_capeId(cape), m_session(session) +{ +} + +void CapeChange::setCape(QString& cape) { + QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active")); + auto requestString = QString("{\"capeId\":\"%1\"}").arg(m_capeId); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_session->access_token).toLocal8Bit()); + QNetworkReply *rep = ENV.qnam().put(request, requestString.toUtf8()); + + setStatus(tr("Equipping cape")); + + m_reply = std::shared_ptr(rep); + connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); + connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); + connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished())); +} + +void CapeChange::clearCape() { + QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active")); + auto requestString = QString("{\"capeId\":\"%1\"}").arg(m_capeId); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_session->access_token).toLocal8Bit()); + QNetworkReply *rep = ENV.qnam().deleteResource(request); + + setStatus(tr("Removing cape")); + + m_reply = std::shared_ptr(rep); + connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); + connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); + connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished())); +} + + +void CapeChange::executeTask() +{ + if(m_capeId.isEmpty()) { + clearCape(); + } + else { + setCape(m_capeId); + } +} + +void CapeChange::downloadError(QNetworkReply::NetworkError error) +{ + // error happened during download. + qCritical() << "Network error: " << error; + emitFailed(m_reply->errorString()); +} + +void CapeChange::downloadFinished() +{ + // if the download failed + if (m_reply->error() != QNetworkReply::NetworkError::NoError) + { + emitFailed(QString("Network error: %1").arg(m_reply->errorString())); + m_reply.reset(); + return; + } + emitSucceeded(); +} diff --git a/launcher/minecraft/services/CapeChange.h b/launcher/minecraft/services/CapeChange.h new file mode 100644 index 00000000..1b6f2f72 --- /dev/null +++ b/launcher/minecraft/services/CapeChange.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include +#include "tasks/Task.h" + +class CapeChange : public Task +{ + Q_OBJECT +public: + CapeChange(QObject *parent, AuthSessionPtr session, QString capeId); + virtual ~CapeChange() {} + +private: + void setCape(QString & cape); + void clearCape(); + +private: + QString m_capeId; + AuthSessionPtr m_session; + std::shared_ptr m_reply; + +protected: + virtual void executeTask(); + +public slots: + void downloadError(QNetworkReply::NetworkError); + void downloadFinished(); +}; + From 50b92c1af2d28ab59a9b70ce0b3dca62bf1a7583 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Fri, 20 Aug 2021 01:57:59 +0200 Subject: [PATCH 10/35] NOISSUE Markdown is not available in Qt 5.4 ... who would have thought? --- launcher/dialogs/LoginDialog.cpp | 4 ++-- launcher/dialogs/MSALoginDialog.cpp | 4 ++-- launcher/dialogs/MSALoginDialog.ui | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/launcher/dialogs/LoginDialog.cpp b/launcher/dialogs/LoginDialog.cpp index b1ca2c88..bf0806e1 100644 --- a/launcher/dialogs/LoginDialog.cpp +++ b/launcher/dialogs/LoginDialog.cpp @@ -77,10 +77,10 @@ void LoginDialog::onTaskFailed(const QString &reason) QString processed; for(auto line: lines) { if(line.size()) { - processed += "" + line + "\n"; + processed += "" + line + "
"; } else { - processed += '\n'; + processed += "
"; } } ui->label->setText(processed); diff --git a/launcher/dialogs/MSALoginDialog.cpp b/launcher/dialogs/MSALoginDialog.cpp index 86ebdf91..14a0e243 100644 --- a/launcher/dialogs/MSALoginDialog.cpp +++ b/launcher/dialogs/MSALoginDialog.cpp @@ -64,10 +64,10 @@ void MSALoginDialog::onTaskFailed(const QString &reason) QString processed; for(auto line: lines) { if(line.size()) { - processed += "" + line + "\n"; + processed += "" + line + "
"; } else { - processed += '\n'; + processed += "
"; } } ui->label->setText(processed); diff --git a/launcher/dialogs/MSALoginDialog.ui b/launcher/dialogs/MSALoginDialog.ui index 5479a726..4d82fe2b 100644 --- a/launcher/dialogs/MSALoginDialog.ui +++ b/launcher/dialogs/MSALoginDialog.ui @@ -28,7 +28,7 @@ aaaaa
- Qt::MarkdownText + Qt::RichText Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse From eae65da110bb957194b34e0f3573ce4e6e6ddc78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Sun, 22 Aug 2021 20:01:18 +0200 Subject: [PATCH 11/35] GH-3392 Switch MS account login to use device flow instead Device flow involves the user manually opening a web page and putting in a code. We no longer need to interact with the browser. --- launcher/dialogs/MSALoginDialog.cpp | 34 ++++++++++++++ launcher/dialogs/MSALoginDialog.h | 10 +++- launcher/dialogs/MSALoginDialog.ui | 3 ++ launcher/minecraft/auth/AccountTask.h | 4 ++ launcher/minecraft/auth/flows/AuthContext.cpp | 46 ++++++++++--------- launcher/minecraft/auth/flows/AuthContext.h | 14 +++--- .../katabasis/include/katabasis/OAuth2.h | 4 +- libraries/katabasis/src/OAuth2.cpp | 23 +++++----- 8 files changed, 96 insertions(+), 42 deletions(-) diff --git a/launcher/dialogs/MSALoginDialog.cpp b/launcher/dialogs/MSALoginDialog.cpp index 14a0e243..989a4c9f 100644 --- a/launcher/dialogs/MSALoginDialog.cpp +++ b/launcher/dialogs/MSALoginDialog.cpp @@ -41,6 +41,9 @@ int MSALoginDialog::exec() { 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); + connect(m_loginTask.get(), &AccountTask::showVerificationUriAndCode, this, &MSALoginDialog::showVerificationUriAndCode); + connect(m_loginTask.get(), &AccountTask::hideVerificationUriAndCode, this, &MSALoginDialog::hideVerificationUriAndCode); + connect(&m_externalLoginTimer, &QTimer::timeout, this, &MSALoginDialog::externalLoginTick); m_loginTask->start(); return QDialog::exec(); @@ -52,6 +55,37 @@ MSALoginDialog::~MSALoginDialog() delete ui; } +void MSALoginDialog::externalLoginTick() { + m_externalLoginElapsed++; + ui->progressBar->setValue(m_externalLoginElapsed); + ui->progressBar->repaint(); + + if(m_externalLoginElapsed >= m_externalLoginTimeout) { + m_externalLoginTimer.stop(); + } +} + + +void MSALoginDialog::showVerificationUriAndCode(const QUrl& uri, const QString& code, int expiresIn) { + m_externalLoginElapsed = 0; + m_externalLoginTimeout = expiresIn; + + m_externalLoginTimer.setInterval(1000); + m_externalLoginTimer.setSingleShot(false); + m_externalLoginTimer.start(); + + ui->progressBar->setMaximum(expiresIn); + ui->progressBar->setValue(m_externalLoginElapsed); + + QString urlString = uri.toString(); + QString linkString = QString("%2").arg(urlString, urlString); + ui->label->setText(tr("

Please open up %1 in a browser and put in the code %2 to proceed with login.

").arg(linkString, code)); +} + +void MSALoginDialog::hideVerificationUriAndCode() { + m_externalLoginTimer.stop(); +} + void MSALoginDialog::setUserInputsEnabled(bool enable) { ui->buttonBox->setEnabled(enable); diff --git a/launcher/dialogs/MSALoginDialog.h b/launcher/dialogs/MSALoginDialog.h index 402180ee..3d26a0dd 100644 --- a/launcher/dialogs/MSALoginDialog.h +++ b/launcher/dialogs/MSALoginDialog.h @@ -17,6 +17,7 @@ #include #include +#include #include "minecraft/auth/MinecraftAccount.h" @@ -46,10 +47,17 @@ slots: void onTaskSucceeded(); void onTaskStatus(const QString &status); void onTaskProgress(qint64 current, qint64 total); + void showVerificationUriAndCode(const QUrl &uri, const QString &code, int expiresIn); + void hideVerificationUriAndCode(); + + void externalLoginTick(); private: Ui::MSALoginDialog *ui; MinecraftAccountPtr m_account; - std::shared_ptr m_loginTask; + std::shared_ptr m_loginTask; + QTimer m_externalLoginTimer; + int m_externalLoginElapsed = 0; + int m_externalLoginTimeout = 0; }; diff --git a/launcher/dialogs/MSALoginDialog.ui b/launcher/dialogs/MSALoginDialog.ui index 4d82fe2b..78cbfb26 100644 --- a/launcher/dialogs/MSALoginDialog.ui +++ b/launcher/dialogs/MSALoginDialog.ui @@ -30,6 +30,9 @@ aaaaa Qt::RichText + + true + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse diff --git a/launcher/minecraft/auth/AccountTask.h b/launcher/minecraft/auth/AccountTask.h index 3f08096f..fc3488eb 100644 --- a/launcher/minecraft/auth/AccountTask.h +++ b/launcher/minecraft/auth/AccountTask.h @@ -83,6 +83,10 @@ public: return m_accountState; } +signals: + void showVerificationUriAndCode(const QUrl &uri, const QString &code, int expiresIn); + void hideVerificationUriAndCode(); + protected: /** diff --git a/launcher/minecraft/auth/flows/AuthContext.cpp b/launcher/minecraft/auth/flows/AuthContext.cpp index 9754d1a9..ecd7e310 100644 --- a/launcher/minecraft/auth/flows/AuthContext.cpp +++ b/launcher/minecraft/auth/flows/AuthContext.cpp @@ -43,7 +43,7 @@ void AuthContext::finishActivity() { throw 0; } m_activity = Katabasis::Activity::Idle; - m_stage = MSAStage::Idle; + setStage(AuthStage::Complete); m_data->validity_ = m_data->minecraftProfile.validity; emit activityChanged(m_activity); } @@ -55,16 +55,16 @@ void AuthContext::initMSA() { Katabasis::OAuth2::Options opts; opts.scope = "XboxLive.signin offline_access"; 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.authorizationUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode"; + opts.accessTokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"; opts.listenerPorts = {28562, 28563, 28564, 28565, 28566}; m_oauth2 = new OAuth2(opts, m_data->msaToken, this, mgr); + m_oauth2->setGrantFlow(Katabasis::OAuth2::GrantFlowDevice); 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::showVerificationUriAndCode, this, &AuthContext::showVerificationUriAndCode); connect(m_oauth2, &OAuth2::activityChanged, this, &AuthContext::onOAuthActivityChanged); } @@ -106,20 +106,14 @@ bool AuthContext::signOut() { } */ -void AuthContext::onOpenBrowser(const QUrl &url) { - QDesktopServices::openUrl(url); -} - -void AuthContext::onCloseBrowser() { - -} - void AuthContext::onOAuthLinkingFailed() { + emit hideVerificationUriAndCode(); finishActivity(); changeState(STATE_FAILED_HARD, tr("Microsoft user authentication failed.")); } void AuthContext::onOAuthLinkingSucceeded() { + emit hideVerificationUriAndCode(); auto *o2t = qobject_cast(sender()); if (!o2t->linked()) { finishActivity(); @@ -143,7 +137,7 @@ void AuthContext::onOAuthActivityChanged(Katabasis::Activity activity) { } void AuthContext::doUserAuth() { - m_stage = MSAStage::UserAuth; + setStage(AuthStage::UserAuth); changeState(STATE_WORKING, tr("Starting user authentication")); QString xbox_auth_template = R"XXX( @@ -307,7 +301,7 @@ void AuthContext::onUserAuthDone( } m_data->userToken = temp; - m_stage = MSAStage::XboxAuth; + setStage(AuthStage::XboxAuth); changeState(STATE_WORKING, tr("Starting XBox authentication")); doSTSAuthMinecraft(); @@ -671,7 +665,7 @@ bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) { } void AuthContext::doMinecraftProfile() { - m_stage = MSAStage::MinecraftProfile; + setStage(AuthStage::MinecraftProfile); changeState(STATE_WORKING, tr("Starting minecraft profile acquisition")); auto url = QUrl("https://api.minecraftservices.com/minecraft/profile"); @@ -710,7 +704,7 @@ void AuthContext::onMinecraftProfileDone(int, QNetworkReply::NetworkError error, } void AuthContext::doGetSkin() { - m_stage = MSAStage::Skin; + setStage(AuthStage::Skin); changeState(STATE_WORKING, tr("Fetching player skin")); auto url = QUrl(m_data->minecraftProfile.skin.url); @@ -730,12 +724,18 @@ void AuthContext::onSkinDone(int, QNetworkReply::NetworkError error, QByteArray changeState(STATE_SUCCEEDED, tr("Finished all authentication steps")); } +void AuthContext::setStage(AuthContext::AuthStage stage) { + m_stage = stage; + emit progress((int)m_stage, (int)AuthStage::Complete); +} + + QString AuthContext::getStateMessage() const { switch (m_accountState) { case STATE_WORKING: switch(m_stage) { - case MSAStage::Idle: { + case AuthStage::Initial: { QString loginMessage = tr("Logging in as %1 user"); if(m_data->type == AccountType::MSA) { return loginMessage.arg("Microsoft"); @@ -744,14 +744,16 @@ QString AuthContext::getStateMessage() const { return loginMessage.arg("Mojang"); } } - case MSAStage::UserAuth: + case AuthStage::UserAuth: return tr("Logging in as XBox user"); - case MSAStage::XboxAuth: + case AuthStage::XboxAuth: return tr("Logging in with XBox and Mojang services"); - case MSAStage::MinecraftProfile: + case AuthStage::MinecraftProfile: return tr("Getting Minecraft profile"); - case MSAStage::Skin: + case AuthStage::Skin: return tr("Getting Minecraft skin"); + case AuthStage::Complete: + return tr("Finished"); default: break; } diff --git a/launcher/minecraft/auth/flows/AuthContext.h b/launcher/minecraft/auth/flows/AuthContext.h index 5f99dba3..1d9f8f72 100644 --- a/launcher/minecraft/auth/flows/AuthContext.h +++ b/launcher/minecraft/auth/flows/AuthContext.h @@ -36,8 +36,7 @@ private slots: // OAuth-specific callbacks void onOAuthLinkingSucceeded(); void onOAuthLinkingFailed(); - void onOpenBrowser(const QUrl &url); - void onCloseBrowser(); + void onOAuthActivityChanged(Katabasis::Activity activity); // Yggdrasil specific callbacks @@ -82,13 +81,16 @@ protected: bool m_xboxProfileSucceeded = false; bool m_mcAuthSucceeded = false; Katabasis::Activity m_activity = Katabasis::Activity::Idle; - enum class MSAStage { - Idle, + enum class AuthStage { + Initial, UserAuth, XboxAuth, MinecraftProfile, - Skin - } m_stage = MSAStage::Idle; + Skin, + Complete + } m_stage = AuthStage::Initial; + + void setStage(AuthStage stage); QNetworkAccessManager *mgr = nullptr; }; diff --git a/libraries/katabasis/include/katabasis/OAuth2.h b/libraries/katabasis/include/katabasis/OAuth2.h index 4361691c..9dbe5c71 100644 --- a/libraries/katabasis/include/katabasis/OAuth2.h +++ b/libraries/katabasis/include/katabasis/OAuth2.h @@ -140,7 +140,7 @@ signals: void closeBrowser(); /// Emitted when client needs to show a verification uri and user code - void showVerificationUriAndCode(const QUrl &uri, const QString &code); + void showVerificationUriAndCode(const QUrl &uri, const QString &code, int expiresIn); /// Emitted when authentication/deauthentication succeeded. void linkingSucceeded(); @@ -181,7 +181,7 @@ protected: void setExpires(QDateTime v); /// Start polling authorization server - void startPollServer(const QVariantMap ¶ms); + void startPollServer(const QVariantMap ¶ms, int expiresIn); /// Set authentication token. void setToken(const QString &v); diff --git a/libraries/katabasis/src/OAuth2.cpp b/libraries/katabasis/src/OAuth2.cpp index 3c07420c..9756d377 100644 --- a/libraries/katabasis/src/OAuth2.cpp +++ b/libraries/katabasis/src/OAuth2.cpp @@ -472,16 +472,8 @@ void OAuth2::setExpires(QDateTime v) { token_.notAfter = v; } -void OAuth2::startPollServer(const QVariantMap ¶ms) +void OAuth2::startPollServer(const QVariantMap ¶ms, int expiresIn) { - bool ok = false; - int expiresIn = params[OAUTH2_EXPIRES_IN].toInt(&ok); - if (!ok) { - qWarning() << "OAuth2::startPollServer: No expired_in parameter"; - emit linkingFailed(); - return; - } - qDebug() << "OAuth2::startPollServer: device_ and user_code expires in" << expiresIn << "seconds"; QUrl url(options_.accessTokenUrl); @@ -502,6 +494,7 @@ void OAuth2::startPollServer(const QVariantMap ¶ms) PollServer * pollServer = new PollServer(manager_, authRequest, payload, expiresIn, this); if (params.contains(OAUTH2_INTERVAL)) { + bool ok = false; int interval = params[OAUTH2_INTERVAL].toInt(&ok); if (ok) pollServer->setInterval(interval); @@ -629,9 +622,17 @@ void OAuth2::onDeviceAuthReplyFinished() if (params.contains(OAUTH2_VERIFICATION_URI_COMPLETE)) emit openBrowser(params.take(OAUTH2_VERIFICATION_URI_COMPLETE).toUrl()); - emit showVerificationUriAndCode(uri, userCode); + bool ok = false; + int expiresIn = params[OAUTH2_EXPIRES_IN].toInt(&ok); + if (!ok) { + qWarning() << "OAuth2::startPollServer: No expired_in parameter"; + emit linkingFailed(); + return; + } - startPollServer(params); + emit showVerificationUriAndCode(uri, userCode, expiresIn); + + startPollServer(params, expiresIn); } else { qWarning() << "OAuth2::onDeviceAuthReplyFinished: Mandatory parameters missing from response"; emit linkingFailed(); From e2be2ada05c503701f048b09cb14ff923a48e056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Sun, 22 Aug 2021 20:10:57 +0200 Subject: [PATCH 12/35] NOISSUE fix build Missing QUrl include --- launcher/dialogs/MSALoginDialog.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/launcher/dialogs/MSALoginDialog.cpp b/launcher/dialogs/MSALoginDialog.cpp index 989a4c9f..15c04061 100644 --- a/launcher/dialogs/MSALoginDialog.cpp +++ b/launcher/dialogs/MSALoginDialog.cpp @@ -19,6 +19,7 @@ #include "minecraft/auth/AccountTask.h" #include +#include MSALoginDialog::MSALoginDialog(QWidget *parent) : QDialog(parent), ui(new Ui::MSALoginDialog) { From 34a5459dcef1adb7eb355bb0f940eb212173857f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Wed, 25 Aug 2021 21:27:51 +0200 Subject: [PATCH 13/35] NOISSUE cut down Requestor --- launcher/minecraft/auth/flows/AuthContext.cpp | 15 +- launcher/pages/global/AccountListPage.cpp | 2 +- .../katabasis/include/katabasis/Requestor.h | 40 ----- libraries/katabasis/src/Requestor.cpp | 141 +----------------- 4 files changed, 9 insertions(+), 189 deletions(-) diff --git a/launcher/minecraft/auth/flows/AuthContext.cpp b/launcher/minecraft/auth/flows/AuthContext.cpp index ecd7e310..ed8acd40 100644 --- a/launcher/minecraft/auth/flows/AuthContext.cpp +++ b/launcher/minecraft/auth/flows/AuthContext.cpp @@ -156,9 +156,7 @@ void AuthContext::doUserAuth() { 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, m_oauth2, this); - requestor->setAddAccessTokenInQuery(false); - + auto *requestor = new Requestor(mgr, m_oauth2, this); connect(requestor, &Requestor::finished, this, &AuthContext::onUserAuthDone); requestor->post(request, xbox_auth_data.toUtf8()); qDebug() << "First layer of XBox auth ... commencing."; @@ -338,8 +336,6 @@ void AuthContext::doSTSAuthMinecraft() { request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setRawHeader("Accept", "application/json"); Requestor *requestor = new Requestor(mgr, m_oauth2, this); - requestor->setAddAccessTokenInQuery(false); - connect(requestor, &Requestor::finished, this, &AuthContext::onSTSAuthMinecraftDone); requestor->post(request, xbox_auth_data.toUtf8()); qDebug() << "Getting Minecraft services STS token..."; @@ -394,8 +390,6 @@ void AuthContext::doSTSAuthGeneric() { request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setRawHeader("Accept", "application/json"); Requestor *requestor = new Requestor(mgr, m_oauth2, this); - requestor->setAddAccessTokenInQuery(false); - connect(requestor, &Requestor::finished, this, &AuthContext::onSTSAuthGenericDone); requestor->post(request, xbox_auth_data.toUtf8()); qDebug() << "Getting generic STS token..."; @@ -444,8 +438,6 @@ void AuthContext::doMinecraftAuth() { request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setRawHeader("Accept", "application/json"); Requestor *requestor = new Requestor(mgr, m_oauth2, this); - requestor->setAddAccessTokenInQuery(false); - connect(requestor, &Requestor::finished, this, &AuthContext::onMinecraftAuthDone); requestor->post(request, data.toUtf8()); qDebug() << "Getting Minecraft access token..."; @@ -538,8 +530,6 @@ void AuthContext::doXBoxProfile() { request.setRawHeader("x-xbl-contract-version", "3"); 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, &AuthContext::onXBoxProfileDone); requestor->get(request); qDebug() << "Getting Xbox profile..."; @@ -675,8 +665,6 @@ void AuthContext::doMinecraftProfile() { request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8()); Requestor *requestor = new Requestor(mgr, m_oauth2, this); - requestor->setAddAccessTokenInQuery(false); - connect(requestor, &Requestor::finished, this, &AuthContext::onMinecraftProfileDone); requestor->get(request); } @@ -710,7 +698,6 @@ void AuthContext::doGetSkin() { auto url = QUrl(m_data->minecraftProfile.skin.url); QNetworkRequest request = QNetworkRequest(url); Requestor *requestor = new Requestor(mgr, m_oauth2, this); - requestor->setAddAccessTokenInQuery(false); connect(requestor, &Requestor::finished, this, &AuthContext::onSkinDone); requestor->get(request); } diff --git a/launcher/pages/global/AccountListPage.cpp b/launcher/pages/global/AccountListPage.cpp index 286bbeec..d71b942e 100644 --- a/launcher/pages/global/AccountListPage.cpp +++ b/launcher/pages/global/AccountListPage.cpp @@ -73,7 +73,7 @@ AccountListPage::AccountListPage(QWidget *parent) // Xbox authentication won't work without a client identifier, so disable the button // if the build didn't specify one (GH-4012) if (BuildConfig.MSA_CLIENT_ID.isEmpty()) { - ui->actionAddMicrosoft->setEnabled(false); + ui->actionAddMicrosoft->setVisible(false); } } diff --git a/libraries/katabasis/include/katabasis/Requestor.h b/libraries/katabasis/include/katabasis/Requestor.h index 4bc0256a..de8016cb 100644 --- a/libraries/katabasis/include/katabasis/Requestor.h +++ b/libraries/katabasis/include/katabasis/Requestor.h @@ -20,47 +20,11 @@ class Requestor: public QObject { public: explicit Requestor(QNetworkAccessManager *manager, OAuth2 *authenticator, QObject *parent = 0); ~Requestor(); - - - /// Some services require the access token to be sent as a Authentication HTTP header - /// and refuse requests with the access token in the query. - /// This function allows to use or ignore the access token in the query. - /// The default value of `true` means that the query will contain the access token. - /// By setting the value to false, the query will not contain the access token. - /// See: - /// https://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-16#section-4.3 - /// https://tools.ietf.org/html/rfc6750#section-2.3 - - void setAddAccessTokenInQuery(bool value); - - /// Some services require the access token to be sent as a Authentication HTTP header. - /// This is the case for Twitch and Mixer. - /// When the access token expires and is refreshed, O2Requestor::retry() needs to update the Authentication HTTP header. - /// In order to do so, O2Requestor needs to know the format of the Authentication HTTP header. - void setAccessTokenInAuthenticationHTTPHeaderFormat(const QString &value); public slots: - /// Make a GET request. - /// @return Request ID or -1 if there are too many requests in the queue. int get(const QNetworkRequest &req, int timeout = 60*1000); - - /// Make a POST request. - /// @return Request ID or -1 if there are too many requests in the queue. int post(const QNetworkRequest &req, const QByteArray &data, int timeout = 60*1000); - int post(const QNetworkRequest &req, QHttpMultiPart* data, int timeout = 60*1000); - /// Make a PUT request. - /// @return Request ID or -1 if there are too many requests in the queue. - int put(const QNetworkRequest &req, const QByteArray &data, int timeout = 60*1000); - int put(const QNetworkRequest &req, QHttpMultiPart* data, int timeout = 60*1000); - - /// Make a HEAD request. - /// @return Request ID or -1 if there are too many requests in the queue. - int head(const QNetworkRequest &req, int timeout = 60*1000); - - /// Make a custom request. - /// @return Request ID or -1 if there are too many requests in the queue. - int customRequest(const QNetworkRequest &req, const QByteArray &verb, const QByteArray &data, int timeout = 60*1000); signals: @@ -103,7 +67,6 @@ protected: OAuth2 *authenticator_; QNetworkRequest request_; QByteArray data_; - QHttpMultiPart* multipartData_; QNetworkReply *reply_; Status status_; int id_; @@ -111,9 +74,6 @@ protected: QUrl url_; ReplyList timedReplies_; QNetworkReply::NetworkError error_; - bool addAccessTokenInQuery_; - QString accessTokenInAuthenticationHTTPHeaderFormat_; - bool rawData_; }; } diff --git a/libraries/katabasis/src/Requestor.cpp b/libraries/katabasis/src/Requestor.cpp index 917f2c07..53d77925 100644 --- a/libraries/katabasis/src/Requestor.cpp +++ b/libraries/katabasis/src/Requestor.cpp @@ -11,35 +11,27 @@ namespace Katabasis { -Requestor::Requestor(QNetworkAccessManager *manager, OAuth2 *authenticator, QObject *parent): QObject(parent), reply_(NULL), status_(Idle), addAccessTokenInQuery_(true), rawData_(false) { +Requestor::Requestor(QNetworkAccessManager *manager, OAuth2 *authenticator, QObject *parent): QObject(parent), reply_(NULL), status_(Idle) { manager_ = manager; authenticator_ = authenticator; if (authenticator) { timedReplies_.setIgnoreSslErrors(authenticator->ignoreSslErrors()); } qRegisterMetaType("QNetworkReply::NetworkError"); - connect(authenticator, &OAuth2::refreshFinished, this, &Requestor::onRefreshFinished, Qt::QueuedConnection); + connect(authenticator, &OAuth2::refreshFinished, this, &Requestor::onRefreshFinished); } Requestor::~Requestor() { } -void Requestor::setAddAccessTokenInQuery(bool value) { - addAccessTokenInQuery_ = value; -} - -void Requestor::setAccessTokenInAuthenticationHTTPHeaderFormat(const QString &value) { - accessTokenInAuthenticationHTTPHeaderFormat_ = value; -} - int Requestor::get(const QNetworkRequest &req, int timeout/* = 60*1000*/) { if (-1 == setup(req, QNetworkAccessManager::GetOperation)) { return -1; } reply_ = manager_->get(request_); timedReplies_.add(new Reply(reply_, timeout)); - connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection); - connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection); + connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError))); + connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished())); connect(reply_, &QNetworkReply::sslErrors, this, &Requestor::onSslErrors); return id_; } @@ -48,99 +40,16 @@ int Requestor::post(const QNetworkRequest &req, const QByteArray &data, int time if (-1 == setup(req, QNetworkAccessManager::PostOperation)) { return -1; } - rawData_ = true; data_ = data; reply_ = manager_->post(request_, data_); timedReplies_.add(new Reply(reply_, timeout)); - connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection); - connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection); + connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError))); + connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished())); connect(reply_, &QNetworkReply::sslErrors, this, &Requestor::onSslErrors); connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64))); return id_; } -int Requestor::post(const QNetworkRequest & req, QHttpMultiPart* data, int timeout/* = 60*1000*/) -{ - if (-1 == setup(req, QNetworkAccessManager::PostOperation)) { - return -1; - } - rawData_ = false; - multipartData_ = data; - reply_ = manager_->post(request_, multipartData_); - multipartData_->setParent(reply_); - timedReplies_.add(new Reply(reply_, timeout)); - connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection); - connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection); - connect(reply_, &QNetworkReply::sslErrors, this, &Requestor::onSslErrors); - connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64))); - return id_; -} - -int Requestor::put(const QNetworkRequest &req, const QByteArray &data, int timeout/* = 60*1000*/) { - if (-1 == setup(req, QNetworkAccessManager::PutOperation)) { - return -1; - } - rawData_ = true; - data_ = data; - reply_ = manager_->put(request_, data_); - timedReplies_.add(new Reply(reply_, timeout)); - connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection); - connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection); - connect(reply_, &QNetworkReply::sslErrors, this, &Requestor::onSslErrors); - connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64))); - return id_; -} - -int Requestor::put(const QNetworkRequest & req, QHttpMultiPart* data, int timeout/* = 60*1000*/) -{ - if (-1 == setup(req, QNetworkAccessManager::PutOperation)) { - return -1; - } - rawData_ = false; - multipartData_ = data; - reply_ = manager_->put(request_, multipartData_); - multipartData_->setParent(reply_); - timedReplies_.add(new Reply(reply_, timeout)); - connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection); - connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection); - connect(reply_, &QNetworkReply::sslErrors, this, &Requestor::onSslErrors); - connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64))); - return id_; -} - -int Requestor::customRequest(const QNetworkRequest &req, const QByteArray &verb, const QByteArray &data, int timeout/* = 60*1000*/) -{ - (void)timeout; - - if (-1 == setup(req, QNetworkAccessManager::CustomOperation, verb)) { - return -1; - } - data_ = data; - QBuffer * buffer = new QBuffer; - buffer->setData(data_); - reply_ = manager_->sendCustomRequest(request_, verb, buffer); - buffer->setParent(reply_); - timedReplies_.add(new Reply(reply_)); - connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection); - connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection); - connect(reply_, &QNetworkReply::sslErrors, this, &Requestor::onSslErrors); - connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64))); - return id_; -} - -int Requestor::head(const QNetworkRequest &req, int timeout/* = 60*1000*/) -{ - if (-1 == setup(req, QNetworkAccessManager::HeadOperation)) { - return -1; - } - reply_ = manager_->head(request_); - timedReplies_.add(new Reply(reply_, timeout)); - connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection); - connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection); - connect(reply_, &QNetworkReply::sslErrors, this, &Requestor::onSslErrors); - return id_; -} - void Requestor::onRefreshFinished(QNetworkReply::NetworkError error) { if (status_ != Requesting) { qWarning() << "O2Requestor::onRefreshFinished: No pending request"; @@ -227,19 +136,8 @@ int Requestor::setup(const QNetworkRequest &req, QNetworkAccessManager::Operatio url_ = req.url(); QUrl url = url_; - if (addAccessTokenInQuery_) { - QUrlQuery query(url); - query.addQueryItem(OAUTH2_ACCESS_TOKEN, authenticator_->token()); - url.setQuery(query); - } - request_.setUrl(url); - // If the service require the access token to be sent as a Authentication HTTP header, we add the access token. - if (!accessTokenInAuthenticationHTTPHeaderFormat_.isEmpty()) { - request_.setRawHeader(HTTP_AUTHORIZATION_HEADER, accessTokenInAuthenticationHTTPHeaderFormat_.arg(authenticator_->token()).toLatin1()); - } - if (!verb.isEmpty()) { request_.setRawHeader(HTTP_HTTP_HEADER, verb); } @@ -273,40 +171,15 @@ void Requestor::retry() { reply_->disconnect(this); reply_->deleteLater(); QUrl url = url_; - if (addAccessTokenInQuery_) { - QUrlQuery query(url); - query.addQueryItem(OAUTH2_ACCESS_TOKEN, authenticator_->token()); - url.setQuery(query); - } request_.setUrl(url); - // If the service require the access token to be sent as a Authentication HTTP header, - // we update the access token when retrying. - if(!accessTokenInAuthenticationHTTPHeaderFormat_.isEmpty()) { - request_.setRawHeader(HTTP_AUTHORIZATION_HEADER, accessTokenInAuthenticationHTTPHeaderFormat_.arg(authenticator_->token()).toLatin1()); - } - status_ = ReRequesting; switch (operation_) { case QNetworkAccessManager::GetOperation: reply_ = manager_->get(request_); break; case QNetworkAccessManager::PostOperation: - reply_ = rawData_ ? manager_->post(request_, data_) : manager_->post(request_, multipartData_); - break; - case QNetworkAccessManager::CustomOperation: - { - QBuffer * buffer = new QBuffer; - buffer->setData(data_); - reply_ = manager_->sendCustomRequest(request_, request_.rawHeader(HTTP_HTTP_HEADER), buffer); - buffer->setParent(reply_); - } - break; - case QNetworkAccessManager::PutOperation: - reply_ = rawData_ ? manager_->post(request_, data_) : manager_->put(request_, multipartData_); - break; - case QNetworkAccessManager::HeadOperation: - reply_ = manager_->head(request_); + reply_ = manager_->post(request_, data_); break; default: assert(!"Unspecified operation for request"); From b2c1100b1c3a1eb64152eba7aafac6493b2f7ff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Fri, 27 Aug 2021 22:35:17 +0200 Subject: [PATCH 14/35] NOISSUE introduce the concept of secrets static library --- .gitignore | 3 +++ CMakeLists.txt | 8 ++++++-- buildconfig/BuildConfig.cpp.in | 1 - buildconfig/BuildConfig.h | 5 ----- launcher/CMakeLists.txt | 4 ++++ launcher/LaunchController.cpp | 11 +++++++++-- launcher/minecraft/auth/MinecraftAccount.cpp | 7 ++++++- launcher/minecraft/auth/flows/AuthContext.cpp | 9 +++++++-- launcher/pages/global/AccountListPage.cpp | 6 +++--- libraries/katabasis/src/OAuth2.cpp | 1 + 10 files changed, 39 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index e11168c3..6b716252 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,6 @@ tags #OSX Stuff .DS_Store + +branding +secrets diff --git a/CMakeLists.txt b/CMakeLists.txt index 817b4cfc..acc777fc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -90,8 +90,8 @@ 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") + +option(MultiMC_EMBED_SECRETS "Determines whether to embed secrets. Secrets are separate and non-public." OFF) #### Check the current Git commit and branch include(GetGitRevisionDescription) @@ -287,5 +287,9 @@ add_subdirectory(libraries/katabasis) # An OAuth2 library that tried to do too m add_subdirectory(buildconfig) +if(MultiMC_EMBED_SECRETS) + add_subdirectory(secrets) +endif() + # NOTE: this must always be last to appease the CMake deity of quirky install command evaluation order. add_subdirectory(launcher) diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in index 9d4771b4..60d417a6 100644 --- a/buildconfig/BuildConfig.cpp.in +++ b/buildconfig/BuildConfig.cpp.in @@ -35,7 +35,6 @@ 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 71880109..de7d4b49 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -75,11 +75,6 @@ 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; diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 84a03895..81740adb 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -947,6 +947,10 @@ install(TARGETS MultiMC RUNTIME DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime ) +if(MultiMC_EMBED_SECRETS) + target_link_libraries(MultiMC_logic secrets) +endif() + #### The MultiMC bundle mess! #### # Bundle utilities are used to complete the portable packages - they add all the libraries that would otherwise be missing on the target system. # NOTE: it seems that this absolutely has to be here, and nowhere else. diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index 11780625..82c97ecf 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -171,9 +171,16 @@ void LaunchController::login() { break; } case AuthSession::RequiresOAuth: { - // FIXME: add UI for expired / broken MS accounts + auto errorString = tr("Microsoft account has expired and needs to be logged into manually again."); + QMessageBox::warning( + nullptr, + tr("Microsoft Account refresh failed"), + errorString, + QMessageBox::StandardButton::Ok, + QMessageBox::StandardButton::Ok + ); tryagain = false; - emitFailed(tr("Microsoft account has expired and needs to be logged into again.")); + emitFailed(errorString); return; } case AuthSession::PlayableOffline: { diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp index 671f9c38..4231d6b0 100644 --- a/launcher/minecraft/auth/MinecraftAccount.cpp +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -245,7 +245,12 @@ void MinecraftAccount::authFailed(QString reason) emit changed(); if (session) { - session->status = AuthSession::RequiresPassword; + if(data.type == AccountType::MSA) { + session->status = AuthSession::RequiresOAuth; + } + else { + session->status = AuthSession::RequiresPassword; + } session->auth_server_online = true; fillSession(session); } diff --git a/launcher/minecraft/auth/flows/AuthContext.cpp b/launcher/minecraft/auth/flows/AuthContext.cpp index ed8acd40..9ae99453 100644 --- a/launcher/minecraft/auth/flows/AuthContext.cpp +++ b/launcher/minecraft/auth/flows/AuthContext.cpp @@ -17,7 +17,10 @@ #include "AuthContext.h" #include "katabasis/Globals.h" #include "katabasis/Requestor.h" -#include "BuildConfig.h" + +#ifdef EMBED_SECRETS +#include "Secrets.h" +#endif using OAuth2 = Katabasis::OAuth2; using Requestor = Katabasis::Requestor; @@ -49,12 +52,13 @@ void AuthContext::finishActivity() { } void AuthContext::initMSA() { +#ifdef EMBED_SECRETS if(m_oauth2) { return; } Katabasis::OAuth2::Options opts; opts.scope = "XboxLive.signin offline_access"; - opts.clientIdentifier = BuildConfig.MSA_CLIENT_ID; + opts.clientIdentifier = Secrets::getMSAClientID('-'); opts.authorizationUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode"; opts.accessTokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"; opts.listenerPorts = {28562, 28563, 28564, 28565, 28566}; @@ -66,6 +70,7 @@ void AuthContext::initMSA() { connect(m_oauth2, &OAuth2::linkingSucceeded, this, &AuthContext::onOAuthLinkingSucceeded); connect(m_oauth2, &OAuth2::showVerificationUriAndCode, this, &AuthContext::showVerificationUriAndCode); connect(m_oauth2, &OAuth2::activityChanged, this, &AuthContext::onOAuthActivityChanged); +#endif } void AuthContext::initMojang() { diff --git a/launcher/pages/global/AccountListPage.cpp b/launcher/pages/global/AccountListPage.cpp index d71b942e..45b778de 100644 --- a/launcher/pages/global/AccountListPage.cpp +++ b/launcher/pages/global/AccountListPage.cpp @@ -72,9 +72,9 @@ AccountListPage::AccountListPage(QWidget *parent) // Xbox authentication won't work without a client identifier, so disable the button // if the build didn't specify one (GH-4012) - if (BuildConfig.MSA_CLIENT_ID.isEmpty()) { - ui->actionAddMicrosoft->setVisible(false); - } +#ifndef EMBED_SECRETS + ui->actionAddMicrosoft->setVisible(false); +#endif } AccountListPage::~AccountListPage() diff --git a/libraries/katabasis/src/OAuth2.cpp b/libraries/katabasis/src/OAuth2.cpp index 9756d377..260aa9c1 100644 --- a/libraries/katabasis/src/OAuth2.cpp +++ b/libraries/katabasis/src/OAuth2.cpp @@ -570,6 +570,7 @@ void OAuth2::onRefreshFinished() { emit refreshFinished(QNetworkReply::NoError); qDebug() << "New token expires in" << expires() << "seconds"; } else { + emit linkingFailed(); qDebug() << "OAuth2::onRefreshFinished: Error" << (int)refreshReply->error() << refreshReply->errorString(); } refreshReply->deleteLater(); From 93c527ed3d2147696bab4ca931b08bc8a183d8d8 Mon Sep 17 00:00:00 2001 From: StaticRocket <35777938+StaticRocket@users.noreply.github.com> Date: Sat, 28 Aug 2021 21:13:35 -0400 Subject: [PATCH 15/35] Add flat icon for custom-commands --- launcher/resources/flat/flat.qrc | 1 + .../flat/scalable/custom-commands.svg | 86 +++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 launcher/resources/flat/scalable/custom-commands.svg diff --git a/launcher/resources/flat/flat.qrc b/launcher/resources/flat/flat.qrc index b6e2ee38..614d7f98 100644 --- a/launcher/resources/flat/flat.qrc +++ b/launcher/resources/flat/flat.qrc @@ -10,6 +10,7 @@ scalable/checkupdate.svg scalable/copy.svg scalable/coremods.svg + scalable/custom-commands.svg scalable/discord.svg scalable/externaltools.svg scalable/help.svg diff --git a/launcher/resources/flat/scalable/custom-commands.svg b/launcher/resources/flat/scalable/custom-commands.svg new file mode 100644 index 00000000..a35634b1 --- /dev/null +++ b/launcher/resources/flat/scalable/custom-commands.svg @@ -0,0 +1,86 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + From 1e1655bc4b2d6fef3bb14736c7f9506fca246876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Sun, 29 Aug 2021 02:12:09 +0200 Subject: [PATCH 16/35] NOISSUE update README.md It has not been touched in a long time. This brings it a bit more up to date. --- README.md | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index ede8f88f..92260d5e 100644 --- a/README.md +++ b/README.md @@ -5,15 +5,14 @@ MultiMC 5 ========= -MultiMC is a custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once. It also allows you to easily install and remove mods by simply dragging and dropping. Here are the current [features](https://github.com/MultiMC/MultiMC5/wiki#features) of MultiMC. - +MultiMC is a custom launcher for Minecraft that focuses on predictability, long term stability and simplicity. ## Development -The project uses C++ and Qt5 as the language and base framework. This might seem odd in the Minecraft community, but allows using 25MB of RAM, where other tools use an excessive amount of resources for no reason. +If you want to contribute, talk to us on [Discord](https://discord.gg/multimc) first. -We can do more, with less, on worse hardware and leave more resources for the game while keeping the launcher running and providing extra features. +While blindly submitting PRs is definitely possible, they're not necessarily going to get accepted. -If you want to contribute, either talk to us on [Discord](https://discord.gg/multimc), [IRC](http://webchat.esper.net/?nick=&channels=MultiMC)(esper.net/#MultiMC) or pick up some item from the github issues [workflowy](https://github.com/MultiMC/MultiMC5/issues) - there is always plenty of ideas around. +We aren't looking for flashy features, but expanding upon the existing feature set without distruption or endangering future viability of the project is OK. ### Building If you want to build MultiMC yourself, check [BUILD.md](BUILD.md) for build instructions. @@ -21,19 +20,21 @@ If you want to build MultiMC yourself, check [BUILD.md](BUILD.md) for build inst ### Code formatting Just follow the existing formatting. -In general: -* Indent with 4 space unless it's in a submodule -* Keep lists (of arguments, parameters, initializators...) as lists, not paragraphs. +In general, in order of importance: +* Make sure your IDE is not messing up line endings or whitespace and avoid using linters. * Prefer readability over dogma. +* Keep to the existing formatting. +* Indent with 4 space unless it's in a submodule. +* Keep lists (of arguments, parameters, initializers...) as lists, not paragraphs. It should either read from top to bottom, or left to right. Not both. ## Translations -Translations can be done [on crowdin](https://translate.multimc.org). +Translations can be done [on crowdin](https://translate.multimc.org). Please avoid making direct pull requests to the translations repository. -## Forking/Redistributing +## Forking/Redistributing/Custom builds policy We keep MultiMC open source because we think it's important to be able to see the source code for a project like this, and we do so using the Apache license. -Part of the reason for using the Apache license is we don't want people using the "MultiMC" name when redistributing the project. This means people must take the time to go through the source code and remove all references to "MultiMC", including but not limited to the project icon and the title of windows, (no *MultiMC-fork* in the title). +Part of the reason for using the Apache license is that we don't want people using the "MultiMC" name when redistributing the project. This means people must take the time to go through the source code and remove all references to "MultiMC", including but not limited to the project icon and the title of windows, (no *MultiMC-fork* in the title). Apache covers reasonable use for the name - a mention of the project's origins in the About dialog and the license is acceptable. However, it should be abundantly clear that the project is a fork *without* implying that you have our blessing. From 7239502675fb68b1a2050c68f483e5d5371114e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Sun, 29 Aug 2021 19:58:35 +0200 Subject: [PATCH 17/35] GH-3392 Add recognition of already migrated Mojang accounts --- launcher/LaunchController.cpp | 13 ++++ launcher/minecraft/auth/AccountTask.cpp | 4 +- launcher/minecraft/auth/AccountTask.h | 1 + launcher/minecraft/auth/AuthSession.h | 3 +- launcher/minecraft/auth/MinecraftAccount.cpp | 74 ++++++++++++++------ launcher/minecraft/auth/flows/Yggdrasil.cpp | 43 +++++++----- 6 files changed, 97 insertions(+), 41 deletions(-) diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index 82c97ecf..190605fd 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -183,6 +183,19 @@ void LaunchController::login() { emitFailed(errorString); return; } + case AuthSession::GoneOrMigrated: { + auto errorString = tr("The account no longer exists on the servers. It may have been migrated, in which case please add the new account you migrated this one to."); + QMessageBox::warning( + nullptr, + tr("Account gone"), + errorString, + QMessageBox::StandardButton::Ok, + QMessageBox::StandardButton::Ok + ); + tryagain = false; + emitFailed(errorString); + return; + } case AuthSession::PlayableOffline: { // we ask the user for a player name bool ok = false; diff --git a/launcher/minecraft/auth/AccountTask.cpp b/launcher/minecraft/auth/AccountTask.cpp index c06be42b..d400ce8d 100644 --- a/launcher/minecraft/auth/AccountTask.cpp +++ b/launcher/minecraft/auth/AccountTask.cpp @@ -49,6 +49,8 @@ QString AccountTask::getStateMessage() const return tr("Failed to contact the authentication server."); case STATE_FAILED_HARD: return tr("Failed to authenticate."); + case STATE_FAILED_GONE: + return tr("Failed to authenticate. The account no longer exists."); default: return tr("..."); } @@ -62,7 +64,7 @@ void AccountTask::changeState(AccountTask::State newState, QString reason) { emitSucceeded(); } - else if (newState == STATE_FAILED_HARD || newState == STATE_FAILED_SOFT) + else if (newState == STATE_FAILED_HARD || newState == STATE_FAILED_SOFT || newState == STATE_FAILED_GONE) { emitFailed(reason); } diff --git a/launcher/minecraft/auth/AccountTask.h b/launcher/minecraft/auth/AccountTask.h index fc3488eb..4f3bd52a 100644 --- a/launcher/minecraft/auth/AccountTask.h +++ b/launcher/minecraft/auth/AccountTask.h @@ -76,6 +76,7 @@ public: 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_FAILED_GONE, //!< hard failure. auth is invalid, and the account no longer exists STATE_SUCCEEDED } m_accountState = STATE_CREATED; diff --git a/launcher/minecraft/auth/AuthSession.h b/launcher/minecraft/auth/AuthSession.h index d77435b8..f609d5d3 100644 --- a/launcher/minecraft/auth/AuthSession.h +++ b/launcher/minecraft/auth/AuthSession.h @@ -18,7 +18,8 @@ struct AuthSession RequiresOAuth, RequiresPassword, PlayableOffline, - PlayableOnline + PlayableOnline, + GoneOrMigrated } status = Undetermined; // client token diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp index 4231d6b0..2d76f9ac 100644 --- a/launcher/minecraft/auth/MinecraftAccount.cpp +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -227,32 +227,60 @@ 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); + switch (m_currentTask->accountState()) { + case AccountTask::STATE_FAILED_SOFT: { + if (session) + { + if(accountStatus() == Verified) { + session->status = AuthSession::PlayableOffline; + } + else { + if(data.type == AccountType::MSA) { + session->status = AuthSession::RequiresOAuth; + } + else { + session->status = 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) - { - if(data.type == AccountType::MSA) { - session->status = AuthSession::RequiresOAuth; + break; + case AccountTask::STATE_FAILED_HARD: { + // FIXME: MSA data clearing + data.yggdrasilToken.token = QString(); + data.yggdrasilToken.validity = Katabasis::Validity::None; + data.validity_ = Katabasis::Validity::None; + emit changed(); + if (session) + { + if(data.type == AccountType::MSA) { + session->status = AuthSession::RequiresOAuth; + } + else { + session->status = AuthSession::RequiresPassword; + } + session->auth_server_online = true; + fillSession(session); } - else { - session->status = AuthSession::RequiresPassword; + } + break; + case AccountTask::STATE_FAILED_GONE: { + data.validity_ = Katabasis::Validity::None; + emit changed(); + if (session) + { + session->status = AuthSession::GoneOrMigrated; + session->auth_server_online = true; + fillSession(session); } - session->auth_server_online = true; - fillSession(session); + } + break; + case AccountTask::STATE_CREATED: + case AccountTask::STATE_WORKING: + case AccountTask::STATE_SUCCEEDED: { + // Not reachable here, as they are not failures. } } m_currentTask.reset(); diff --git a/launcher/minecraft/auth/flows/Yggdrasil.cpp b/launcher/minecraft/auth/flows/Yggdrasil.cpp index 7cea059c..c2935d05 100644 --- a/launcher/minecraft/auth/flows/Yggdrasil.cpp +++ b/launcher/minecraft/auth/flows/Yggdrasil.cpp @@ -255,10 +255,17 @@ void Yggdrasil::processReply() case QNetworkReply::ContentAccessDenied: case QNetworkReply::ContentOperationNotPermittedError: break; + case QNetworkReply::ContentGoneError: { + changeState( + STATE_FAILED_GONE, + tr("The Mojang account no longer exists. It may have been migrated to a Microsoft account.") + ); + } default: - changeState(STATE_FAILED_SOFT, - tr("Authentication operation failed due to a network error: %1 (%2)") - .arg(m_netReply->errorString()).arg(m_netReply->error())); + changeState( + STATE_FAILED_SOFT, + tr("Authentication operation failed due to a network error: %1 (%2)").arg(m_netReply->errorString()).arg(m_netReply->error()) + ); return; } @@ -283,10 +290,10 @@ void Yggdrasil::processReply() } else { - changeState(STATE_FAILED_SOFT, tr("Failed to parse authentication server response " - "JSON response: %1 at offset %2.") - .arg(jsonError.errorString()) - .arg(jsonError.offset)); + changeState( + STATE_FAILED_SOFT, + tr("Failed to parse authentication server response JSON response: %1 at offset %2.").arg(jsonError.errorString()).arg(jsonError.offset) + ); qCritical() << replyData; } return; @@ -301,19 +308,18 @@ void Yggdrasil::processReply() // We were able to parse the server's response. Woo! // Call processError. If a subclass has overridden it then they'll handle their // stuff there. - qDebug() << "The request failed, but the server gave us an error message. " - "Processing error."; + qDebug() << "The request failed, but the server gave us an error message. Processing error."; processError(doc.object()); } else { // The server didn't say anything regarding the error. Give the user an unknown // error. - qDebug() - << "The request failed and the server gave no error message. Unknown error."; - changeState(STATE_FAILED_SOFT, - tr("An unknown error occurred when trying to communicate with the " - "authentication server: %1").arg(m_netReply->errorString())); + qDebug() << "The request failed and the server gave no error message. Unknown error."; + changeState( + STATE_FAILED_SOFT, + tr("An unknown error occurred when trying to communicate with the authentication server: %1").arg(m_netReply->errorString()) + ); } } @@ -325,8 +331,13 @@ void Yggdrasil::processError(QJsonObject responseData) if (errorVal.isString() && errorMessageValue.isString()) { - m_error = std::shared_ptr(new Error{ - errorVal.toString(""), errorMessageValue.toString(""), causeVal.toString("")}); + m_error = std::shared_ptr( + new Error { + errorVal.toString(""), + errorMessageValue.toString(""), + causeVal.toString("") + } + ); changeState(STATE_FAILED_HARD, m_error->m_errorMessageVerbose); } else From 317101430148e3bbc52995aa92d668b8473026d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Sun, 29 Aug 2021 22:55:33 +0200 Subject: [PATCH 18/35] GH-3392 checking for migration status and refresh button in accounts list --- launcher/minecraft/auth/AccountList.cpp | 16 +++++ launcher/minecraft/auth/AccountList.h | 1 + launcher/minecraft/auth/MinecraftAccount.h | 8 +++ launcher/minecraft/auth/flows/AuthContext.cpp | 68 +++++++++++++++++++ launcher/minecraft/auth/flows/AuthContext.h | 4 ++ launcher/pages/global/AccountListPage.cpp | 17 +++++ launcher/pages/global/AccountListPage.h | 1 + launcher/pages/global/AccountListPage.ui | 9 +++ 8 files changed, 124 insertions(+) diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp index 59028b60..76af0ac0 100644 --- a/launcher/minecraft/auth/AccountList.cpp +++ b/launcher/minecraft/auth/AccountList.cpp @@ -205,6 +205,18 @@ QVariant AccountList::data(const QModelIndex &index, int role) const return account->profileName(); } + case MigrationColumn: { + if(account->isMSA()) { + return tr("N/A", "Can Migrate?"); + } + if (account->canMigrate()) { + return tr("Yes", "Can Migrate?"); + } + else { + return tr("No", "Can Migrate?"); + } + } + default: return QVariant(); } @@ -238,6 +250,8 @@ QVariant AccountList::headerData(int section, Qt::Orientation orientation, int r return tr("Account"); case TypeColumn: return tr("Type"); + case MigrationColumn: + return tr("Can Migrate?"); case ProfileNameColumn: return tr("Profile"); default: @@ -251,6 +265,8 @@ QVariant AccountList::headerData(int section, Qt::Orientation orientation, int r return tr("User name of the account."); case TypeColumn: return tr("Type of the account - Mojang or MSA."); + case MigrationColumn: + return tr("Can this account migrate to Microsoft account?"); case ProfileNameColumn: return tr("Name of the Minecraft profile associated with the account."); default: diff --git a/launcher/minecraft/auth/AccountList.h b/launcher/minecraft/auth/AccountList.h index ac3684ee..ed08bb1d 100644 --- a/launcher/minecraft/auth/AccountList.h +++ b/launcher/minecraft/auth/AccountList.h @@ -40,6 +40,7 @@ public: // TODO: Add icon column. NameColumn = 0, ProfileNameColumn, + MigrationColumn, TypeColumn, NUM_COLUMNS diff --git a/launcher/minecraft/auth/MinecraftAccount.h b/launcher/minecraft/auth/MinecraftAccount.h index 72bb6bd4..5b0c1ec7 100644 --- a/launcher/minecraft/auth/MinecraftAccount.h +++ b/launcher/minecraft/auth/MinecraftAccount.h @@ -117,6 +117,14 @@ public: /* queries */ return data.profileName(); } + bool canMigrate() const { + return data.canMigrateToMSA; + } + + bool isMSA() const { + return data.type == AccountType::MSA; + } + QString typeString() const { switch(data.type) { case AccountType::Mojang: { diff --git a/launcher/minecraft/auth/flows/AuthContext.cpp b/launcher/minecraft/auth/flows/AuthContext.cpp index 9ae99453..5d7d858d 100644 --- a/launcher/minecraft/auth/flows/AuthContext.cpp +++ b/launcher/minecraft/auth/flows/AuthContext.cpp @@ -192,6 +192,15 @@ bool getNumber(QJsonValue value, double & out) { return true; } + +bool getBool(QJsonValue value, bool & out) { + if(!value.isBool()) { + return false; + } + out = value.toBool(); + return true; +} + /* { "IssueInstant":"2020-12-07T19:52:08.4463796Z", @@ -693,6 +702,63 @@ void AuthContext::onMinecraftProfileDone(int, QNetworkReply::NetworkError error, changeState(STATE_FAILED_HARD, tr("Minecraft Java profile response could not be parsed")); return; } + + if(m_data->type == AccountType::Mojang) { + doMigrationEligibilityCheck(); + } + else { + doGetSkin(); + } +} + +void AuthContext::doMigrationEligibilityCheck() { + setStage(AuthStage::MigrationEligibility); + changeState(STATE_WORKING, tr("Starting check for migration eligibility")); + + auto url = QUrl("https://api.minecraftservices.com/rollout/v1/msamigration"); + QNetworkRequest request = QNetworkRequest(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8()); + + Requestor *requestor = new Requestor(mgr, m_oauth2, this); + connect(requestor, &Requestor::finished, this, &AuthContext::onMigrationEligibilityCheckDone); + requestor->get(request); +} + +bool parseRolloutResponse(QByteArray & data, bool& result) { + qDebug() << "Parsing Rollout response..."; +#ifndef NDEBUG + qDebug() << data; +#endif + + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if(jsonError.error) { + qWarning() << "Failed to parse response from https://api.minecraftservices.com/rollout/v1/msamigration as JSON: " << jsonError.errorString(); + return false; + } + + auto obj = doc.object(); + QString feature; + if(!getString(obj.value("feature"), feature)) { + qWarning() << "Rollout feature is not a string"; + return false; + } + if(feature != "msamigration") { + qWarning() << "Rollout feature is not what we expected (msamigration), but is instead \"" << feature << "\""; + return false; + } + if(!getBool(obj.value("rollout"), result)) { + qWarning() << "Rollout feature is not a string"; + return false; + } + return true; +} + +void AuthContext::onMigrationEligibilityCheckDone(int, QNetworkReply::NetworkError error, QByteArray data, QList headers) { + if (error == QNetworkReply::NoError) { + parseRolloutResponse(data, m_data->canMigrateToMSA); + } doGetSkin(); } @@ -742,6 +808,8 @@ QString AuthContext::getStateMessage() const { return tr("Logging in with XBox and Mojang services"); case AuthStage::MinecraftProfile: return tr("Getting Minecraft profile"); + case AuthStage::MigrationEligibility: + return tr("Checking for migration eligibility"); case AuthStage::Skin: return tr("Getting Minecraft skin"); case AuthStage::Complete: diff --git a/launcher/minecraft/auth/flows/AuthContext.h b/launcher/minecraft/auth/flows/AuthContext.h index 1d9f8f72..7bf69623 100644 --- a/launcher/minecraft/auth/flows/AuthContext.h +++ b/launcher/minecraft/auth/flows/AuthContext.h @@ -63,6 +63,9 @@ protected: void doMinecraftProfile(); Q_SLOT void onMinecraftProfileDone(int, QNetworkReply::NetworkError, QByteArray, QList); + void doMigrationEligibilityCheck(); + Q_SLOT void onMigrationEligibilityCheckDone(int, QNetworkReply::NetworkError, QByteArray, QList); + void doGetSkin(); Q_SLOT void onSkinDone(int, QNetworkReply::NetworkError, QByteArray, QList); @@ -86,6 +89,7 @@ protected: UserAuth, XboxAuth, MinecraftProfile, + MigrationEligibility, Skin, Complete } m_stage = AuthStage::Initial; diff --git a/launcher/pages/global/AccountListPage.cpp b/launcher/pages/global/AccountListPage.cpp index 45b778de..6bb07b22 100644 --- a/launcher/pages/global/AccountListPage.cpp +++ b/launcher/pages/global/AccountListPage.cpp @@ -153,6 +153,22 @@ void AccountListPage::on_actionRemove_triggered() } } +void AccountListPage::on_actionRefresh_triggered() { + QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); + if (selection.size() > 0) { + QModelIndex selected = selection.first(); + MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value(); + AuthSessionPtr session = std::make_shared(); + auto task = account->refresh(session); + if (task) { + ProgressDialog progDialog(this); + progDialog.execWithTask(task.get()); + // TODO: respond to results of the task + } + } +} + + void AccountListPage::on_actionSetDefault_triggered() { QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); @@ -178,6 +194,7 @@ void AccountListPage::updateButtonStates() ui->actionSetDefault->setEnabled(selection.size() > 0); ui->actionUploadSkin->setEnabled(selection.size() > 0); ui->actionDeleteSkin->setEnabled(selection.size() > 0); + ui->actionRefresh->setEnabled(selection.size() > 0); if(m_accounts->activeAccount().get() == nullptr) { ui->actionNoDefault->setEnabled(false); diff --git a/launcher/pages/global/AccountListPage.h b/launcher/pages/global/AccountListPage.h index 24bb96da..4474802e 100644 --- a/launcher/pages/global/AccountListPage.h +++ b/launcher/pages/global/AccountListPage.h @@ -63,6 +63,7 @@ public slots: void on_actionAddMojang_triggered(); void on_actionAddMicrosoft_triggered(); void on_actionRemove_triggered(); + void on_actionRefresh_triggered(); void on_actionSetDefault_triggered(); void on_actionNoDefault_triggered(); void on_actionUploadSkin_triggered(); diff --git a/launcher/pages/global/AccountListPage.ui b/launcher/pages/global/AccountListPage.ui index 887c3d48..8af23a2f 100644 --- a/launcher/pages/global/AccountListPage.ui +++ b/launcher/pages/global/AccountListPage.ui @@ -54,6 +54,7 @@ + @@ -102,6 +103,14 @@ Add Microsoft
+ + + Refresh + + + Refresh the account tokens + + From 23442442d86862536acab5a7deca28c1c703b11e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Tue, 31 Aug 2021 00:55:56 +0200 Subject: [PATCH 19/35] GH-3392 fix a bunch of bugs and implement STS error states --- launcher/CMakeLists.txt | 2 + launcher/minecraft/auth/flows/AuthContext.cpp | 257 ++++++++++++------ launcher/minecraft/auth/flows/AuthContext.h | 27 +- launcher/minecraft/auth/flows/AuthRequest.cpp | 121 +++++++++ launcher/minecraft/auth/flows/AuthRequest.h | 65 +++++ launcher/minecraft/auth/flows/MSAHelper.txt | 51 ---- 6 files changed, 377 insertions(+), 146 deletions(-) create mode 100644 launcher/minecraft/auth/flows/AuthRequest.cpp create mode 100644 launcher/minecraft/auth/flows/AuthRequest.h delete mode 100644 launcher/minecraft/auth/flows/MSAHelper.txt diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 81740adb..7a5e4173 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -215,6 +215,8 @@ set(MINECRAFT_SOURCES minecraft/auth/MinecraftAccount.cpp minecraft/auth/flows/AuthContext.h minecraft/auth/flows/AuthContext.cpp + minecraft/auth/flows/AuthRequest.h + minecraft/auth/flows/AuthRequest.cpp minecraft/auth/flows/MSAInteractive.h minecraft/auth/flows/MSAInteractive.cpp diff --git a/launcher/minecraft/auth/flows/AuthContext.cpp b/launcher/minecraft/auth/flows/AuthContext.cpp index 5d7d858d..1203dc5f 100644 --- a/launcher/minecraft/auth/flows/AuthContext.cpp +++ b/launcher/minecraft/auth/flows/AuthContext.cpp @@ -16,20 +16,21 @@ #include "AuthContext.h" #include "katabasis/Globals.h" -#include "katabasis/Requestor.h" +#include "AuthRequest.h" #ifdef EMBED_SECRETS #include "Secrets.h" #endif +#include "Env.h" + using OAuth2 = Katabasis::OAuth2; -using Requestor = Katabasis::Requestor; +using Requestor = AuthRequest; using Activity = Katabasis::Activity; AuthContext::AuthContext(AccountData * data, QObject *parent) : AccountTask(data, parent) { - mgr = new QNetworkAccessManager(this); } void AuthContext::beginActivity(Activity activity) { @@ -63,7 +64,7 @@ void AuthContext::initMSA() { opts.accessTokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"; opts.listenerPorts = {28562, 28563, 28564, 28565, 28566}; - m_oauth2 = new OAuth2(opts, m_data->msaToken, this, mgr); + m_oauth2 = new OAuth2(opts, m_data->msaToken, this, &ENV.qnam()); m_oauth2->setGrantFlow(Katabasis::OAuth2::GrantFlowDevice); connect(m_oauth2, &OAuth2::linkingFailed, this, &AuthContext::onOAuthLinkingFailed); @@ -161,7 +162,7 @@ void AuthContext::doUserAuth() { 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 Requestor(mgr, m_oauth2, this); + auto *requestor = new Requestor(this); connect(requestor, &Requestor::finished, this, &AuthContext::onUserAuthDone); requestor->post(request, xbox_auth_data.toUtf8()); qDebug() << "First layer of XBox auth ... commencing."; @@ -192,6 +193,13 @@ bool getNumber(QJsonValue value, double & out) { return true; } +bool getNumber(QJsonValue value, int64_t & out) { + if(!value.isDouble()) { + return false; + } + out = (int64_t) value.toDouble(); + return true; +} bool getBool(QJsonValue value, bool & out) { if(!value.isBool()) { @@ -292,7 +300,6 @@ bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output, const char } void AuthContext::onUserAuthDone( - int requestId, QNetworkReply::NetworkError error, QByteArray replyData, QList headers @@ -349,35 +356,58 @@ void AuthContext::doSTSAuthMinecraft() { 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, m_oauth2, this); + Requestor *requestor = new Requestor(this); connect(requestor, &Requestor::finished, this, &AuthContext::onSTSAuthMinecraftDone); requestor->post(request, xbox_auth_data.toUtf8()); qDebug() << "Getting Minecraft services STS token..."; } +void AuthContext::processSTSError(QNetworkReply::NetworkError error, QByteArray data, QList headers) { + if(error == QNetworkReply::AuthenticationRequiredError) { + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if(jsonError.error) { + qWarning() << "Cannot parse error XSTS response as JSON: " << jsonError.errorString(); + return; + } + + int64_t errorCode = -1; + auto obj = doc.object(); + if(!getNumber(obj.value("XErr"), errorCode)) { + qWarning() << "XErr is not a number"; + return; + } + stsErrors.insert(errorCode); + stsFailed = true; + } +} + + void AuthContext::onSTSAuthMinecraftDone( - int requestId, QNetworkReply::NetworkError error, QByteArray replyData, QList headers ) { +#ifndef NDEBUG + qDebug() << replyData; +#endif if (error != QNetworkReply::NoError) { qWarning() << "Reply error:" << error; - m_requestsDone ++; + processSTSError(error, replyData, headers); + failResult(m_mcAuthSucceeded); return; } Katabasis::Token temp; if(!parseXTokenResponse(replyData, temp, "STSAuthMinecraft")) { qWarning() << "Could not parse authorization response for access to mojang services..."; - m_requestsDone ++; + failResult(m_mcAuthSucceeded); return; } if(temp.extra["uhs"] != m_data->userToken.extra["uhs"]) { qWarning() << "Server has changed user hash in the reply... something is wrong. ABORTING"; - qDebug() << replyData; - m_requestsDone ++; + failResult(m_mcAuthSucceeded); return; } m_data->mojangservicesToken = temp; @@ -385,61 +415,6 @@ void AuthContext::onSTSAuthMinecraftDone( doMinecraftAuth(); } -void AuthContext::doSTSAuthGeneric() { - QString xbox_auth_template = R"XXX( -{ - "Properties": { - "SandboxId": "RETAIL", - "UserTokens": [ - "%1" - ] - }, - "RelyingParty": "http://xboxlive.com", - "TokenType": "JWT" -} -)XXX"; - 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, m_oauth2, this); - connect(requestor, &Requestor::finished, this, &AuthContext::onSTSAuthGenericDone); - requestor->post(request, xbox_auth_data.toUtf8()); - qDebug() << "Getting generic STS token..."; -} - -void AuthContext::onSTSAuthGenericDone( - int requestId, - QNetworkReply::NetworkError error, - QByteArray replyData, - QList headers -) { - if (error != QNetworkReply::NoError) { - qWarning() << "Reply error:" << error; - m_requestsDone ++; - return; - } - - Katabasis::Token temp; - if(!parseXTokenResponse(replyData, temp, "STSAuthGaneric")) { - qWarning() << "Could not parse authorization response for access to xbox API..."; - m_requestsDone ++; - return; - } - - if(temp.extra["uhs"] != m_data->userToken.extra["uhs"]) { - qWarning() << "Server has changed user hash in the reply... something is wrong. ABORTING"; - qDebug() << replyData; - m_requestsDone ++; - return; - } - m_data->xboxApiToken = temp; - - doXBoxProfile(); -} - - void AuthContext::doMinecraftAuth() { QString mc_auth_template = R"XXX( { @@ -451,7 +426,7 @@ void AuthContext::doMinecraftAuth() { 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, m_oauth2, this); + Requestor *requestor = new Requestor(this); connect(requestor, &Requestor::finished, this, &AuthContext::onMinecraftAuthDone); requestor->post(request, data.toUtf8()); qDebug() << "Getting Minecraft access token..."; @@ -498,18 +473,16 @@ bool parseMojangResponse(QByteArray & data, Katabasis::Token &output) { } void AuthContext::onMinecraftAuthDone( - int requestId, QNetworkReply::NetworkError error, QByteArray replyData, QList headers ) { - m_requestsDone ++; - if (error != QNetworkReply::NoError) { qWarning() << "Reply error:" << error; #ifndef NDEBUG qDebug() << replyData; #endif + failResult(m_mcAuthSucceeded); return; } @@ -518,11 +491,67 @@ void AuthContext::onMinecraftAuthDone( #ifndef NDEBUG qDebug() << replyData; #endif + failResult(m_mcAuthSucceeded); return; } - m_mcAuthSucceeded = true; - checkResult(); + succeedResult(m_mcAuthSucceeded); +} + +void AuthContext::doSTSAuthGeneric() { + QString xbox_auth_template = R"XXX( +{ + "Properties": { + "SandboxId": "RETAIL", + "UserTokens": [ + "%1" + ] + }, + "RelyingParty": "http://xboxlive.com", + "TokenType": "JWT" +} +)XXX"; + 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(this); + connect(requestor, &Requestor::finished, this, &AuthContext::onSTSAuthGenericDone); + requestor->post(request, xbox_auth_data.toUtf8()); + qDebug() << "Getting generic STS token..."; +} + +void AuthContext::onSTSAuthGenericDone( + QNetworkReply::NetworkError error, + QByteArray replyData, + QList headers +) { +#ifndef NDEBUG + qDebug() << replyData; +#endif + if (error != QNetworkReply::NoError) { + qWarning() << "Reply error:" << error; + processSTSError(error, replyData, headers); + failResult(m_xboxProfileSucceeded); + return; + } + + Katabasis::Token temp; + if(!parseXTokenResponse(replyData, temp, "STSAuthGaneric")) { + qWarning() << "Could not parse authorization response for access to xbox API..."; + failResult(m_xboxProfileSucceeded); + return; + } + + if(temp.extra["uhs"] != m_data->userToken.extra["uhs"]) { + qWarning() << "Server has changed user hash in the reply... something is wrong. ABORTING"; + failResult(m_xboxProfileSucceeded); + return; + } + m_data->xboxApiToken = temp; + + doXBoxProfile(); } void AuthContext::doXBoxProfile() { @@ -543,25 +572,23 @@ void AuthContext::doXBoxProfile() { request.setRawHeader("Accept", "application/json"); request.setRawHeader("x-xbl-contract-version", "3"); 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 *requestor = new Requestor(this); connect(requestor, &Requestor::finished, this, &AuthContext::onXBoxProfileDone); requestor->get(request); qDebug() << "Getting Xbox profile..."; } void AuthContext::onXBoxProfileDone( - int requestId, QNetworkReply::NetworkError error, QByteArray replyData, QList headers ) { - m_requestsDone ++; - if (error != QNetworkReply::NoError) { qWarning() << "Reply error:" << error; #ifndef NDEBUG qDebug() << replyData; #endif + failResult(m_xboxProfileSucceeded); return; } @@ -569,7 +596,18 @@ void AuthContext::onXBoxProfileDone( qDebug() << "XBox profile: " << replyData; #endif - m_xboxProfileSucceeded = true; + succeedResult(m_xboxProfileSucceeded); +} + +void AuthContext::succeedResult(bool& flag) { + m_requestsDone ++; + flag = true; + checkResult(); +} + +void AuthContext::failResult(bool& flag) { + m_requestsDone ++; + flag = false; checkResult(); } @@ -584,7 +622,42 @@ void AuthContext::checkResult() { } else { finishActivity(); - changeState(STATE_FAILED_HARD, tr("XBox and/or Mojang authentication steps did not succeed")); + if(stsFailed) { + if(stsErrors.contains(2148916233)) { + changeState( + STATE_FAILED_HARD, + tr("This Microsoft account does not have an XBox Live profile. Buy the game on %1 first.") + .arg("minecraft.net") + ); + } + else if (stsErrors.contains(2148916235)){ + // NOTE: this is the Grulovia error + changeState( + STATE_FAILED_HARD, + tr("XBox Live is not available in your country. You've been blocked.") + ); + } + else if (stsErrors.contains(2148916238)){ + changeState( + STATE_FAILED_HARD, + tr("This Microsoft account is underaged and is not linked to a family.\n\nPlease set up your account according to %1.") + .arg("help.minecraft.net") + ); + } + else { + QStringList errorList; + for(auto & error: stsErrors) { + errorList.append(QString::number(error)); + } + changeState( + STATE_FAILED_HARD, + tr("XSTS authentication ended with unrecognized error(s):\n\n%1").arg(errorList.join("\n")) + ); + } + } + else { + changeState(STATE_FAILED_HARD, tr("XBox and/or Mojang authentication steps did not succeed")); + } } } @@ -678,13 +751,19 @@ void AuthContext::doMinecraftProfile() { // request.setRawHeader("Accept", "application/json"); request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8()); - Requestor *requestor = new Requestor(mgr, m_oauth2, this); + Requestor *requestor = new Requestor(this); connect(requestor, &Requestor::finished, this, &AuthContext::onMinecraftProfileDone); requestor->get(request); } -void AuthContext::onMinecraftProfileDone(int, QNetworkReply::NetworkError error, QByteArray data, QList headers) { +void AuthContext::onMinecraftProfileDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers +) { +#ifndef NDEBUG qDebug() << data; +#endif if (error == QNetworkReply::ContentNotFoundError) { m_data->minecraftProfile = MinecraftProfile(); finishActivity(); @@ -720,7 +799,7 @@ void AuthContext::doMigrationEligibilityCheck() { request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8()); - Requestor *requestor = new Requestor(mgr, m_oauth2, this); + Requestor *requestor = new Requestor(this); connect(requestor, &Requestor::finished, this, &AuthContext::onMigrationEligibilityCheckDone); requestor->get(request); } @@ -755,7 +834,11 @@ bool parseRolloutResponse(QByteArray & data, bool& result) { return true; } -void AuthContext::onMigrationEligibilityCheckDone(int, QNetworkReply::NetworkError error, QByteArray data, QList headers) { +void AuthContext::onMigrationEligibilityCheckDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers +) { if (error == QNetworkReply::NoError) { parseRolloutResponse(data, m_data->canMigrateToMSA); } @@ -768,12 +851,16 @@ void AuthContext::doGetSkin() { auto url = QUrl(m_data->minecraftProfile.skin.url); QNetworkRequest request = QNetworkRequest(url); - Requestor *requestor = new Requestor(mgr, m_oauth2, this); + Requestor *requestor = new Requestor(this); connect(requestor, &Requestor::finished, this, &AuthContext::onSkinDone); requestor->get(request); } -void AuthContext::onSkinDone(int, QNetworkReply::NetworkError error, QByteArray data, QList) { +void AuthContext::onSkinDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList +) { if (error == QNetworkReply::NoError) { m_data->minecraftProfile.skin.data = data; } diff --git a/launcher/minecraft/auth/flows/AuthContext.h b/launcher/minecraft/auth/flows/AuthContext.h index 7bf69623..dc7552ac 100644 --- a/launcher/minecraft/auth/flows/AuthContext.h +++ b/launcher/minecraft/auth/flows/AuthContext.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -48,27 +49,31 @@ protected: void initMojang(); void doUserAuth(); - Q_SLOT void onUserAuthDone(int, QNetworkReply::NetworkError, QByteArray, QList); + Q_SLOT void onUserAuthDone(QNetworkReply::NetworkError, QByteArray, QList); + + void processSTSError(QNetworkReply::NetworkError, QByteArray, QList); void doSTSAuthMinecraft(); - Q_SLOT void onSTSAuthMinecraftDone(int, QNetworkReply::NetworkError, QByteArray, QList); + Q_SLOT void onSTSAuthMinecraftDone(QNetworkReply::NetworkError, QByteArray, QList); void doMinecraftAuth(); - Q_SLOT void onMinecraftAuthDone(int, QNetworkReply::NetworkError, QByteArray, QList); + Q_SLOT void onMinecraftAuthDone(QNetworkReply::NetworkError, QByteArray, QList); void doSTSAuthGeneric(); - Q_SLOT void onSTSAuthGenericDone(int, QNetworkReply::NetworkError, QByteArray, QList); + Q_SLOT void onSTSAuthGenericDone(QNetworkReply::NetworkError, QByteArray, QList); void doXBoxProfile(); - Q_SLOT void onXBoxProfileDone(int, QNetworkReply::NetworkError, QByteArray, QList); + Q_SLOT void onXBoxProfileDone(QNetworkReply::NetworkError, QByteArray, QList); void doMinecraftProfile(); - Q_SLOT void onMinecraftProfileDone(int, QNetworkReply::NetworkError, QByteArray, QList); + Q_SLOT void onMinecraftProfileDone(QNetworkReply::NetworkError, QByteArray, QList); void doMigrationEligibilityCheck(); - Q_SLOT void onMigrationEligibilityCheckDone(int, QNetworkReply::NetworkError, QByteArray, QList); + Q_SLOT void onMigrationEligibilityCheckDone(QNetworkReply::NetworkError, QByteArray, QList); void doGetSkin(); - Q_SLOT void onSkinDone(int, QNetworkReply::NetworkError, QByteArray, QList); + Q_SLOT void onSkinDone(QNetworkReply::NetworkError, QByteArray, QList); + void failResult(bool & flag); + void succeedResult(bool & flag); void checkResult(); protected: @@ -83,6 +88,10 @@ protected: int m_requestsDone = 0; bool m_xboxProfileSucceeded = false; bool m_mcAuthSucceeded = false; + + QSet stsErrors; + bool stsFailed = false; + Katabasis::Activity m_activity = Katabasis::Activity::Idle; enum class AuthStage { Initial, @@ -95,6 +104,4 @@ protected: } m_stage = AuthStage::Initial; void setStage(AuthStage stage); - - QNetworkAccessManager *mgr = nullptr; }; diff --git a/launcher/minecraft/auth/flows/AuthRequest.cpp b/launcher/minecraft/auth/flows/AuthRequest.cpp new file mode 100644 index 00000000..77558fd3 --- /dev/null +++ b/launcher/minecraft/auth/flows/AuthRequest.cpp @@ -0,0 +1,121 @@ +#include + +#include +#include +#include +#include + +#include "AuthRequest.h" +#include "katabasis/Globals.h" +#include "Env.h" + +AuthRequest::AuthRequest(QObject *parent): QObject(parent) { +} + +AuthRequest::~AuthRequest() { +} + +void AuthRequest::get(const QNetworkRequest &req, int timeout/* = 60*1000*/) { + setup(req, QNetworkAccessManager::GetOperation); + reply_ = ENV.qnam().get(request_); + status_ = Requesting; + timedReplies_.add(new Katabasis::Reply(reply_, timeout)); + connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError))); + connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished())); + connect(reply_, &QNetworkReply::sslErrors, this, &AuthRequest::onSslErrors); +} + +void AuthRequest::post(const QNetworkRequest &req, const QByteArray &data, int timeout/* = 60*1000*/) { + setup(req, QNetworkAccessManager::PostOperation); + data_ = data; + status_ = Requesting; + reply_ = ENV.qnam().post(request_, data_); + timedReplies_.add(new Katabasis::Reply(reply_, timeout)); + connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError))); + connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished())); + connect(reply_, &QNetworkReply::sslErrors, this, &AuthRequest::onSslErrors); + connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64))); +} + +void AuthRequest::onRequestFinished() { + if (status_ == Idle) { + return; + } + if (reply_ != qobject_cast(sender())) { + return; + } + finish(); +} + +void AuthRequest::onRequestError(QNetworkReply::NetworkError error) { + qWarning() << "AuthRequest::onRequestError: Error" << (int)error; + if (status_ == Idle) { + return; + } + if (reply_ != qobject_cast(sender())) { + return; + } + qWarning() << "AuthRequest::onRequestError: Error string: " << reply_->errorString(); + int httpStatus = reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + qWarning() << "AuthRequest::onRequestError: HTTP status" << httpStatus << reply_->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); + error_ = error; + + // QTimer::singleShot(10, this, SLOT(finish())); +} + +void AuthRequest::onSslErrors(QList errors) { + int i = 1; + for (auto error : errors) { + qCritical() << "LOGIN SSL Error #" << i << " : " << error.errorString(); + auto cert = error.certificate(); + qCritical() << "Certificate in question:\n" << cert.toText(); + i++; + } +} + +void AuthRequest::onUploadProgress(qint64 uploaded, qint64 total) { + if (status_ == Idle) { + qWarning() << "AuthRequest::onUploadProgress: No pending request"; + return; + } + if (reply_ != qobject_cast(sender())) { + return; + } + // Restart timeout because request in progress + Katabasis::Reply *o2Reply = timedReplies_.find(reply_); + if(o2Reply) { + o2Reply->start(); + } + emit uploadProgress(uploaded, total); +} + +void AuthRequest::setup(const QNetworkRequest &req, QNetworkAccessManager::Operation operation, const QByteArray &verb) { + request_ = req; + operation_ = operation; + url_ = req.url(); + + QUrl url = url_; + request_.setUrl(url); + + if (!verb.isEmpty()) { + request_.setRawHeader(Katabasis::HTTP_HTTP_HEADER, verb); + } + + status_ = Requesting; + error_ = QNetworkReply::NoError; +} + +void AuthRequest::finish() { + QByteArray data; + if (status_ == Idle) { + qWarning() << "AuthRequest::finish: No pending request"; + return; + } + data = reply_->readAll(); + status_ = Idle; + timedReplies_.remove(reply_); + reply_->disconnect(this); + reply_->deleteLater(); + QList headers = reply_->rawHeaderPairs(); + emit finished(error_, data, headers); +} diff --git a/launcher/minecraft/auth/flows/AuthRequest.h b/launcher/minecraft/auth/flows/AuthRequest.h new file mode 100644 index 00000000..6a45a0bd --- /dev/null +++ b/launcher/minecraft/auth/flows/AuthRequest.h @@ -0,0 +1,65 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +#include "katabasis/Reply.h" + +/// Makes authentication requests. +class AuthRequest: public QObject { + Q_OBJECT + +public: + explicit AuthRequest(QObject *parent = 0); + ~AuthRequest(); + +public slots: + void get(const QNetworkRequest &req, int timeout = 60*1000); + void post(const QNetworkRequest &req, const QByteArray &data, int timeout = 60*1000); + + +signals: + + /// Emitted when a request has been completed or failed. + void finished(QNetworkReply::NetworkError error, QByteArray data, QList headers); + + /// Emitted when an upload has progressed. + void uploadProgress(qint64 bytesSent, qint64 bytesTotal); + +protected slots: + + /// Handle request finished. + void onRequestFinished(); + + /// Handle request error. + void onRequestError(QNetworkReply::NetworkError error); + + /// Handle ssl errors. + void onSslErrors(QList errors); + + /// Finish the request, emit finished() signal. + void finish(); + + /// Handle upload progress. + void onUploadProgress(qint64 uploaded, qint64 total); + +protected: + void setup(const QNetworkRequest &request, QNetworkAccessManager::Operation operation, const QByteArray &verb = QByteArray()); + + enum Status { + Idle, Requesting, ReRequesting + }; + + QNetworkRequest request_; + QByteArray data_; + QNetworkReply *reply_; + Status status_; + QNetworkAccessManager::Operation operation_; + QUrl url_; + Katabasis::ReplyList timedReplies_; + QNetworkReply::NetworkError error_; +}; diff --git a/launcher/minecraft/auth/flows/MSAHelper.txt b/launcher/minecraft/auth/flows/MSAHelper.txt deleted file mode 100644 index dfaec374..00000000 --- a/launcher/minecraft/auth/flows/MSAHelper.txt +++ /dev/null @@ -1,51 +0,0 @@ -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_; -}; From 92895f11d1461f48d9fc23eaead83012f9624f7e Mon Sep 17 00:00:00 2001 From: StaticRocket <35777938+StaticRocket@users.noreply.github.com> Date: Mon, 30 Aug 2021 23:33:22 -0400 Subject: [PATCH 20/35] Add custom-commands to OSX icon theme --- launcher/resources/OSX/OSX.qrc | 1 + .../OSX/scalable/custom-commands.svg | 71 +++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 launcher/resources/OSX/scalable/custom-commands.svg diff --git a/launcher/resources/OSX/OSX.qrc b/launcher/resources/OSX/OSX.qrc index 19fd4b6a..1a6ec0dc 100644 --- a/launcher/resources/OSX/OSX.qrc +++ b/launcher/resources/OSX/OSX.qrc @@ -9,6 +9,7 @@ scalable/checkupdate.svg scalable/copy.svg scalable/coremods.svg + scalable/custom-commands.svg scalable/externaltools.svg scalable/help.svg scalable/instance-settings.svg diff --git a/launcher/resources/OSX/scalable/custom-commands.svg b/launcher/resources/OSX/scalable/custom-commands.svg new file mode 100644 index 00000000..e663452b --- /dev/null +++ b/launcher/resources/OSX/scalable/custom-commands.svg @@ -0,0 +1,71 @@ + +image/svg+xml From 62ecb3e58d81be4ea931497cefc6f30c204a5ad4 Mon Sep 17 00:00:00 2001 From: StaticRocket <35777938+StaticRocket@users.noreply.github.com> Date: Mon, 30 Aug 2021 23:33:41 -0400 Subject: [PATCH 21/35] Add custom-commands to iOS icon theme --- launcher/resources/iOS/iOS.qrc | 1 + .../iOS/scalable/custom-commands.svg | 63 +++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 launcher/resources/iOS/scalable/custom-commands.svg diff --git a/launcher/resources/iOS/iOS.qrc b/launcher/resources/iOS/iOS.qrc index 511e390b..75c88bb0 100644 --- a/launcher/resources/iOS/iOS.qrc +++ b/launcher/resources/iOS/iOS.qrc @@ -9,6 +9,7 @@ scalable/checkupdate.svg scalable/copy.svg scalable/coremods.svg + scalable/custom-commands.svg scalable/externaltools.svg scalable/help.svg scalable/instance-settings.svg diff --git a/launcher/resources/iOS/scalable/custom-commands.svg b/launcher/resources/iOS/scalable/custom-commands.svg new file mode 100644 index 00000000..f44e2bfe --- /dev/null +++ b/launcher/resources/iOS/scalable/custom-commands.svg @@ -0,0 +1,63 @@ + +image/svg+xml + + + + + From acbca16013874b246584b2fb0f2a434af6844cae Mon Sep 17 00:00:00 2001 From: StaticRocket <35777938+StaticRocket@users.noreply.github.com> Date: Mon, 30 Aug 2021 23:33:56 -0400 Subject: [PATCH 22/35] Add custom-commands to pe_blue icon theme --- launcher/resources/pe_blue/pe_blue.qrc | 1 + .../pe_blue/scalable/custom-commands.svg | 336 ++++++++++++++++++ 2 files changed, 337 insertions(+) create mode 100644 launcher/resources/pe_blue/scalable/custom-commands.svg diff --git a/launcher/resources/pe_blue/pe_blue.qrc b/launcher/resources/pe_blue/pe_blue.qrc index 98445d88..2a685979 100644 --- a/launcher/resources/pe_blue/pe_blue.qrc +++ b/launcher/resources/pe_blue/pe_blue.qrc @@ -9,6 +9,7 @@ scalable/checkupdate.svg scalable/copy.svg scalable/coremods.svg + scalable/custom-commands.svg scalable/externaltools.svg scalable/help.svg scalable/instance-settings.svg diff --git a/launcher/resources/pe_blue/scalable/custom-commands.svg b/launcher/resources/pe_blue/scalable/custom-commands.svg new file mode 100644 index 00000000..be76ece9 --- /dev/null +++ b/launcher/resources/pe_blue/scalable/custom-commands.svg @@ -0,0 +1,336 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 51cdb8c790bc07058a2311bbed487b7803694503 Mon Sep 17 00:00:00 2001 From: StaticRocket <35777938+StaticRocket@users.noreply.github.com> Date: Mon, 30 Aug 2021 23:34:21 -0400 Subject: [PATCH 23/35] Add custom-commands to pe_colored icon theme --- launcher/resources/pe_colored/pe_colored.qrc | 1 + .../pe_colored/scalable/custom-commands.svg | 345 ++++++++++++++++++ 2 files changed, 346 insertions(+) create mode 100644 launcher/resources/pe_colored/scalable/custom-commands.svg diff --git a/launcher/resources/pe_colored/pe_colored.qrc b/launcher/resources/pe_colored/pe_colored.qrc index fbaaf9e4..4472f5e0 100644 --- a/launcher/resources/pe_colored/pe_colored.qrc +++ b/launcher/resources/pe_colored/pe_colored.qrc @@ -9,6 +9,7 @@ scalable/checkupdate.svg scalable/copy.svg scalable/coremods.svg + scalable/custom-commands.svg scalable/externaltools.svg scalable/help.svg scalable/instance-settings.svg diff --git a/launcher/resources/pe_colored/scalable/custom-commands.svg b/launcher/resources/pe_colored/scalable/custom-commands.svg new file mode 100644 index 00000000..44dd1992 --- /dev/null +++ b/launcher/resources/pe_colored/scalable/custom-commands.svg @@ -0,0 +1,345 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From b47d986f22f249c808d3eda1b058dafba3d8e1f5 Mon Sep 17 00:00:00 2001 From: StaticRocket <35777938+StaticRocket@users.noreply.github.com> Date: Mon, 30 Aug 2021 23:34:59 -0400 Subject: [PATCH 24/35] Add custom-commands to pe_dark icon theme --- launcher/resources/pe_dark/pe_dark.qrc | 1 + .../pe_dark/scalable/custom-commands.svg | 335 ++++++++++++++++++ 2 files changed, 336 insertions(+) create mode 100644 launcher/resources/pe_dark/scalable/custom-commands.svg diff --git a/launcher/resources/pe_dark/pe_dark.qrc b/launcher/resources/pe_dark/pe_dark.qrc index a57b6a14..a138abc4 100644 --- a/launcher/resources/pe_dark/pe_dark.qrc +++ b/launcher/resources/pe_dark/pe_dark.qrc @@ -9,6 +9,7 @@ scalable/checkupdate.svg scalable/copy.svg scalable/coremods.svg + scalable/custom-commands.svg scalable/externaltools.svg scalable/help.svg scalable/instance-settings.svg diff --git a/launcher/resources/pe_dark/scalable/custom-commands.svg b/launcher/resources/pe_dark/scalable/custom-commands.svg new file mode 100644 index 00000000..42185e37 --- /dev/null +++ b/launcher/resources/pe_dark/scalable/custom-commands.svg @@ -0,0 +1,335 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 4f7aad0f8de515a0df3a8ab9a399d279c9d70358 Mon Sep 17 00:00:00 2001 From: StaticRocket <35777938+StaticRocket@users.noreply.github.com> Date: Mon, 30 Aug 2021 23:35:16 -0400 Subject: [PATCH 25/35] Add custom-commands to pe_light icon theme --- launcher/resources/pe_light/pe_light.qrc | 1 + .../pe_light/scalable/custom-commands.svg | 336 ++++++++++++++++++ 2 files changed, 337 insertions(+) create mode 100644 launcher/resources/pe_light/scalable/custom-commands.svg diff --git a/launcher/resources/pe_light/pe_light.qrc b/launcher/resources/pe_light/pe_light.qrc index 6d77c835..f518b650 100644 --- a/launcher/resources/pe_light/pe_light.qrc +++ b/launcher/resources/pe_light/pe_light.qrc @@ -9,6 +9,7 @@ scalable/checkupdate.svg scalable/copy.svg scalable/coremods.svg + scalable/custom-commands.svg scalable/externaltools.svg scalable/help.svg scalable/instance-settings.svg diff --git a/launcher/resources/pe_light/scalable/custom-commands.svg b/launcher/resources/pe_light/scalable/custom-commands.svg new file mode 100644 index 00000000..b3dfe12a --- /dev/null +++ b/launcher/resources/pe_light/scalable/custom-commands.svg @@ -0,0 +1,336 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From cd87029e6fc0c8d8b25c9162812ae066066ad11a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Tue, 31 Aug 2021 18:55:56 +0200 Subject: [PATCH 26/35] NOISSUE add style plugins to packaging if present --- launcher/CMakeLists.txt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 7a5e4173..7241b89d 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -985,6 +985,14 @@ if(INSTALL_BUNDLE STREQUAL "full") COMPONENT Runtime REGEX "minimal|linuxfb|offscreen" EXCLUDE ) + # Style plugins + if(EXISTS "${QT_PLUGINS_DIR}/styles") + install( + DIRECTORY "${QT_PLUGINS_DIR}/styles" + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + ) + endif() else() # Image formats install( @@ -1016,6 +1024,17 @@ if(INSTALL_BUNDLE STREQUAL "full") REGEX "_debug\\." EXCLUDE REGEX "\\.dSYM" EXCLUDE ) + # Style plugins + if(EXISTS "${QT_PLUGINS_DIR}/styles") + install( + DIRECTORY "${QT_PLUGINS_DIR}/styles" + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + REGEX "d\\." EXCLUDE + REGEX "_debug\\." EXCLUDE + REGEX "\\.dSYM" EXCLUDE + ) + endif() endif() configure_file( "${CMAKE_CURRENT_SOURCE_DIR}/install_prereqs.cmake.in" From 938f896bfa7775cf7dcf1ee6883572f514f53993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Sat, 4 Sep 2021 21:27:09 +0200 Subject: [PATCH 27/35] GH-4014 change updater to recognize new Qt 5.15.2 builds --- CMakeLists.txt | 2 +- buildconfig/BuildConfig.cpp.in | 4 +- buildconfig/BuildConfig.h | 2 +- launcher/MultiMC.cpp | 45 ++++++++++++++++++- launcher/minecraft/auth/flows/AuthContext.cpp | 2 +- launcher/pages/global/MultiMCPage.cpp | 3 +- launcher/updater/DownloadTask.cpp | 2 +- launcher/updater/UpdateChecker.cpp | 40 ++++++++++++----- launcher/updater/UpdateChecker.h | 4 +- libraries/systeminfo/include/sys.h | 14 ++++++ libraries/systeminfo/src/sys_apple.cpp | 22 ++++++++- libraries/systeminfo/src/sys_unix.cpp | 23 +++++++++- libraries/systeminfo/src/sys_win32.cpp | 4 ++ 13 files changed, 141 insertions(+), 26 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index acc777fc..84c4a180 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -64,7 +64,7 @@ set(MultiMC_VERSION_BUILD -1 CACHE STRING "Build number. -1 for no build number. set(MultiMC_BUILD_PLATFORM "" CACHE STRING "A short string identifying the platform that this build was built for. Only used by the notification system and to display in the about dialog.") # Channel list URL -set(MultiMC_CHANLIST_URL "" CACHE STRING "URL for the channel list.") +set(MultiMC_UPDATER_BASE "" CACHE STRING "Base URL for the updater.") # Notification URL set(MultiMC_NOTIFICATION_URL "" CACHE STRING "URL for checking for notifications.") diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in index 60d417a6..d9f4d1f1 100644 --- a/buildconfig/BuildConfig.cpp.in +++ b/buildconfig/BuildConfig.cpp.in @@ -12,14 +12,14 @@ Config::Config() VERSION_BUILD = @MultiMC_VERSION_BUILD@; BUILD_PLATFORM = "@MultiMC_BUILD_PLATFORM@"; - CHANLIST_URL = "@MultiMC_CHANLIST_URL@"; + UPDATER_BASE = "@MultiMC_UPDATER_BASE@"; ANALYTICS_ID = "@MultiMC_ANALYTICS_ID@"; NOTIFICATION_URL = "@MultiMC_NOTIFICATION_URL@"; FULL_VERSION_STR = "@MultiMC_VERSION_MAJOR@.@MultiMC_VERSION_MINOR@.@MultiMC_VERSION_BUILD@"; GIT_COMMIT = "@MultiMC_GIT_COMMIT@"; GIT_REFSPEC = "@MultiMC_GIT_REFSPEC@"; - if(GIT_REFSPEC.startsWith("refs/heads/") && !CHANLIST_URL.isEmpty() && VERSION_BUILD >= 0) + if(GIT_REFSPEC.startsWith("refs/heads/") && !UPDATER_BASE.isEmpty() && !BUILD_PLATFORM.isEmpty() && VERSION_BUILD >= 0) { VERSION_CHANNEL = GIT_REFSPEC; VERSION_CHANNEL.remove("refs/heads/"); diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index de7d4b49..6a35d1b3 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -29,7 +29,7 @@ public: QString BUILD_PLATFORM; /// URL for the updater's channel - QString CHANLIST_URL; + QString UPDATER_BASE; /// User-Agent to use. QString USER_AGENT = "MultiMC/5.0"; diff --git a/launcher/MultiMC.cpp b/launcher/MultiMC.cpp index 5961a45d..69ba4ac5 100644 --- a/launcher/MultiMC.cpp +++ b/launcher/MultiMC.cpp @@ -91,7 +91,8 @@ using namespace Commandline; "This usually fixes the problem and you can move the application elsewhere afterwards.\n"\ "\n" -static void appDebugOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg) +namespace { +void appDebugOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg) { const char *levels = "DWCFIS"; const QString format("%1 %2 %3\n"); @@ -111,6 +112,43 @@ static void appDebugOutput(QtMsgType type, const QMessageLogContext &context, co fflush(stderr); } +QString getIdealPlatform(QString currentPlatform) { + auto info = Sys::getKernelInfo(); + switch(info.kernelType) { + case Sys::KernelType::Darwin: { + if(info.kernelMajor >= 17) { + // macOS 10.13 or newer + return "osx64-5.15.2"; + } + else { + // macOS 10.12 or older + return "osx64"; + } + } + case Sys::KernelType::Windows: { + if(info.kernelMajor == 6 && info.kernelMinor >= 1) { + // Windows 7 + return "win32-5.15.2"; + } + else if (info.kernelMajor > 6) { + // Above Windows 7 + return "win32-5.15.2"; + } + else { + // Below Windows 7 + return "win32"; + } + } + case Sys::KernelType::Undetermined: + case Sys::KernelType::Linux: { + break; + } + } + return currentPlatform; +} + +} + MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv) { #if defined Q_OS_WIN32 @@ -678,7 +716,10 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv) // initialize the updater if(BuildConfig.UPDATER_ENABLED) { - m_updateChecker.reset(new UpdateChecker(BuildConfig.CHANLIST_URL, BuildConfig.VERSION_CHANNEL, BuildConfig.VERSION_BUILD)); + auto platform = getIdealPlatform(BuildConfig.BUILD_PLATFORM); + auto channelUrl = BuildConfig.UPDATER_BASE + platform + "/channels.json"; + qDebug() << "Initializing updater with platform: " << platform << " -- " << channelUrl; + m_updateChecker.reset(new UpdateChecker(channelUrl, BuildConfig.VERSION_CHANNEL, BuildConfig.VERSION_BUILD)); qDebug() << "<> Updater started."; } diff --git a/launcher/minecraft/auth/flows/AuthContext.cpp b/launcher/minecraft/auth/flows/AuthContext.cpp index 1203dc5f..776f45fe 100644 --- a/launcher/minecraft/auth/flows/AuthContext.cpp +++ b/launcher/minecraft/auth/flows/AuthContext.cpp @@ -538,7 +538,7 @@ void AuthContext::onSTSAuthGenericDone( } Katabasis::Token temp; - if(!parseXTokenResponse(replyData, temp, "STSAuthGaneric")) { + if(!parseXTokenResponse(replyData, temp, "STSAuthGeneric")) { qWarning() << "Could not parse authorization response for access to xbox API..."; failResult(m_xboxProfileSucceeded); return; diff --git a/launcher/pages/global/MultiMCPage.cpp b/launcher/pages/global/MultiMCPage.cpp index d383e6ed..5d43b187 100644 --- a/launcher/pages/global/MultiMCPage.cpp +++ b/launcher/pages/global/MultiMCPage.cpp @@ -58,8 +58,7 @@ MultiMCPage::MultiMCPage(QWidget *parent) : QWidget(parent), ui(new Ui::MultiMCP if(BuildConfig.UPDATER_ENABLED) { - QObject::connect(MMC->updateChecker().get(), &UpdateChecker::channelListLoaded, this, - &MultiMCPage::refreshUpdateChannelList); + QObject::connect(MMC->updateChecker().get(), &UpdateChecker::channelListLoaded, this, &MultiMCPage::refreshUpdateChannelList); if (MMC->updateChecker()->hasChannels()) { diff --git a/launcher/updater/DownloadTask.cpp b/launcher/updater/DownloadTask.cpp index 20b26ebb..2c62ad24 100644 --- a/launcher/updater/DownloadTask.cpp +++ b/launcher/updater/DownloadTask.cpp @@ -170,4 +170,4 @@ OperationList DownloadTask::operations() return m_operations; } -} \ No newline at end of file +} diff --git a/launcher/updater/UpdateChecker.cpp b/launcher/updater/UpdateChecker.cpp index be33c73c..eea73dcf 100644 --- a/launcher/updater/UpdateChecker.cpp +++ b/launcher/updater/UpdateChecker.cpp @@ -23,9 +23,12 @@ #define API_VERSION 0 #define CHANLIST_FORMAT 0 -UpdateChecker::UpdateChecker(QString channelListUrl, QString currentChannel, int currentBuild) +#include "BuildConfig.h" +#include "sys.h" + +UpdateChecker::UpdateChecker(QString channelUrl, QString currentChannel, int currentBuild) { - m_channelListUrl = channelListUrl; + m_channelUrl = channelUrl; m_currentChannel = currentChannel; m_currentBuild = currentBuild; } @@ -48,8 +51,7 @@ void UpdateChecker::checkForUpdate(QString updateChannel, bool notifyNoUpdate) // later. if (!m_chanListLoaded) { - qDebug() << "Channel list isn't loaded yet. Loading channel list and deferring " - "update check."; + qDebug() << "Channel list isn't loaded yet. Loading channel list and deferring update check."; m_checkUpdateWaiting = true; m_deferredUpdateChannel = updateChannel; updateChanList(notifyNoUpdate); @@ -62,29 +64,43 @@ void UpdateChecker::checkForUpdate(QString updateChannel, bool notifyNoUpdate) return; } - m_updateChecking = true; - // Find the desired channel within the channel list and get its repo URL. If if cannot be // found, error. + QString stableUrl; m_newRepoUrl = ""; for (ChannelListEntry entry : m_channels) { - if (entry.id == updateChannel) + qDebug() << "channelEntry = " << entry.id; + if(entry.id == "stable") { + stableUrl = entry.url; + } + if (entry.id == updateChannel) { m_newRepoUrl = entry.url; - if (entry.id == m_currentChannel) + qDebug() << "is intended update channel: " << entry.id; + } + if (entry.id == m_currentChannel) { m_currentRepoUrl = entry.url; + qDebug() << "is current update channel: " << entry.id; + } } qDebug() << "m_repoUrl = " << m_newRepoUrl; - // If we didn't find our channel, error. + if (m_newRepoUrl.isEmpty()) { + qWarning() << "m_repoUrl was empty. defaulting to 'stable': " << stableUrl; + m_newRepoUrl = stableUrl; + } + + // If nothing applies, error if (m_newRepoUrl.isEmpty()) { - qCritical() << "m_repoUrl is empty!"; + qCritical() << "failed to select any update repository for: " << updateChannel; emit updateCheckFailed(); return; } + m_updateChecking = true; + QUrl indexUrl = QUrl(m_newRepoUrl).resolved(QUrl("index.json")); auto job = new NetJob("GoUpdate Repository Index"); @@ -174,7 +190,7 @@ void UpdateChecker::updateChanList(bool notifyNoUpdate) return; } - if (m_channelListUrl.isEmpty()) + if (m_channelUrl.isEmpty()) { qCritical() << "Failed to update channel list. No channel list URL set." << "If you'd like to use MultiMC's update system, please pass the channel " @@ -184,7 +200,7 @@ void UpdateChecker::updateChanList(bool notifyNoUpdate) m_chanListLoading = true; NetJob *job = new NetJob("Update System Channel List"); - job->addNetAction(Net::Download::makeByteArray(QUrl(m_channelListUrl), &chanlistData)); + job->addNetAction(Net::Download::makeByteArray(QUrl(m_channelUrl), &chanlistData)); connect(job, &NetJob::succeeded, [this, notifyNoUpdate]() { chanListDownloadFinished(notifyNoUpdate); }); QObject::connect(job, &NetJob::failed, this, &UpdateChecker::chanListDownloadFailed); chanListJob.reset(job); diff --git a/launcher/updater/UpdateChecker.h b/launcher/updater/UpdateChecker.h index 91b6e26e..219c3c62 100644 --- a/launcher/updater/UpdateChecker.h +++ b/launcher/updater/UpdateChecker.h @@ -23,7 +23,7 @@ class UpdateChecker : public QObject Q_OBJECT public: - UpdateChecker(QString channelListUrl, QString currentChannel, int currentBuild); + UpdateChecker(QString channelUrl, QString currentChannel, int currentBuild); void checkForUpdate(QString updateChannel, bool notifyNoUpdate); /*! @@ -78,7 +78,7 @@ private: NetJobPtr chanListJob; QByteArray chanlistData; - QString m_channelListUrl; + QString m_channelUrl; QList m_channels; diff --git a/libraries/systeminfo/include/sys.h b/libraries/systeminfo/include/sys.h index 914d2555..bd6e2486 100644 --- a/libraries/systeminfo/include/sys.h +++ b/libraries/systeminfo/include/sys.h @@ -4,10 +4,24 @@ namespace Sys { const uint64_t mebibyte = 1024ull * 1024ull; + +enum class KernelType { + Undetermined, + Windows, + Darwin, + Linux +}; + struct KernelInfo { QString kernelName; QString kernelVersion; + + KernelType kernelType = KernelType::Undetermined; + int kernelMajor = 0; + int kernelMinor = 0; + int kernelPatch = 0; + bool isCursed = false; }; KernelInfo getKernelInfo(); diff --git a/libraries/systeminfo/src/sys_apple.cpp b/libraries/systeminfo/src/sys_apple.cpp index 4bcffae4..2d7d6083 100644 --- a/libraries/systeminfo/src/sys_apple.cpp +++ b/libraries/systeminfo/src/sys_apple.cpp @@ -2,13 +2,33 @@ #include +#include +#include + Sys::KernelInfo Sys::getKernelInfo() { Sys::KernelInfo out; struct utsname buf; uname(&buf); + out.kernelType = KernelType::Darwin; out.kernelName = buf.sysname; - out.kernelVersion = buf.release; + QString release = out.kernelVersion = buf.release; + + // TODO: figure out how to detect cursed-ness (macOS emulated on linux via mad hacks and so on) + out.isCursed = false; + + out.kernelMajor = 0; + out.kernelMinor = 0; + out.kernelPatch = 0; + auto sections = release.split('-'); + if(sections.size() >= 1) { + auto versionParts = sections[0].split('.'); + if(sections.size() >= 3) { + out.kernelMajor = sections[0].toInt(); + out.kernelMinor = sections[1].toInt(); + out.kernelPatch = sections[2].toInt(); + } + } return out; } diff --git a/libraries/systeminfo/src/sys_unix.cpp b/libraries/systeminfo/src/sys_unix.cpp index 42c0d319..303ead1f 100644 --- a/libraries/systeminfo/src/sys_unix.cpp +++ b/libraries/systeminfo/src/sys_unix.cpp @@ -6,13 +6,34 @@ #include #include +#include +#include + Sys::KernelInfo Sys::getKernelInfo() { Sys::KernelInfo out; struct utsname buf; uname(&buf); + // NOTE: we assume linux here. this needs further elaboration + out.kernelType = KernelType::Linux; out.kernelName = buf.sysname; - out.kernelVersion = buf.release; + QString release = out.kernelVersion = buf.release; + + // linux binary running on WSL is cursed. + out.isCursed = release.contains("WSL", Qt::CaseInsensitive) || release.contains("Microsoft", Qt::CaseInsensitive); + + out.kernelMajor = 0; + out.kernelMinor = 0; + out.kernelPatch = 0; + auto sections = release.split('-'); + if(sections.size() >= 1) { + auto versionParts = sections[0].split('.'); + if(sections.size() >= 3) { + out.kernelMajor = sections[0].toInt(); + out.kernelMinor = sections[1].toInt(); + out.kernelPatch = sections[2].toInt(); + } + } return out; } diff --git a/libraries/systeminfo/src/sys_win32.cpp b/libraries/systeminfo/src/sys_win32.cpp index a750b3a7..430b87e4 100644 --- a/libraries/systeminfo/src/sys_win32.cpp +++ b/libraries/systeminfo/src/sys_win32.cpp @@ -5,12 +5,16 @@ Sys::KernelInfo Sys::getKernelInfo() { Sys::KernelInfo out; + out.kernelType = KernelType::Windows; out.kernelName = "Windows"; OSVERSIONINFOW osvi; ZeroMemory(&osvi, sizeof(OSVERSIONINFOW)); osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOW); GetVersionExW(&osvi); out.kernelVersion = QString("%1.%2").arg(osvi.dwMajorVersion).arg(osvi.dwMinorVersion); + out.kernelMajor = osvi.dwMajorVersion; + out.kernelMinor = osvi.dwMinorVersion; + out.kernelPatch = osvi.dwBuildNumber; return out; } From c17b359d0380ff7d541b42b2dcc07aa5cd8f2bc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Sat, 4 Sep 2021 22:10:57 +0200 Subject: [PATCH 28/35] GH-4014 fix kernel version scanning on macOS and linux --- libraries/systeminfo/src/sys_apple.cpp | 9 ++++++++- libraries/systeminfo/src/sys_unix.cpp | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/libraries/systeminfo/src/sys_apple.cpp b/libraries/systeminfo/src/sys_apple.cpp index 2d7d6083..7fc0017b 100644 --- a/libraries/systeminfo/src/sys_apple.cpp +++ b/libraries/systeminfo/src/sys_apple.cpp @@ -4,6 +4,7 @@ #include #include +#include Sys::KernelInfo Sys::getKernelInfo() { @@ -23,11 +24,17 @@ Sys::KernelInfo Sys::getKernelInfo() auto sections = release.split('-'); if(sections.size() >= 1) { auto versionParts = sections[0].split('.'); - if(sections.size() >= 3) { + if(versionParts.size() >= 3) { out.kernelMajor = sections[0].toInt(); out.kernelMinor = sections[1].toInt(); out.kernelPatch = sections[2].toInt(); } + else { + qWarning() << "Not enough version numbers in " << sections[0] << " found " << versionParts.size(); + } + } + else { + qWarning() << "Not enough '-' sections in " << release << " found " << sections.size(); } return out; } diff --git a/libraries/systeminfo/src/sys_unix.cpp b/libraries/systeminfo/src/sys_unix.cpp index 303ead1f..cba52cfb 100644 --- a/libraries/systeminfo/src/sys_unix.cpp +++ b/libraries/systeminfo/src/sys_unix.cpp @@ -8,6 +8,7 @@ #include #include +#include Sys::KernelInfo Sys::getKernelInfo() { @@ -28,11 +29,17 @@ Sys::KernelInfo Sys::getKernelInfo() auto sections = release.split('-'); if(sections.size() >= 1) { auto versionParts = sections[0].split('.'); - if(sections.size() >= 3) { + if(versionParts.size() >= 3) { out.kernelMajor = sections[0].toInt(); out.kernelMinor = sections[1].toInt(); out.kernelPatch = sections[2].toInt(); } + else { + qWarning() << "Not enough version numbers in " << sections[0] << " found " << versionParts.size(); + } + } + else { + qWarning() << "Not enough '-' sections in " << release << " found " << sections.size(); } return out; } From 823e7d22c70ac56f3bba6a6685c67902e8535ad9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Sat, 4 Sep 2021 22:18:29 +0200 Subject: [PATCH 29/35] GH-4014 fix kernel version scanning on macOS and linux some more --- libraries/systeminfo/src/sys_apple.cpp | 6 +++--- libraries/systeminfo/src/sys_unix.cpp | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/libraries/systeminfo/src/sys_apple.cpp b/libraries/systeminfo/src/sys_apple.cpp index 7fc0017b..6353b747 100644 --- a/libraries/systeminfo/src/sys_apple.cpp +++ b/libraries/systeminfo/src/sys_apple.cpp @@ -25,9 +25,9 @@ Sys::KernelInfo Sys::getKernelInfo() if(sections.size() >= 1) { auto versionParts = sections[0].split('.'); if(versionParts.size() >= 3) { - out.kernelMajor = sections[0].toInt(); - out.kernelMinor = sections[1].toInt(); - out.kernelPatch = sections[2].toInt(); + out.kernelMajor = versionParts[0].toInt(); + out.kernelMinor = versionParts[1].toInt(); + out.kernelPatch = versionParts[2].toInt(); } else { qWarning() << "Not enough version numbers in " << sections[0] << " found " << versionParts.size(); diff --git a/libraries/systeminfo/src/sys_unix.cpp b/libraries/systeminfo/src/sys_unix.cpp index cba52cfb..fb96c72c 100644 --- a/libraries/systeminfo/src/sys_unix.cpp +++ b/libraries/systeminfo/src/sys_unix.cpp @@ -30,9 +30,9 @@ Sys::KernelInfo Sys::getKernelInfo() if(sections.size() >= 1) { auto versionParts = sections[0].split('.'); if(versionParts.size() >= 3) { - out.kernelMajor = sections[0].toInt(); - out.kernelMinor = sections[1].toInt(); - out.kernelPatch = sections[2].toInt(); + out.kernelMajor = versionParts[0].toInt(); + out.kernelMinor = versionParts[1].toInt(); + out.kernelPatch = versionParts[2].toInt(); } else { qWarning() << "Not enough version numbers in " << sections[0] << " found " << versionParts.size(); From d644fb2094f623e45bff237ede7d432121f72072 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Sat, 4 Sep 2021 23:51:57 +0200 Subject: [PATCH 30/35] GH-4014 do not switch to Qt 5.15.2 on Windows It is unstable for reasons unknown. --- launcher/MultiMC.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/launcher/MultiMC.cpp b/launcher/MultiMC.cpp index 69ba4ac5..c532ce82 100644 --- a/launcher/MultiMC.cpp +++ b/launcher/MultiMC.cpp @@ -126,6 +126,9 @@ QString getIdealPlatform(QString currentPlatform) { } } case Sys::KernelType::Windows: { + // FIXME: 5.15.2 is not stable on Windows, due to a large number of completely unpredictable and hard to reproduce issues + break; +/* if(info.kernelMajor == 6 && info.kernelMinor >= 1) { // Windows 7 return "win32-5.15.2"; @@ -138,6 +141,7 @@ QString getIdealPlatform(QString currentPlatform) { // Below Windows 7 return "win32"; } +*/ } case Sys::KernelType::Undetermined: case Sys::KernelType::Linux: { From 878c4fb8103bc866e5368fbb7287e94cca190dff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Sun, 5 Sep 2021 18:23:49 +0200 Subject: [PATCH 31/35] NOISSUE Provide dummy implementation for the secrets library --- CMakeLists.txt | 3 ++ launcher/CMakeLists.txt | 4 +- launcher/minecraft/auth/flows/AuthContext.cpp | 12 +++--- launcher/pages/global/AccountListPage.cpp | 9 ++-- notsecrets/CMakeLists.txt | 4 ++ notsecrets/Secrets.cpp | 42 +++++++++++++++++++ notsecrets/Secrets.h | 8 ++++ 7 files changed, 69 insertions(+), 13 deletions(-) create mode 100644 notsecrets/CMakeLists.txt create mode 100644 notsecrets/Secrets.cpp create mode 100644 notsecrets/Secrets.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 84c4a180..9356f326 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -289,7 +289,10 @@ add_subdirectory(buildconfig) if(MultiMC_EMBED_SECRETS) add_subdirectory(secrets) +else() + add_subdirectory(notsecrets) endif() + # NOTE: this must always be last to appease the CMake deity of quirky install command evaluation order. add_subdirectory(launcher) diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 7241b89d..c29ee3e1 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -949,9 +949,7 @@ install(TARGETS MultiMC RUNTIME DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime ) -if(MultiMC_EMBED_SECRETS) - target_link_libraries(MultiMC_logic secrets) -endif() +target_link_libraries(MultiMC_logic secrets) #### The MultiMC bundle mess! #### # Bundle utilities are used to complete the portable packages - they add all the libraries that would otherwise be missing on the target system. diff --git a/launcher/minecraft/auth/flows/AuthContext.cpp b/launcher/minecraft/auth/flows/AuthContext.cpp index 776f45fe..b4db6c2d 100644 --- a/launcher/minecraft/auth/flows/AuthContext.cpp +++ b/launcher/minecraft/auth/flows/AuthContext.cpp @@ -18,9 +18,7 @@ #include "katabasis/Globals.h" #include "AuthRequest.h" -#ifdef EMBED_SECRETS #include "Secrets.h" -#endif #include "Env.h" @@ -53,13 +51,18 @@ void AuthContext::finishActivity() { } void AuthContext::initMSA() { -#ifdef EMBED_SECRETS if(m_oauth2) { return; } + + auto clientId = Secrets::getMSAClientID('-'); + if(clientId.isEmpty()) { + return; + } + Katabasis::OAuth2::Options opts; opts.scope = "XboxLive.signin offline_access"; - opts.clientIdentifier = Secrets::getMSAClientID('-'); + opts.clientIdentifier = clientId; opts.authorizationUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode"; opts.accessTokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"; opts.listenerPorts = {28562, 28563, 28564, 28565, 28566}; @@ -71,7 +74,6 @@ void AuthContext::initMSA() { connect(m_oauth2, &OAuth2::linkingSucceeded, this, &AuthContext::onOAuthLinkingSucceeded); connect(m_oauth2, &OAuth2::showVerificationUriAndCode, this, &AuthContext::showVerificationUriAndCode); connect(m_oauth2, &OAuth2::activityChanged, this, &AuthContext::onOAuthActivityChanged); -#endif } void AuthContext::initMojang() { diff --git a/launcher/pages/global/AccountListPage.cpp b/launcher/pages/global/AccountListPage.cpp index 6bb07b22..f52fa834 100644 --- a/launcher/pages/global/AccountListPage.cpp +++ b/launcher/pages/global/AccountListPage.cpp @@ -37,6 +37,8 @@ #include "BuildConfig.h" #include +#include "Secrets.h" + AccountListPage::AccountListPage(QWidget *parent) : QMainWindow(parent), ui(new Ui::AccountListPage) { @@ -70,11 +72,8 @@ AccountListPage::AccountListPage(QWidget *parent) updateButtonStates(); - // Xbox authentication won't work without a client identifier, so disable the button - // if the build didn't specify one (GH-4012) -#ifndef EMBED_SECRETS - ui->actionAddMicrosoft->setVisible(false); -#endif + // Xbox authentication won't work without a client identifier, so disable the button if it is missing + ui->actionAddMicrosoft->setVisible(Secrets::hasMSAClientID()); } AccountListPage::~AccountListPage() diff --git a/notsecrets/CMakeLists.txt b/notsecrets/CMakeLists.txt new file mode 100644 index 00000000..f27aeb70 --- /dev/null +++ b/notsecrets/CMakeLists.txt @@ -0,0 +1,4 @@ +add_library(secrets STATIC Secrets.cpp Secrets.h) +target_link_libraries(secrets Qt5::Core) +target_compile_definitions(secrets PUBLIC -DEMBED_SECRETS) +target_include_directories(secrets PUBLIC .) diff --git a/notsecrets/Secrets.cpp b/notsecrets/Secrets.cpp new file mode 100644 index 00000000..88995635 --- /dev/null +++ b/notsecrets/Secrets.cpp @@ -0,0 +1,42 @@ +#include "Secrets.h" + +#include +#include + +namespace { + +/* + * This is the MSA client ID. It is confidential and should not be reused. + * You can obtain one for yourself by using azure app registration: + * https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app + * + * The app registration should: + * - Be only for personal accounts. + * - Not have any redirect URI. + * - Not have any platform. + * - Have no credentials. + * - No certificates. + * - No client secrets. + * - Enable 'Live SDK support' for access to XBox APIs. + * - Enable 'public client flows' for OAuth2 device flow. + * + * By putting one in here, you accept the terms and conditions for using the MS Identity Plaform and assume all responsibilities associated with it. + * See: https://docs.microsoft.com/en-us/legal/microsoft-identity-platform/terms-of-use + * + * Above all else, do not impersonate other applications! This includes the Mojang Launcher and MultiMC - your builds are *NOT* MultiMC. + * + * If you intend to base your own launcher on this code, take care and customize this to obfuscate the client ID, so it cannot be trivially found by casual attackers. + */ + +QString MSAClientID = ""; +} + +namespace Secrets { +bool hasMSAClientID() { + return !MSAClientID.isEmpty(); +} + +QString getMSAClientID(uint8_t separator) { + return MSAClientID; +} +} diff --git a/notsecrets/Secrets.h b/notsecrets/Secrets.h new file mode 100644 index 00000000..6872b68e --- /dev/null +++ b/notsecrets/Secrets.h @@ -0,0 +1,8 @@ +#pragma once +#include +#include + +namespace Secrets { +bool hasMSAClientID(); +QString getMSAClientID(uint8_t separator); +} From 46468c8f142144bd5c90a7d3e0dffb43f79a9223 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Sun, 5 Sep 2021 18:54:27 +0200 Subject: [PATCH 32/35] NOISSUE block MS account adding on macOS < 10.13 builds It's never going to work with Qt 5.6, so there's no point. People need to update. --- launcher/pages/global/AccountListPage.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/launcher/pages/global/AccountListPage.cpp b/launcher/pages/global/AccountListPage.cpp index f52fa834..74537712 100644 --- a/launcher/pages/global/AccountListPage.cpp +++ b/launcher/pages/global/AccountListPage.cpp @@ -128,6 +128,18 @@ void AccountListPage::on_actionAddMojang_triggered() void AccountListPage::on_actionAddMicrosoft_triggered() { + if(BuildConfig.BUILD_PLATFORM == "osx64") { + CustomMessageBox::selectable( + this, + tr("Microsoft Accounts not available"), + tr( + "Microsoft accounts are only usable on macOS 10.13 or newer, with fully updated MultiMC.\n\n" + "Please update both your operating system and MultiMC." + ), + QMessageBox::Warning + )->exec(); + return; + } MinecraftAccountPtr account = MSALoginDialog::newAccount( this, tr("Please enter your Mojang account email and password to add your account.") From 426135b76ac859e964f9476b4c3b311625ccefda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Sun, 5 Sep 2021 22:21:59 +0200 Subject: [PATCH 33/35] NOISSUE bump version to 0.6.13 and update changelog --- CMakeLists.txt | 2 +- changelog.md | 133 ++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 128 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9356f326..44028f76 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,7 +55,7 @@ set(MultiMC_NEWS_RSS_URL "https://multimc.org/rss.xml" CACHE STRING "URL to fetc ######## Set version numbers ######## set(MultiMC_VERSION_MAJOR 0) set(MultiMC_VERSION_MINOR 6) -set(MultiMC_VERSION_HOTFIX 12) +set(MultiMC_VERSION_HOTFIX 13) # Build number set(MultiMC_VERSION_BUILD -1 CACHE STRING "Build number. -1 for no build number.") diff --git a/changelog.md b/changelog.md index 31b99a6b..2846d1df 100644 --- a/changelog.md +++ b/changelog.md @@ -1,10 +1,133 @@ -# MultiMC 0.6.12 +# MultiMC 0.6.13 + +This release brings initial support for Microsoft accounts, along with a nice pile of modpack platform support changes and improved Java runtime detection. + +Java runtimes still need an overhaul, so we're staying on the 0.6 version for a little longer. + +Next release should also tackle the current Forge 1.17.x issues in a systematic way. + +### Microsoft accounts + +This is the first release with Microsoft accounts in. + +Implementation is loosely based on documentation available from [wiki.vg](https://wiki.vg/Microsoft_Authentication_Scheme) with some notable changes: + +- More complete implementation including getting and displaying GamerTags [(see XR-046)](https://docs.microsoft.com/en-us/gaming/gdk/_content/gc/policies/pc/live-policies-pc#xr-046-display-name-and-gamerpic-). + +- Using the OAuth Device Flow instead of closely integrating with a browser engine. + + MultiMC asks you to open a Microsoft login web page and put in a code that lets MultiMC authenticate. + + This lets you authenticate on a completely separate device like your phone, leaving code we ship and the computer you may not even trust out of the picture. + +As part of this, the skin fetching no longer uses a third party services and instead gets skins directly from Mojang. Capes can also be selected in MultiMC now. + +### macOS update + +Because of issues with the Microsoft accounts, we now have two builds on macOS: + +- The old build with Qt 5.6 that does not work with Microsoft accounts, but can run on macOS older than 10.13. + +- A new build with Qt 5.15.2 that does work with Microsoft accounts, supports the new macOS dark theme and highlight colors, but requires at least macOS 10.13. + +MultiMC will update to the 5.15.2 builds when it detects that this is possible. **It may look like it is updating twice, just let it do its thing.** + +Similar approach got attempted on Windows, aiming to fix various display scaling and theming issues, but it ran into too many problems and will be attempted later, with more caution. + +### Modpack platforms + +In general, the modpack platform pages have been made more consistent with each other (GH-3118, GH-3720, GH-3731). + +- FTB improvements: + + - Modpack file downloads are now checked with checksums and cached. + + - GH-1949: Allow Legacy FTB and FTB pack downloads to be aborted. + +- CurseForge improvements: + + - CurseForge modpack platform is now presented as CurseForge, not Twitch. + + - UI has been updated to match other platforms + + - Added sorting + + - GH-3667: Added version selection + + - GH-3611: Added ability to install beta versions + + - GH-3633: When a CurseForge pack suppors multiple Minecraft versions, we assume the latest one. + +- ATLauncher improvements: + + - Handling latest/custom/recommended mod loader versions. + + - Fabric loader packs should now work. + + - GH-3764: Only client mods are installed now for ATL packs. + + - Improved error handling + + - Optional mods are supported. + + - GH-1949: Allow ATLauncher pack downloads to be aborted + + +- Fixed bugs in FTB platform search. + +### Other changes + +- Forge installation is disabled on Minecraft 1.17+ because of incompatible/unresolved changes on the Forge side. + + We're going to aim for fixing it in time for 1.18. Thankfully, 1.17 is more of a in-between release, so go play some 1.16.x packs! + +- GH-2529: On macOS, MultiMC will ask to move all the instance data to a new `Data` folder in order to fix long load times caused by macOS checking all files. + +- Detection of a large amount of various Java runtime flavors have been added. + +- It is now possible to join servers when starting an instance: + + - From command line via the `--launch` and `--server` arguments. + + - Or by setting this up in the instance settings page. + + This may not work correctly in some cases, because it is a rarely used feature and modders do not test with it. + +- MultiMC now prints resolved IP addresses of Minecraft services into the game log for diagnostic purposes. + +- Updated instance icons based on Minecraft textures. + +- Forge `mods.toml` files are now used for displaying mods in the UI. + +- Datapack button is now disabled when no world is selected. + +- MultiMC warns about GLFW and OpenAL workarounds being enabled in the game log. + +- Languages in the translations list are now sorted by their two/three letter key + +- GH-3450: Displaying and recording gameplay time is now optional and can be turned off. + +- GH-3930: MultiMC can now track the gameplay time of the last session. + +- GH-3033: The version pages of instances now have a filter bar. + +- GH-2971: UI descriptions of texture and resource packs no longer mention mods. + +- Quick and dirty minimum Java runtime versions checks have been added. This needs to be expanded in the future. + +### Technical changes + +- The codebase continues to move towards being debranded and harder to build as 'MultiMC' for third parties. + +# Previous releases + +## MultiMC 0.6.12 After roughly one year of maintenance and development work by various contributors, we're just calling it a good time to release. What got added since the last time? Quite a bit! But in general, this is more of a spring cleaning before the major changes that we need to make come in. -### Modpack platforms +#### Modpack platforms We've added a whole bunch of new modpack platforms to pick from right into the new instance dialog. If you run into any unusual issues with the imported packs, report them on the bug tracker. @@ -18,7 +141,7 @@ We've added a whole bunch of new modpack platforms to pick from right into the n - GH-405: Added a ATLauncher pack browser -### Other changes +#### Other changes - Added the option to not use OpenAL and/or GLFW libraries bundled with the game. @@ -46,7 +169,7 @@ We've added a whole bunch of new modpack platforms to pick from right into the n - GH-3602: Pre-launch commands could fail on first launch of the instance because the .minecraft folder has not been created yet. -### Technical changes +#### Technical changes - GH-3234: At build time, the meta URL can be changed. @@ -58,8 +181,6 @@ We've added a whole bunch of new modpack platforms to pick from right into the n - Compatibility with unusual build environments has been increased -# Previous releases - ## MultiMC 0.6.11 This adds Forge 1.13+ support using [ForgeWrapper](https://github.com/ZekerZhayard/ForgeWrapper) by ZekerZhayard. From 6c9dc4c86ad934d08554b0358ac2875b9fc5f9dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Tue, 7 Sep 2021 21:02:41 +0200 Subject: [PATCH 34/35] NOISSUE fix typos in changelog --- changelog.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/changelog.md b/changelog.md index 2846d1df..97454d7f 100644 --- a/changelog.md +++ b/changelog.md @@ -20,7 +20,9 @@ Implementation is loosely based on documentation available from [wiki.vg](https: This lets you authenticate on a completely separate device like your phone, leaving code we ship and the computer you may not even trust out of the picture. -As part of this, the skin fetching no longer uses a third party services and instead gets skins directly from Mojang. Capes can also be selected in MultiMC now. +As part of this, the skin fetching no longer uses a third party service and instead gets skins directly from Mojang. + +Capes can also be selected in MultiMC now. With how many people will now get one for migrating their accounts, it only makes sense. ### macOS update @@ -28,7 +30,7 @@ Because of issues with the Microsoft accounts, we now have two builds on macOS: - The old build with Qt 5.6 that does not work with Microsoft accounts, but can run on macOS older than 10.13. -- A new build with Qt 5.15.2 that does work with Microsoft accounts, supports the new macOS dark theme and highlight colors, but requires at least macOS 10.13. +- A new build with Qt 5.15.2 that does work with Microsoft accounts, can use the new macOS dark theme and highlight colors, but requires at least macOS 10.13. MultiMC will update to the 5.15.2 builds when it detects that this is possible. **It may look like it is updating twice, just let it do its thing.** @@ -56,7 +58,7 @@ In general, the modpack platform pages have been made more consistent with each - GH-3611: Added ability to install beta versions - - GH-3633: When a CurseForge pack suppors multiple Minecraft versions, we assume the latest one. + - GH-3633: When a CurseForge pack is available for multiple Minecraft versions, we assume the latest one. - ATLauncher improvements: From e2355eb276bf355ca4acf526a0f3cc390aa88f8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Thu, 9 Sep 2021 00:27:46 +0200 Subject: [PATCH 35/35] NOISSUE enable listing symlinks in mod/world lists --- launcher/minecraft/WorldList.cpp | 3 +-- launcher/minecraft/legacy/LegacyModList.cpp | 2 +- launcher/minecraft/mod/ModFolderModel.cpp | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/launcher/minecraft/WorldList.cpp b/launcher/minecraft/WorldList.cpp index f6309dbd..dcdbc321 100644 --- a/launcher/minecraft/WorldList.cpp +++ b/launcher/minecraft/WorldList.cpp @@ -26,8 +26,7 @@ WorldList::WorldList(const QString &dir) : QAbstractListModel(), m_dir(dir) { FS::ensureFolderPathExists(m_dir.absolutePath()); - m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs | - QDir::NoSymLinks); + m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); m_watcher = new QFileSystemWatcher(this); is_watching = false; diff --git a/launcher/minecraft/legacy/LegacyModList.cpp b/launcher/minecraft/legacy/LegacyModList.cpp index 7301eb8c..e9948ab1 100644 --- a/launcher/minecraft/legacy/LegacyModList.cpp +++ b/launcher/minecraft/legacy/LegacyModList.cpp @@ -22,7 +22,7 @@ LegacyModList::LegacyModList(const QString &dir, const QString &list_file) : m_dir(dir), m_list_file(list_file) { FS::ensureFolderPathExists(m_dir.absolutePath()); - m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs | QDir::NoSymLinks); + m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); } diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 031eebe5..f0c53c39 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -29,7 +29,7 @@ ModFolderModel::ModFolderModel(const QString &dir) : QAbstractListModel(), m_dir(dir) { FS::ensureFolderPathExists(m_dir.absolutePath()); - m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs | QDir::NoSymLinks); + m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); m_watcher = new QFileSystemWatcher(this); connect(m_watcher, SIGNAL(directoryChanged(QString)), this, SLOT(directoryChanged(QString)));