diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 37724038..2d0c81bb 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -827,6 +827,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) qDebug() << "Loading accounts..."; m_accounts->setListFilePath("accounts.json", true); m_accounts->loadList(); + m_accounts->fillQueue(); qDebug() << "<> Accounts loaded."; } diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 08c878d1..2dfc78b5 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -196,36 +196,52 @@ set(ICONS_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/AccountList.h + minecraft/auth/AccountData.h minecraft/auth/AccountList.cpp - minecraft/auth/MinecraftAccount.h + minecraft/auth/AccountList.h + minecraft/auth/AccountTask.cpp + minecraft/auth/AccountTask.h + minecraft/auth/AuthRequest.cpp + minecraft/auth/AuthRequest.h + minecraft/auth/AuthSession.cpp + minecraft/auth/AuthSession.h + minecraft/auth/AuthStep.cpp + minecraft/auth/AuthStep.h 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/MinecraftAccount.h + minecraft/auth/Parsers.cpp + minecraft/auth/Parsers.h + minecraft/auth/Yggdrasil.cpp + minecraft/auth/Yggdrasil.h - minecraft/auth/flows/MSAInteractive.h - minecraft/auth/flows/MSAInteractive.cpp - minecraft/auth/flows/MSASilent.h - minecraft/auth/flows/MSASilent.cpp + minecraft/auth/flows/AuthFlow.cpp + minecraft/auth/flows/AuthFlow.h + minecraft/auth/flows/Mojang.cpp + minecraft/auth/flows/Mojang.h + minecraft/auth/flows/MSA.cpp + minecraft/auth/flows/MSA.h - 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/auth/flows/Parsers.h - minecraft/auth/flows/Parsers.cpp + minecraft/auth/steps/EntitlementsStep.cpp + minecraft/auth/steps/EntitlementsStep.h + minecraft/auth/steps/GetSkinStep.cpp + minecraft/auth/steps/GetSkinStep.h + minecraft/auth/steps/LauncherLoginStep.cpp + minecraft/auth/steps/LauncherLoginStep.h + minecraft/auth/steps/MigrationEligibilityStep.cpp + minecraft/auth/steps/MigrationEligibilityStep.h + minecraft/auth/steps/MinecraftProfileStep.cpp + minecraft/auth/steps/MinecraftProfileStep.h + minecraft/auth/steps/MSAStep.cpp + minecraft/auth/steps/MSAStep.h + minecraft/auth/steps/XboxAuthorizationStep.cpp + minecraft/auth/steps/XboxAuthorizationStep.h + minecraft/auth/steps/XboxProfileStep.cpp + minecraft/auth/steps/XboxProfileStep.h + minecraft/auth/steps/XboxUserStep.cpp + minecraft/auth/steps/XboxUserStep.h + minecraft/auth/steps/YggdrasilStep.cpp + minecraft/auth/steps/YggdrasilStep.h minecraft/gameoptions/GameOptions.h minecraft/gameoptions/GameOptions.cpp diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index 8bd5732f..39fec9e6 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -35,6 +35,8 @@ void LaunchController::executeTask() return; } + JavaCommon::checkJVMArgs(m_instance->settings()->get("JvmArgs").toString(), m_parentWidget); + login(); } @@ -90,8 +92,6 @@ void LaunchController::decideAccount() void LaunchController::login() { - JavaCommon::checkJVMArgs(m_instance->settings()->get("JvmArgs").toString(), m_parentWidget); - decideAccount(); // if no account is selected, we bail @@ -113,133 +113,10 @@ void LaunchController::login() { { m_session = std::make_shared(); m_session->wants_online = m_online; - shared_qobject_ptr task; - if(!password.isNull()) { - task = m_accountToUse->login(m_session, password); - } - else { - task = m_accountToUse->refresh(m_session); - } - if (task) - { - // We'll need to validate the access token to make sure the account - // is still logged in. - ProgressDialog progDialog(m_parentWidget); - if (m_online) - { - progDialog.setSkipButton(true, tr("Play Offline")); - } - progDialog.execWithTask(task.get()); - if (!task->wasSuccessful()) - { - auto failReasonNew = task->failReason(); - if(failReasonNew == "Invalid token." || failReasonNew == "Invalid Signature") - { - // account->invalidateClientToken(); - failReason = needLoginAgain; - } - else failReason = failReasonNew; - } - } - 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.")); - 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 - { - if(toChop.size() > N) - { - auto left = toChop.left(N); - left += QString("\u25CF").repeated(toChop.size() - N); - return left; - } - return toChop; - }; + m_accountToUse->fillSession(m_session); - 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; - } - case AuthSession::RequiresProfileSetup: { - auto entitlement = m_accountToUse->accountData()->minecraftEntitlement; - QString errorString; - if(!entitlement.canPlayMinecraft) { - errorString = tr("The account does not own Minecraft. You need to purchase the game first to play it."); - QMessageBox::warning( - nullptr, - tr("Missing Minecraft profile"), - errorString, - QMessageBox::StandardButton::Ok, - QMessageBox::StandardButton::Ok - ); - tryagain = false; - emitFailed(errorString); - return; - } - // Now handle setting up a profile name here... - ProfileSetupDialog dialog(m_accountToUse, m_parentWidget); - if (dialog.exec() == QDialog::Accepted) - { - tryagain = true; - continue; - } - else - { - tryagain = false; - emitFailed(tr("Received undetermined session status during login.")); - return; - } - } - case AuthSession::RequiresOAuth: { - auto errorString = tr("Microsoft account has expired and needs to be logged into manually again."); - QMessageBox::warning( - m_parentWidget, - tr("Microsoft Account refresh failed"), - errorString, - QMessageBox::StandardButton::Ok, - QMessageBox::StandardButton::Ok - ); - tryagain = false; - 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( - m_parentWidget, - tr("Account gone"), - errorString, - QMessageBox::StandardButton::Ok, - QMessageBox::StandardButton::Ok - ); - tryagain = false; - emitFailed(errorString); - return; - } - case AuthSession::PlayableOffline: { + switch(m_accountToUse->accountState()) { + case AccountState::Offline: { // we ask the user for a player name bool ok = false; QString usedname = m_session->player_name; @@ -262,11 +139,90 @@ void LaunchController::login() { } m_session->MakeOffline(usedname); // offline flavored game from here :3 + // NOTE: fallthrough is intentional } - case AuthSession::PlayableOnline: - { - launchInstance(); - tryagain = false; + case AccountState::Online: { + if(m_accountToUse->ownsMinecraft() && !m_accountToUse->hasProfile()) { + auto entitlement = m_accountToUse->accountData()->minecraftEntitlement; + QString errorString; + if(!entitlement.canPlayMinecraft) { + errorString = tr("The account does not own Minecraft. You need to purchase the game first to play it."); + QMessageBox::warning( + nullptr, + tr("Missing Minecraft profile"), + errorString, + QMessageBox::StandardButton::Ok, + QMessageBox::StandardButton::Ok + ); + emitFailed(errorString); + return; + } + // Now handle setting up a profile name here... + ProfileSetupDialog dialog(m_accountToUse, m_parentWidget); + if (dialog.exec() == QDialog::Accepted) + { + tryagain = true; + continue; + } + else + { + emitFailed(tr("Received undetermined session status during login.")); + return; + } + } + else { + launchInstance(); + } + return; + } + case AccountState::Unchecked: { + m_accountToUse->refresh(); + // NOTE: fallthrough intentional + } + case AccountState::Working: { + // refresh is in progress, we need to wait for it to finish to proceed. + ProgressDialog progDialog(m_parentWidget); + if (m_online) + { + progDialog.setSkipButton(true, tr("Play Offline")); + } + auto task = m_accountToUse->currentTask(); + progDialog.execWithTask(task.get()); + continue; + } + // FIXME: this is missing - the meaning is that the account is queued for refresh and we should wait for that + /* + case AccountState::Queued: { + return; + } + */ + case AccountState::Errored: { + // This means some sort of soft error that we can fix with a refresh ... so let's refresh. + // TODO: implement + return; + } + case AccountState::Expired: { + auto errorString = tr("The account has expired and needs to be logged into manually again."); + QMessageBox::warning( + m_parentWidget, + tr("Account refresh failed"), + errorString, + QMessageBox::StandardButton::Ok, + QMessageBox::StandardButton::Ok + ); + emitFailed(errorString); + return; + } + case AccountState::Gone: { + 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( + m_parentWidget, + tr("Account gone"), + errorString, + QMessageBox::StandardButton::Ok, + QMessageBox::StandardButton::Ok + ); + emitFailed(errorString); return; } } @@ -334,14 +290,7 @@ void LaunchController::launchInstance() online_mode = "offline"; } - QString auth_server_status; - if(m_session->auth_server_online) { - auth_server_status = "online"; - } else { - auth_server_status = "offline"; - } - - m_launcher->prependStep(new TextPrint(m_launcher.get(), "Launched instance in " + online_mode + " mode\nAuthentication server is " + auth_server_status + "\n", MessageLevel::Launcher)); + m_launcher->prependStep(new TextPrint(m_launcher.get(), "Launched instance in " + online_mode + " mode\n", MessageLevel::Launcher)); // Prepend Version m_launcher->prependStep(new TextPrint(m_launcher.get(), BuildConfig.LAUNCHER_NAME + " version: " + BuildConfig.printableVersionString() + "\n\n", MessageLevel::Launcher)); diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h index 09cd2c73..fa42747e 100644 --- a/launcher/minecraft/auth/AccountData.h +++ b/launcher/minecraft/auth/AccountData.h @@ -41,6 +41,16 @@ enum class AccountType { Mojang }; +enum class AccountState { + Unchecked, + Offline, + Working, + Online, + Errored, + Expired, + Gone +}; + struct AccountData { QJsonObject saveState() const; bool resumeStateFromV2(QJsonObject data); @@ -77,4 +87,9 @@ struct AccountData { MinecraftProfile minecraftProfile; MinecraftEntitlement minecraftEntitlement; Katabasis::Validity validity_ = Katabasis::Validity::None; + + // runtime only information (not saved with the account) + QString internalId; + QString errorString; + AccountState accountState = AccountState::Unchecked; }; diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp index d7537345..c44e3e89 100644 --- a/launcher/minecraft/auth/AccountList.cpp +++ b/launcher/minecraft/auth/AccountList.cpp @@ -15,6 +15,7 @@ #include "AccountList.h" #include "AccountData.h" +#include "AccountTask.h" #include #include @@ -24,6 +25,7 @@ #include #include #include +#include #include @@ -35,7 +37,14 @@ enum AccountListVersion { MojangMSA = 3 }; -AccountList::AccountList(QObject *parent) : QAbstractListModel(parent) { } +AccountList::AccountList(QObject *parent) : QAbstractListModel(parent) { + m_refreshTimer = new QTimer(this); + m_refreshTimer->setSingleShot(true); + connect(m_refreshTimer, &QTimer::timeout, this, &AccountList::fillQueue); + m_nextTimer = new QTimer(this); + m_nextTimer->setSingleShot(true); + connect(m_nextTimer, &QTimer::timeout, this, &AccountList::tryNext); +} AccountList::~AccountList() noexcept {} @@ -244,13 +253,29 @@ QVariant AccountList::data(const QModelIndex &index, int role) const } case StatusColumn: { - if(account->isActive()) { - return tr("Working", "Account status"); + switch(account->accountState()) { + case AccountState::Unchecked: { + return tr("Unchecked", "Account status"); + } + case AccountState::Offline: { + return tr("Offline", "Account status"); + } + case AccountState::Online: { + return tr("Online", "Account status"); + } + case AccountState::Working: { + return tr("Working", "Account status"); + } + case AccountState::Errored: { + return tr("Errored", "Account status"); + } + case AccountState::Expired: { + return tr("Expired", "Account status"); + } + case AccountState::Gone: { + return tr("Gone", "Account status"); + } } - if(account->isExpired()) { - return tr("Expired", "Account status"); - } - return tr("Ready", "Account status"); } case ProfileNameColumn: { @@ -583,10 +608,105 @@ void AccountList::setListFilePath(QString path, bool autosave) bool AccountList::anyAccountIsValid() { - for(auto account:m_accounts) + for(auto account: m_accounts) { - if(account->accountStatus() != NotVerified) + if(account->ownsMinecraft()) { return true; + } } return false; } + +void AccountList::fillQueue() { + + if(m_defaultAccount && m_defaultAccount->shouldRefresh()) { + auto idToRefresh = m_defaultAccount->internalId(); + m_refreshQueue.push_back(idToRefresh); + qDebug() << "AccountList: Queued default account with internal ID " << idToRefresh << " to refresh first"; + } + + for(int i = 0; i < count(); i++) { + auto account = at(i); + if(account == m_defaultAccount) { + continue; + } + + if(account->shouldRefresh()) { + auto idToRefresh = account->internalId(); + m_refreshQueue.push_back(idToRefresh); + qDebug() << "AccountList: Queued account with internal ID " << idToRefresh << " to refresh"; + } + } + m_refreshQueue.removeDuplicates(); + tryNext(); +} + +void AccountList::requestRefresh(QString accountId) { + m_refreshQueue.push_back(accountId); + if(!isActive()) { + tryNext(); + } +} + +void AccountList::tryNext() { + beginActivity(); + while (m_refreshQueue.length()) { + auto accountId = m_refreshQueue.front(); + m_refreshQueue.pop_front(); + for(int i = 0; i < count(); i++) { + auto account = at(i); + if(account->internalId() == accountId) { + m_currentTask = account->refresh(); + if(m_currentTask) { + connect(m_currentTask.get(), &AccountTask::succeeded, this, &AccountList::authSucceeded); + connect(m_currentTask.get(), &AccountTask::failed, this, &AccountList::authFailed); + m_currentTask->start(); + qDebug() << "RefreshSchedule: Processing account " << account->accountDisplayString() << " with internal ID " << accountId; + return; + } + } + } + qDebug() << "RefreshSchedule: Account with with internal ID " << accountId << " not found."; + } + endActivity(); + // if we get here, no account needed refreshing. Schedule refresh in an hour. + m_refreshTimer->start(std::chrono::hours(1)); +} + +void AccountList::authSucceeded() { + qDebug() << "RefreshSchedule: Background account refresh succeeded"; + m_currentTask.reset(); + endActivity(); + m_nextTimer->start(std::chrono::seconds(20)); +} + +void AccountList::authFailed(QString reason) { + qDebug() << "RefreshSchedule: Background account refresh failed: " << reason; + m_currentTask.reset(); + endActivity(); + m_nextTimer->start(std::chrono::seconds(20)); +} + +bool AccountList::isActive() const { + return m_activityCount != 0; +} + +void AccountList::beginActivity() { + bool activating = m_activityCount == 0; + m_activityCount++; + if(activating) { + emit activityChanged(true); + } +} + +void AccountList::endActivity() { + if(m_activityCount == 0) { + qWarning() << m_name << " - Activity count would become below zero"; + return; + } + bool deactivating = m_activityCount == 1; + m_activityCount--; + if(deactivating) { + emit activityChanged(false); + } +} diff --git a/launcher/minecraft/auth/AccountList.h b/launcher/minecraft/auth/AccountList.h index 08004628..75686c21 100644 --- a/launcher/minecraft/auth/AccountList.h +++ b/launcher/minecraft/auth/AccountList.h @@ -67,6 +67,8 @@ public: MinecraftAccountPtr getAccountByProfileName(const QString &profileName) const; QStringList profileNames() const; + void requestRefresh(QString accountId); + /*! * 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. @@ -85,10 +87,20 @@ public: void setDefaultAccount(MinecraftAccountPtr profileId); bool anyAccountIsValid(); + bool isActive() const; + +protected: + void beginActivity(); + void endActivity(); + +private: + const char* m_name; + uint32_t m_activityCount = 0; signals: void listChanged(); void listActivityChanged(); void defaultAccountChanged(); + void activityChanged(bool active); public slots: /** @@ -101,7 +113,23 @@ public slots: */ void accountActivityChanged(bool active); + /** + * This is initially to run background account refresh tasks, or on a hourly timer + */ + void fillQueue(); + +private slots: + void tryNext(); + + void authSucceeded(); + void authFailed(QString reason); + protected: + QList m_refreshQueue; + QTimer *m_refreshTimer; + QTimer *m_nextTimer; + shared_qobject_ptr m_currentTask; + /*! * Called whenever the list changes. * This emits the listChanged() signal and autosaves the list (if autosave is enabled). diff --git a/launcher/minecraft/auth/AccountTask.cpp b/launcher/minecraft/auth/AccountTask.cpp index 25d753de..98d8d94d 100644 --- a/launcher/minecraft/auth/AccountTask.cpp +++ b/launcher/minecraft/auth/AccountTask.cpp @@ -28,40 +28,79 @@ AccountTask::AccountTask(AccountData *data, QObject *parent) : Task(parent), m_data(data) { - changeState(STATE_CREATED); + changeState(AccountTaskState::STATE_CREATED); } QString AccountTask::getStateMessage() const { - switch (m_accountState) + switch (m_taskState) { - case STATE_CREATED: + case AccountTaskState::STATE_CREATED: return "Waiting..."; - case STATE_WORKING: + case AccountTaskState::STATE_WORKING: return tr("Sending request to auth servers..."); - case STATE_SUCCEEDED: + case AccountTaskState::STATE_SUCCEEDED: return tr("Authentication task succeeded."); - case STATE_FAILED_SOFT: + case AccountTaskState::STATE_OFFLINE: return tr("Failed to contact the authentication server."); - case STATE_FAILED_HARD: - return tr("Failed to authenticate."); - case STATE_FAILED_GONE: + case AccountTaskState::STATE_FAILED_SOFT: + return tr("Encountered an error during authentication."); + case AccountTaskState::STATE_FAILED_HARD: + return tr("Failed to authenticate. The session has expired."); + case AccountTaskState::STATE_FAILED_GONE: return tr("Failed to authenticate. The account no longer exists."); default: return tr("..."); } } -void AccountTask::changeState(AccountTask::State newState, QString reason) +bool AccountTask::changeState(AccountTaskState newState, QString reason) { - m_accountState = newState; + m_taskState = newState; setStatus(getStateMessage()); - if (newState == STATE_SUCCEEDED) - { - emitSucceeded(); - } - else if (newState == STATE_FAILED_HARD || newState == STATE_FAILED_SOFT || newState == STATE_FAILED_GONE) - { - emitFailed(reason); + switch(newState) { + case AccountTaskState::STATE_CREATED: { + m_data->errorString.clear(); + return true; + } + case AccountTaskState::STATE_WORKING: { + m_data->accountState = AccountState::Working; + return true; + } + case AccountTaskState::STATE_SUCCEEDED: { + m_data->accountState = AccountState::Online; + emitSucceeded(); + return false; + } + case AccountTaskState::STATE_OFFLINE: { + m_data->errorString = reason; + m_data->accountState = AccountState::Offline; + emitFailed(reason); + return false; + } + case AccountTaskState::STATE_FAILED_SOFT: { + m_data->errorString = reason; + m_data->accountState = AccountState::Errored; + emitFailed(reason); + return false; + } + case AccountTaskState::STATE_FAILED_HARD: { + m_data->errorString = reason; + m_data->accountState = AccountState::Expired; + emitFailed(reason); + return false; + } + case AccountTaskState::STATE_FAILED_GONE: { + m_data->errorString = reason; + m_data->accountState = AccountState::Gone; + emitFailed(reason); + return false; + } + default: { + QString error = tr("Unknown account task state: %1").arg(int(newState)); + m_data->accountState = AccountState::Errored; + emitFailed(error); + return false; + } } } diff --git a/launcher/minecraft/auth/AccountTask.h b/launcher/minecraft/auth/AccountTask.h index 4f3bd52a..dac3f1b5 100644 --- a/launcher/minecraft/auth/AccountTask.h +++ b/launcher/minecraft/auth/AccountTask.h @@ -26,62 +26,32 @@ class QNetworkReply; +/** + * Enum for describing the state of the current task. + * Used by the getStateMessage function to determine what the status message should be. + */ +enum class AccountTaskState +{ + STATE_CREATED, + STATE_WORKING, + STATE_SUCCEEDED, + STATE_FAILED_SOFT, //!< soft failure. authentication went through partially + STATE_FAILED_HARD, //!< hard failure. main tokens are invalid + STATE_FAILED_GONE, //!< hard failure. main tokens are invalid, and the account no longer exists + STATE_OFFLINE //!< soft failure. authentication failed in the first step in a 'soft' way +}; + class AccountTask : public Task { - friend class AuthContext; Q_OBJECT public: explicit AccountTask(AccountData * data, QObject *parent = 0); virtual ~AccountTask() {}; - /** - * assign a session to this task. the session will be filled with required infomration - * upon completion - */ - void assignSession(AuthSessionPtr session) - { - m_session = session; - } + AccountTaskState m_taskState = AccountTaskState::STATE_CREATED; - /// get the assigned session for filling with information. - AuthSessionPtr getAssignedSession() - { - return m_session; - } - - /** - * Class describing a Account error response. - */ - struct Error - { - QString m_errorMessageShort; - QString m_errorMessageVerbose; - QString m_cause; - }; - - enum AbortedBy - { - BY_NOTHING, - BY_USER, - BY_TIMEOUT - } m_aborted = BY_NOTHING; - - /** - * Enum for describing the state of the current task. - * Used by the getStateMessage function to determine what the status message should be. - */ - enum State - { - STATE_CREATED, - 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; - - State accountState() { - return m_accountState; + AccountTaskState taskState() { + return m_taskState; } signals: @@ -98,11 +68,9 @@ protected: virtual QString getStateMessage() const; protected slots: - void changeState(State newState, QString reason=QString()); + // NOTE: true -> non-terminal state, false -> terminal state + bool changeState(AccountTaskState newState, QString reason = QString()); protected: - // FIXME: segfault disaster waiting to happen AccountData *m_data = nullptr; - std::shared_ptr m_error; - AuthSessionPtr m_session; }; diff --git a/launcher/minecraft/auth/flows/AuthRequest.cpp b/launcher/minecraft/auth/AuthRequest.cpp similarity index 92% rename from launcher/minecraft/auth/flows/AuthRequest.cpp rename to launcher/minecraft/auth/AuthRequest.cpp index 82dba591..459d2354 100644 --- a/launcher/minecraft/auth/flows/AuthRequest.cpp +++ b/launcher/minecraft/auth/AuthRequest.cpp @@ -44,6 +44,7 @@ void AuthRequest::onRequestFinished() { if (reply_ != qobject_cast(sender())) { return; } + httpStatus_ = 200; finish(); } @@ -55,10 +56,11 @@ void AuthRequest::onRequestError(QNetworkReply::NetworkError error) { 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(); + errorString_ = reply_->errorString(); + httpStatus_ = reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); error_ = error; + qWarning() << "AuthRequest::onRequestError: Error string: " << errorString_; + qWarning() << "AuthRequest::onRequestError: HTTP status" << httpStatus_ << reply_->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); // QTimer::singleShot(10, this, SLOT(finish())); } @@ -103,6 +105,8 @@ void AuthRequest::setup(const QNetworkRequest &req, QNetworkAccessManager::Opera status_ = Requesting; error_ = QNetworkReply::NoError; + errorString_.clear(); + httpStatus_ = 0; } void AuthRequest::finish() { diff --git a/launcher/minecraft/auth/flows/AuthRequest.h b/launcher/minecraft/auth/AuthRequest.h similarity index 95% rename from launcher/minecraft/auth/flows/AuthRequest.h rename to launcher/minecraft/auth/AuthRequest.h index a547aea4..89f7a123 100644 --- a/launcher/minecraft/auth/flows/AuthRequest.h +++ b/launcher/minecraft/auth/AuthRequest.h @@ -46,6 +46,11 @@ protected slots: /// Handle upload progress. void onUploadProgress(qint64 uploaded, qint64 total); +public: + QNetworkReply::NetworkError error_; + int httpStatus_ = 0; + QString errorString_; + protected: void setup(const QNetworkRequest &request, QNetworkAccessManager::Operation operation, const QByteArray &verb = QByteArray()); @@ -60,5 +65,6 @@ protected: QNetworkAccessManager::Operation operation_; QUrl url_; Katabasis::ReplyList timedReplies_; - QNetworkReply::NetworkError error_; + + QTimer *timer_; }; diff --git a/launcher/minecraft/auth/AuthStep.cpp b/launcher/minecraft/auth/AuthStep.cpp new file mode 100644 index 00000000..ffa2581b --- /dev/null +++ b/launcher/minecraft/auth/AuthStep.cpp @@ -0,0 +1,7 @@ +#include "AuthStep.h" + +AuthStep::AuthStep(AccountData *data) : QObject(nullptr), m_data(data) { +} + +AuthStep::~AuthStep() noexcept = default; + diff --git a/launcher/minecraft/auth/AuthStep.h b/launcher/minecraft/auth/AuthStep.h new file mode 100644 index 00000000..2a8dc2ca --- /dev/null +++ b/launcher/minecraft/auth/AuthStep.h @@ -0,0 +1,33 @@ +#pragma once +#include +#include +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AccountData.h" +#include "AccountTask.h" + +class AuthStep : public QObject { + Q_OBJECT + +public: + using Ptr = shared_qobject_ptr; + +public: + explicit AuthStep(AccountData *data); + virtual ~AuthStep() noexcept; + + virtual QString describe() = 0; + +public slots: + virtual void perform() = 0; + virtual void rehydrate() = 0; + +signals: + void finished(AccountTaskState resultingState, QString message); + void showVerificationUriAndCode(const QUrl &uri, const QString &code, int expiresIn); + void hideVerificationUriAndCode(); + +protected: + AccountData *m_data; +}; diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp index 30ed6afe..7ce87a3d 100644 --- a/launcher/minecraft/auth/MinecraftAccount.cpp +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -16,7 +16,6 @@ */ #include "MinecraftAccount.h" -#include "flows/AuthContext.h" #include #include @@ -28,14 +27,12 @@ #include #include -#include "flows/MSASilent.h" -#include "flows/MSAInteractive.h" -#include "flows/MojangRefresh.h" -#include "flows/MojangLogin.h" +#include "flows/MSA.h" +#include "flows/Mojang.h" MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) { - m_internalId = QUuid::createUuid().toString().remove(QRegExp("[{}-]")); + data.internalId = QUuid::createUuid().toString().remove(QRegExp("[{}-]")); } @@ -77,42 +74,10 @@ 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; - } +AccountState MinecraftAccount::accountState() const { + return data.accountState; } -bool MinecraftAccount::isExpired() const { - switch(data.type) { - case AccountType::Mojang: { - return data.accessToken().isEmpty(); - } - break; - case AccountType::MSA: { - return data.msaToken.validity == Katabasis::Validity::None; - } - break; - default: { - return true; - } - } -} - - QPixmap MinecraftAccount::getFace() const { QPixmap skinTexture; if(!skinTexture.loadFromData(data.minecraftProfile.skin.data, "PNG")) { @@ -126,136 +91,51 @@ QPixmap MinecraftAccount::getFace() const { } -shared_qobject_ptr MinecraftAccount::login(AuthSessionPtr session, QString password) -{ +shared_qobject_ptr MinecraftAccount::login(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))); - emit activityChanged(true); - } + m_currentTask.reset(new MojangLogin(&data, password)); + connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); + connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); + emit activityChanged(true); return m_currentTask; } -shared_qobject_ptr MinecraftAccount::loginMSA(AuthSessionPtr session) { +shared_qobject_ptr MinecraftAccount::loginMSA() { 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))); - emit activityChanged(true); - } + m_currentTask.reset(new MSAInteractive(&data)); + connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); + connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); + emit activityChanged(true); return m_currentTask; } -shared_qobject_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; +shared_qobject_ptr MinecraftAccount::refresh() { + if(m_currentTask) { + return m_currentTask; } - if(accountStatus() == Verified && !session->wants_online) - { - session->status = AuthSession::PlayableOffline; - session->auth_server_online = false; - fillSession(session); - return nullptr; + if(data.type == AccountType::MSA) { + m_currentTask.reset(new MSASilent(&data)); + } + else { + m_currentTask.reset(new MojangRefresh(&data)); } - 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))); - emit activityChanged(true); - } + connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); + connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); + emit activityChanged(true); + return m_currentTask; +} + +shared_qobject_ptr MinecraftAccount::currentTask() { return m_currentTask; } void MinecraftAccount::authSucceeded() { - auto session = m_currentTask->getAssignedSession(); - if (session) - { - /* - session->status = AuthSession::RequiresProfileSetup; - session->auth_server_online = true; - */ - if(data.profileId().size() == 0) { - session->status = AuthSession::RequiresProfileSetup; - } - else { - if(session->wants_online) { - session->status = AuthSession::PlayableOnline; - } - else { - session->status = AuthSession::PlayableOffline; - } - } - fillSession(session); - session->auth_server_online = true; - } m_currentTask.reset(); emit changed(); emit activityChanged(false); @@ -263,62 +143,35 @@ void MinecraftAccount::authSucceeded() 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 - 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); - } + switch (m_currentTask->taskState()) { + case AccountTaskState::STATE_OFFLINE: + case AccountTaskState::STATE_FAILED_SOFT: { + // NOTE: this doesn't do much. There was an error of some sort. } break; - case AccountTask::STATE_FAILED_HARD: { - // FIXME: MSA data clearing - data.yggdrasilToken.token = QString(); - data.yggdrasilToken.validity = Katabasis::Validity::None; + case AccountTaskState::STATE_FAILED_HARD: { + if(isMSA()) { + data.msaToken.token = QString(); + data.msaToken.refresh_token = QString(); + data.msaToken.validity = Katabasis::Validity::None; + data.validity_ = Katabasis::Validity::None; + } + else { + data.yggdrasilToken.token = QString(); + data.yggdrasilToken.validity = Katabasis::Validity::None; + data.validity_ = Katabasis::Validity::None; + } + emit changed(); + } + break; + case AccountTaskState::STATE_FAILED_GONE: { 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); - } } 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); - } - } - break; - case AccountTask::STATE_CREATED: - case AccountTask::STATE_WORKING: - case AccountTask::STATE_SUCCEEDED: { + case AccountTaskState::STATE_CREATED: + case AccountTaskState::STATE_WORKING: + case AccountTaskState::STATE_SUCCEEDED: { // Not reachable here, as they are not failures. } } @@ -366,6 +219,18 @@ bool MinecraftAccount::shouldRefresh() const { void MinecraftAccount::fillSession(AuthSessionPtr session) { + if(ownsMinecraft() && !hasProfile()) { + session->status = AuthSession::RequiresProfileSetup; + } + else { + if(session->wants_online) { + session->status = AuthSession::PlayableOnline; + } + else { + session->status = AuthSession::PlayableOffline; + } + } + // the user name. you have to have an user name // FIXME: not with MSA session->username = data.userName(); diff --git a/launcher/minecraft/auth/MinecraftAccount.h b/launcher/minecraft/auth/MinecraftAccount.h index 459ef903..18f142c4 100644 --- a/launcher/minecraft/auth/MinecraftAccount.h +++ b/launcher/minecraft/auth/MinecraftAccount.h @@ -24,6 +24,7 @@ #include #include + #include "AuthSession.h" #include "Usable.h" #include "AccountData.h" @@ -50,12 +51,6 @@ struct AccountProfile bool legacy; }; -enum AccountStatus -{ - NotVerified, - Verified -}; - /** * Object that stores information about a certain Mojang account. * @@ -90,15 +85,17 @@ public: /* manipulation */ * Attempt to login. Empty password means we use the token. * If the attempt fails because we already are performing some task, it returns false. */ - shared_qobject_ptr login(AuthSessionPtr session, QString password); + shared_qobject_ptr login(QString password); - shared_qobject_ptr loginMSA(AuthSessionPtr session); + shared_qobject_ptr loginMSA(); - shared_qobject_ptr refresh(AuthSessionPtr session); + shared_qobject_ptr refresh(); + + shared_qobject_ptr currentTask(); public: /* queries */ QString internalId() const { - return m_internalId; + return data.internalId; } QString accountDisplayString() const { @@ -123,8 +120,6 @@ public: /* queries */ bool isActive() const; - bool isExpired() const; - bool canMigrate() const { return data.canMigrateToMSA; } @@ -133,6 +128,14 @@ public: /* queries */ return data.type == AccountType::MSA; } + bool ownsMinecraft() const { + return data.minecraftEntitlement.ownsMinecraft; + } + + bool hasProfile() const { + return data.profileId().size() != 0; + } + QString typeString() const { switch(data.type) { case AccountType::Mojang: { @@ -154,8 +157,8 @@ public: /* queries */ QPixmap getFace() const; - //! Returns whether the account is NotVerified, Verified or Online - AccountStatus accountStatus() const; + //! Returns the current state of the account + AccountState accountState() const; AccountData * accountData() { return &data; @@ -163,6 +166,8 @@ public: /* queries */ bool shouldRefresh() const; + void fillSession(AuthSessionPtr session); + signals: /** * This signal is emitted when the account changes @@ -174,7 +179,6 @@ signals: // TODO: better signalling for the various possible state changes - especially errors protected: /* variables */ - QString m_internalId; AccountData data; // current task we are executing here @@ -189,7 +193,4 @@ private slots: void authSucceeded(); void authFailed(QString reason); - -private: - void fillSession(AuthSessionPtr session); }; diff --git a/launcher/minecraft/auth/flows/Parsers.cpp b/launcher/minecraft/auth/Parsers.cpp similarity index 99% rename from launcher/minecraft/auth/flows/Parsers.cpp rename to launcher/minecraft/auth/Parsers.cpp index ecb11cf9..4cab78ef 100644 --- a/launcher/minecraft/auth/flows/Parsers.cpp +++ b/launcher/minecraft/auth/Parsers.cpp @@ -72,7 +72,7 @@ bool getBool(QJsonValue value, bool & out) { // 2148916238 = child account not linked to a family */ -bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output, const char * name) { +bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output, QString name) { qDebug() << "Parsing" << name <<":"; #ifndef NDEBUG qDebug() << data; diff --git a/launcher/minecraft/auth/flows/Parsers.h b/launcher/minecraft/auth/Parsers.h similarity index 92% rename from launcher/minecraft/auth/flows/Parsers.h rename to launcher/minecraft/auth/Parsers.h index b484a073..dac7f69b 100644 --- a/launcher/minecraft/auth/flows/Parsers.h +++ b/launcher/minecraft/auth/Parsers.h @@ -1,6 +1,6 @@ #pragma once -#include "../AccountData.h" +#include "AccountData.h" namespace Parsers { @@ -10,7 +10,7 @@ namespace Parsers bool getNumber(QJsonValue value, int64_t & out); bool getBool(QJsonValue value, bool & out); - bool parseXTokenResponse(QByteArray &data, Katabasis::Token &output, const char * name); + bool parseXTokenResponse(QByteArray &data, Katabasis::Token &output, QString name); bool parseMojangResponse(QByteArray &data, Katabasis::Token &output); bool parseMinecraftProfile(QByteArray &data, MinecraftProfile &output); diff --git a/launcher/minecraft/auth/flows/Yggdrasil.cpp b/launcher/minecraft/auth/Yggdrasil.cpp similarity index 89% rename from launcher/minecraft/auth/flows/Yggdrasil.cpp rename to launcher/minecraft/auth/Yggdrasil.cpp index 5ea168e8..7ac842a6 100644 --- a/launcher/minecraft/auth/flows/Yggdrasil.cpp +++ b/launcher/minecraft/auth/Yggdrasil.cpp @@ -14,7 +14,7 @@ */ #include "Yggdrasil.h" -#include "../AccountData.h" +#include "AccountData.h" #include #include @@ -30,11 +30,11 @@ Yggdrasil::Yggdrasil(AccountData *data, QObject *parent) : AccountTask(data, parent) { - changeState(STATE_CREATED); + changeState(AccountTaskState::STATE_CREATED); } void Yggdrasil::sendRequest(QUrl endpoint, QByteArray content) { - changeState(STATE_WORKING); + changeState(AccountTaskState::STATE_WORKING); QNetworkRequest netRequest(endpoint); netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); @@ -185,14 +185,14 @@ void Yggdrasil::processResponse(QJsonObject responseData) { 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.")); + changeState(AccountTaskState::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.")); + changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server attempted to change the client token. This isn't supported.")); return; } @@ -201,7 +201,7 @@ void Yggdrasil::processResponse(QJsonObject responseData) { 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.")); + changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send an access token.")); return; } // Set the access token. @@ -212,25 +212,25 @@ void Yggdrasil::processResponse(QJsonObject responseData) { // 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); + changeState(AccountTaskState::STATE_SUCCEEDED); } void Yggdrasil::processReply() { - changeState(STATE_WORKING); + changeState(AccountTaskState::STATE_WORKING); switch (m_netReply->error()) { case QNetworkReply::NoError: break; case QNetworkReply::TimeoutError: - changeState(STATE_FAILED_SOFT, tr("Authentication operation timed out.")); + changeState(AccountTaskState::STATE_FAILED_SOFT, tr("Authentication operation timed out.")); return; case QNetworkReply::OperationCanceledError: - changeState(STATE_FAILED_SOFT, tr("Authentication operation cancelled.")); + changeState(AccountTaskState::STATE_FAILED_SOFT, tr("Authentication operation cancelled.")); return; case QNetworkReply::SslHandshakeFailedError: changeState( - STATE_FAILED_SOFT, + AccountTaskState::STATE_FAILED_SOFT, tr( "SSL Handshake failed.
There might be a few causes for it:
" "
    " @@ -248,13 +248,13 @@ void Yggdrasil::processReply() { break; case QNetworkReply::ContentGoneError: { changeState( - STATE_FAILED_GONE, + AccountTaskState::STATE_FAILED_GONE, tr("The Mojang account no longer exists. It may have been migrated to a Microsoft account.") ); } default: changeState( - STATE_FAILED_SOFT, + AccountTaskState::STATE_FAILED_SOFT, tr("Authentication operation failed due to a network error: %1 (%2)").arg(m_netReply->errorString()).arg(m_netReply->error()) ); return; @@ -279,7 +279,7 @@ void Yggdrasil::processReply() { } else { changeState( - STATE_FAILED_SOFT, + AccountTaskState::STATE_FAILED_SOFT, tr("Failed to parse authentication server response JSON response: %1 at offset %2.").arg(jsonError.errorString()).arg(jsonError.offset) ); qCritical() << replyData; @@ -303,7 +303,7 @@ void Yggdrasil::processReply() { // error. qDebug() << "The request failed and the server gave no error message. Unknown error."; changeState( - STATE_FAILED_SOFT, + AccountTaskState::STATE_FAILED_SOFT, tr("An unknown error occurred when trying to communicate with the authentication server: %1").arg(m_netReply->errorString()) ); } @@ -322,10 +322,10 @@ void Yggdrasil::processError(QJsonObject responseData) { causeVal.toString("") } ); - changeState(STATE_FAILED_HARD, m_error->m_errorMessageVerbose); + changeState(AccountTaskState::STATE_FAILED_HARD, m_error->m_errorMessageVerbose); } else { // Error is not in standard format. Don't set m_error and return unknown error. - changeState(STATE_FAILED_HARD, tr("An unknown Yggdrasil error occurred.")); + changeState(AccountTaskState::STATE_FAILED_HARD, tr("An unknown Yggdrasil error occurred.")); } } diff --git a/launcher/minecraft/auth/flows/Yggdrasil.h b/launcher/minecraft/auth/Yggdrasil.h similarity index 86% rename from launcher/minecraft/auth/flows/Yggdrasil.h rename to launcher/minecraft/auth/Yggdrasil.h index b9670ec7..4f52a04c 100644 --- a/launcher/minecraft/auth/flows/Yggdrasil.h +++ b/launcher/minecraft/auth/Yggdrasil.h @@ -15,14 +15,14 @@ #pragma once -#include "../AccountTask.h" +#include "AccountTask.h" #include #include #include #include -#include "../MinecraftAccount.h" +#include "MinecraftAccount.h" class QNetworkAccessManager; class QNetworkReply; @@ -38,10 +38,26 @@ public: AccountData *data, QObject *parent = 0 ); - virtual ~Yggdrasil() {}; + virtual ~Yggdrasil() = default; void refresh(); void login(QString password); + + struct Error + { + QString m_errorMessageShort; + QString m_errorMessageVerbose; + QString m_cause; + }; + std::shared_ptr m_error; + + enum AbortedBy + { + BY_NOTHING, + BY_USER, + BY_TIMEOUT + } m_aborted = BY_NOTHING; + protected: void executeTask() override; diff --git a/launcher/minecraft/auth/flows/AuthContext.cpp b/launcher/minecraft/auth/flows/AuthContext.cpp deleted file mode 100644 index 00957fd4..00000000 --- a/launcher/minecraft/auth/flows/AuthContext.cpp +++ /dev/null @@ -1,671 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "AuthContext.h" -#include "katabasis/Globals.h" -#include "AuthRequest.h" - -#include "Parsers.h" - -#include - -using OAuth2 = Katabasis::DeviceFlow; -using Activity = Katabasis::Activity; - -AuthContext::AuthContext(AccountData * data, QObject *parent) : - AccountTask(data, parent) -{ -} - -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; - setStage(AuthStage::Complete); - m_data->validity_ = m_data->minecraftProfile.validity; - emit activityChanged(m_activity); -} - -void AuthContext::initMSA() { - if(m_oauth2) { - return; - } - - OAuth2::Options opts; - opts.scope = "XboxLive.signin offline_access"; - opts.clientIdentifier = APPLICATION->msaClientId(); - opts.authorizationUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode"; - opts.accessTokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"; - - // FIXME: OAuth2 is not aware of our fancy shared pointers - m_oauth2 = new OAuth2(opts, m_data->msaToken, this, APPLICATION->network().get()); - - connect(m_oauth2, &OAuth2::activityChanged, this, &AuthContext::onOAuthActivityChanged); - connect(m_oauth2, &OAuth2::showVerificationUriAndCode, this, &AuthContext::showVerificationUriAndCode); -} - -void AuthContext::initMojang() { - if(m_yggdrasil) { - return; - } - m_yggdrasil = new Yggdrasil(m_data, this); - - connect(m_yggdrasil, &Task::failed, this, &AuthContext::onMojangFailed); - connect(m_yggdrasil, &Task::succeeded, this, &AuthContext::onMojangSucceeded); -} - -void AuthContext::onMojangSucceeded() { - doMinecraftProfile(); -} - - -void AuthContext::onMojangFailed() { - finishActivity(); - m_error = m_yggdrasil->m_error; - m_aborted = m_yggdrasil->m_aborted; - changeState(m_yggdrasil->accountState(), tr("Mojang user authentication failed.")); -} - -void AuthContext::onOAuthActivityChanged(Katabasis::Activity activity) { - switch(activity) { - case Katabasis::Activity::Idle: - case Katabasis::Activity::LoggingIn: - case Katabasis::Activity::Refreshing: - case Katabasis::Activity::LoggingOut: { - // We asked it to do something, it's doing it. Nothing to act upon. - return; - } - case Katabasis::Activity::Succeeded: { - // Succeeded or did not invalidate tokens - emit hideVerificationUriAndCode(); - if (!m_oauth2->linked()) { - finishActivity(); - changeState(STATE_FAILED_HARD, tr("Microsoft user authentication ended with an impossible state (succeeded, but not succeeded at the same time).")); - return; - } - QVariantMap extraTokens = m_oauth2->extraTokens(); -#ifndef NDEBUG - if (!extraTokens.isEmpty()) { - qDebug() << "Extra tokens in response:"; - foreach (QString key, extraTokens.keys()) { - qDebug() << "\t" << key << ":" << extraTokens.value(key); - } - } -#endif - doUserAuth(); - return; - } - case Katabasis::Activity::FailedSoft: { - emit hideVerificationUriAndCode(); - finishActivity(); - changeState(STATE_FAILED_SOFT, tr("Microsoft user authentication failed with a soft error.")); - return; - } - case Katabasis::Activity::FailedGone: - case Katabasis::Activity::FailedHard: { - emit hideVerificationUriAndCode(); - finishActivity(); - changeState(STATE_FAILED_HARD, tr("Microsoft user authentication failed.")); - return; - } - default: { - emit hideVerificationUriAndCode(); - finishActivity(); - changeState(STATE_FAILED_HARD, tr("Microsoft user authentication completed with an unrecognized result.")); - return; - } - - } -} - -void AuthContext::doUserAuth() { - setStage(AuthStage::UserAuth); - changeState(STATE_WORKING, tr("Starting user authentication")); - - QString xbox_auth_template = R"XXX( -{ - "Properties": { - "AuthMethod": "RPS", - "SiteName": "user.auth.xboxlive.com", - "RpsTicket": "d=%1" - }, - "RelyingParty": "http://auth.xboxlive.com", - "TokenType": "JWT" -} -)XXX"; - 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 AuthRequest(this); - connect(requestor, &AuthRequest::finished, this, &AuthContext::onUserAuthDone); - requestor->post(request, xbox_auth_data.toUtf8()); - qDebug() << "First layer of XBox auth ... commencing."; -} - -void AuthContext::onUserAuthDone( - QNetworkReply::NetworkError error, - QByteArray replyData, - QList headers -) { - if (error != QNetworkReply::NoError) { - qWarning() << "Reply error:" << error; - finishActivity(); - changeState(STATE_FAILED_HARD, tr("XBox user authentication failed.")); - return; - } - - Katabasis::Token temp; - if(!Parsers::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.")); - return; - } - m_data->userToken = temp; - - setStage(AuthStage::XboxAuth); - changeState(STATE_WORKING, tr("Starting XBox authentication")); - - doSTSAuthMinecraft(); - doSTSAuthGeneric(); -} -/* - url = "https://xsts.auth.xboxlive.com/xsts/authorize" - headers = {"x-xbl-contract-version": "1"} - data = { - "RelyingParty": relying_party, - "TokenType": "JWT", - "Properties": { - "UserTokens": [self.user_token.token], - "SandboxId": "RETAIL", - }, - } -*/ -void AuthContext::doSTSAuthMinecraft() { - QString xbox_auth_template = R"XXX( -{ - "Properties": { - "SandboxId": "RETAIL", - "UserTokens": [ - "%1" - ] - }, - "RelyingParty": "rp://api.minecraftservices.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"); - AuthRequest *requestor = new AuthRequest(this); - connect(requestor, &AuthRequest::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(!Parsers::getNumber(obj.value("XErr"), errorCode)) { - qWarning() << "XErr is not a number"; - return; - } - stsErrors.insert(errorCode); - stsFailed = true; - } -} - - -void AuthContext::onSTSAuthMinecraftDone( - 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_mcAuthSucceeded); - return; - } - - Katabasis::Token temp; - if(!Parsers::parseXTokenResponse(replyData, temp, "STSAuthMinecraft")) { - qWarning() << "Could not parse authorization response for access to mojang services..."; - 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"; - failResult(m_mcAuthSucceeded); - return; - } - m_data->mojangservicesToken = temp; - - doMinecraftAuth(); -} - -void AuthContext::doMinecraftAuth() { - auto requestURL = "https://api.minecraftservices.com/launcher/login"; - auto uhs = m_data->mojangservicesToken.extra["uhs"].toString(); - auto xToken = m_data->mojangservicesToken.token; - - QString mc_auth_template = R"XXX( -{ - "xtoken": "XBL3.0 x=%1;%2", - "platform": "PC_LAUNCHER" -} -)XXX"; - auto requestBody = mc_auth_template.arg(uhs, xToken); - - QNetworkRequest request = QNetworkRequest(QUrl(requestURL)); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - request.setRawHeader("Accept", "application/json"); - AuthRequest *requestor = new AuthRequest(this); - connect(requestor, &AuthRequest::finished, this, &AuthContext::onMinecraftAuthDone); - requestor->post(request, requestBody.toUtf8()); - qDebug() << "Getting Minecraft access token..."; -} - -void AuthContext::onMinecraftAuthDone( - QNetworkReply::NetworkError error, - QByteArray replyData, - QList headers -) { - qDebug() << replyData; - if (error != QNetworkReply::NoError) { - qWarning() << "Reply error:" << error; -#ifndef NDEBUG - qDebug() << replyData; -#endif - failResult(m_mcAuthSucceeded); - return; - } - - if(!Parsers::parseMojangResponse(replyData, m_data->yggdrasilToken)) { - qWarning() << "Could not parse login_with_xbox response..."; -#ifndef NDEBUG - qDebug() << replyData; -#endif - failResult(m_mcAuthSucceeded); - return; - } - - 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"); - AuthRequest *requestor = new AuthRequest(this); - connect(requestor, &AuthRequest::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(!Parsers::parseXTokenResponse(replyData, temp, "STSAuthGeneric")) { - 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() { - auto url = QUrl("https://profile.xboxlive.com/users/me/profile/settings"); - QUrlQuery q; - q.addQueryItem( - "settings", - "GameDisplayName,AppDisplayName,AppDisplayPicRaw,GameDisplayPicRaw," - "PublicGamerpic,ShowUserAsAvatar,Gamerscore,Gamertag,ModernGamertag,ModernGamertagSuffix," - "UniqueModernGamertag,AccountTier,TenureLevel,XboxOneRep," - "PreferredColor,Location,Bio,Watermarks," - "RealName,RealNameOverride,IsQuarantined" - ); - url.setQuery(q); - - QNetworkRequest request = QNetworkRequest(url); - 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_data->userToken.extra["uhs"].toString(), m_data->xboxApiToken.token).toUtf8()); - AuthRequest *requestor = new AuthRequest(this); - connect(requestor, &AuthRequest::finished, this, &AuthContext::onXBoxProfileDone); - requestor->get(request); - qDebug() << "Getting Xbox profile..."; -} - -void AuthContext::onXBoxProfileDone( - QNetworkReply::NetworkError error, - QByteArray replyData, - QList headers -) { - if (error != QNetworkReply::NoError) { - qWarning() << "Reply error:" << error; -#ifndef NDEBUG - qDebug() << replyData; -#endif - failResult(m_xboxProfileSucceeded); - return; - } - -#ifndef NDEBUG - qDebug() << "XBox profile: " << replyData; -#endif - - succeedResult(m_xboxProfileSucceeded); -} - -void AuthContext::succeedResult(bool& flag) { - m_requestsDone ++; - flag = true; - checkResult(); -} - -void AuthContext::failResult(bool& flag) { - m_requestsDone ++; - flag = false; - checkResult(); -} - -void AuthContext::checkResult() { - qDebug() << "AuthContext::checkResult called"; - if(m_requestsDone != 2) { - qDebug() << "Number of ready results:" << m_requestsDone; - return; - } - if(m_mcAuthSucceeded && m_xboxProfileSucceeded) { - doEntitlements(); - } - else { - finishActivity(); - 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")); - } - } -} - -void AuthContext::doEntitlements() { - auto uuid = QUuid::createUuid(); - entitlementsRequestId = uuid.toString().remove('{').remove('}'); - auto url = "https://api.minecraftservices.com/entitlements/license?requestId=" + entitlementsRequestId; - QNetworkRequest request = QNetworkRequest(url); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - request.setRawHeader("Accept", "application/json"); - request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8()); - AuthRequest *requestor = new AuthRequest(this); - connect(requestor, &AuthRequest::finished, this, &AuthContext::onEntitlementsDone); - requestor->get(request); - qDebug() << "Getting Xbox profile..."; -} - - -void AuthContext::onEntitlementsDone( - QNetworkReply::NetworkError error, - QByteArray data, - QList headers -) { -#ifndef NDEBUG - qDebug() << data; -#endif - // TODO: check presence of same entitlementsRequestId? - // TODO: validate JWTs? - Parsers::parseMinecraftEntitlements(data, m_data->minecraftEntitlement); - doMinecraftProfile(); -} - -void AuthContext::doMinecraftProfile() { - setStage(AuthStage::MinecraftProfile); - changeState(STATE_WORKING, tr("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_data->yggdrasilToken.token).toUtf8()); - - AuthRequest *requestor = new AuthRequest(this); - connect(requestor, &AuthRequest::finished, this, &AuthContext::onMinecraftProfileDone); - requestor->get(request); -} - -void AuthContext::onMinecraftProfileDone( - QNetworkReply::NetworkError error, - QByteArray data, - QList headers -) { -#ifndef NDEBUG - qDebug() << data; -#endif - if (error == QNetworkReply::ContentNotFoundError) { - // NOTE: Succeed even if we do not have a profile. This is a valid account state. - if(m_data->type == AccountType::Mojang) { - m_data->minecraftEntitlement.canPlayMinecraft = false; - m_data->minecraftEntitlement.ownsMinecraft = false; - } - m_data->minecraftProfile = MinecraftProfile(); - succeed(); - return; - } - if (error != QNetworkReply::NoError) { - finishActivity(); - changeState(STATE_FAILED_HARD, tr("Minecraft Java profile acquisition failed.")); - return; - } - if(!Parsers::parseMinecraftProfile(data, m_data->minecraftProfile)) { - m_data->minecraftProfile = MinecraftProfile(); - finishActivity(); - changeState(STATE_FAILED_HARD, tr("Minecraft Java profile response could not be parsed")); - return; - } - - if(m_data->type == AccountType::Mojang) { - auto validProfile = m_data->minecraftProfile.validity == Katabasis::Validity::Certain; - m_data->minecraftEntitlement.canPlayMinecraft = validProfile; - m_data->minecraftEntitlement.ownsMinecraft = validProfile; - 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()); - - AuthRequest *requestor = new AuthRequest(this); - connect(requestor, &AuthRequest::finished, this, &AuthContext::onMigrationEligibilityCheckDone); - requestor->get(request); -} - -void AuthContext::onMigrationEligibilityCheckDone( - QNetworkReply::NetworkError error, - QByteArray data, - QList headers -) { - if (error == QNetworkReply::NoError) { - Parsers::parseRolloutResponse(data, m_data->canMigrateToMSA); - } - doGetSkin(); -} - -void AuthContext::doGetSkin() { - setStage(AuthStage::Skin); - changeState(STATE_WORKING, tr("Fetching player skin")); - - auto url = QUrl(m_data->minecraftProfile.skin.url); - QNetworkRequest request = QNetworkRequest(url); - AuthRequest *requestor = new AuthRequest(this); - connect(requestor, &AuthRequest::finished, this, &AuthContext::onSkinDone); - requestor->get(request); -} - -void AuthContext::onSkinDone( - QNetworkReply::NetworkError error, - QByteArray data, - QList -) { - if (error == QNetworkReply::NoError) { - m_data->minecraftProfile.skin.data = data; - } - succeed(); - -} - -void AuthContext::succeed() { - m_data->validity_ = Katabasis::Validity::Certain; - finishActivity(); - 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 AuthStage::Initial: { - QString loginMessage = tr("Logging in as %1 user"); - if(m_data->type == AccountType::MSA) { - return loginMessage.arg("Microsoft"); - } - else { - return loginMessage.arg("Mojang"); - } - } - case AuthStage::UserAuth: - return tr("Logging in as XBox user"); - case AuthStage::XboxAuth: - 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: - return tr("Finished"); - default: - break; - } - default: - return AccountTask::getStateMessage(); - } -} diff --git a/launcher/minecraft/auth/flows/AuthContext.h b/launcher/minecraft/auth/flows/AuthContext.h deleted file mode 100644 index 5e4e9edc..00000000 --- a/launcher/minecraft/auth/flows/AuthContext.h +++ /dev/null @@ -1,110 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -#include -#include "Yggdrasil.h" -#include "../AccountData.h" -#include "../AccountTask.h" - -class AuthContext : public AccountTask -{ - Q_OBJECT - -public: - explicit AuthContext(AccountData * data, QObject *parent = 0); - - bool isBusy() { - return m_activity != Katabasis::Activity::Idle; - }; - Katabasis::Validity validity() { - return m_data->validity_; - }; - - //bool signOut(); - - QString getStateMessage() const override; - -signals: - void activityChanged(Katabasis::Activity activity); - -private slots: -// OAuth-specific callbacks - void onOAuthActivityChanged(Katabasis::Activity activity); - -// Yggdrasil specific callbacks - void onMojangSucceeded(); - void onMojangFailed(); - -protected: - void initMSA(); - void initMojang(); - - void doUserAuth(); - Q_SLOT void onUserAuthDone(QNetworkReply::NetworkError, QByteArray, QList); - - void processSTSError(QNetworkReply::NetworkError, QByteArray, QList); - - void doSTSAuthMinecraft(); - Q_SLOT void onSTSAuthMinecraftDone(QNetworkReply::NetworkError, QByteArray, QList); - void doMinecraftAuth(); - Q_SLOT void onMinecraftAuthDone(QNetworkReply::NetworkError, QByteArray, QList); - - void doSTSAuthGeneric(); - Q_SLOT void onSTSAuthGenericDone(QNetworkReply::NetworkError, QByteArray, QList); - void doXBoxProfile(); - Q_SLOT void onXBoxProfileDone(QNetworkReply::NetworkError, QByteArray, QList); - - void doEntitlements(); - Q_SLOT void onEntitlementsDone(QNetworkReply::NetworkError, QByteArray, QList); - - void doMinecraftProfile(); - Q_SLOT void onMinecraftProfileDone(QNetworkReply::NetworkError, QByteArray, QList); - - void doMigrationEligibilityCheck(); - Q_SLOT void onMigrationEligibilityCheckDone(QNetworkReply::NetworkError, QByteArray, QList); - - void doGetSkin(); - Q_SLOT void onSkinDone(QNetworkReply::NetworkError, QByteArray, QList); - - void succeed(); - - void failResult(bool & flag); - void succeedResult(bool & flag); - void checkResult(); - -protected: - void beginActivity(Katabasis::Activity activity); - void finishActivity(); - void clearTokens(); - -protected: - Katabasis::DeviceFlow *m_oauth2 = nullptr; - Yggdrasil *m_yggdrasil = nullptr; - - int m_requestsDone = 0; - bool m_xboxProfileSucceeded = false; - bool m_mcAuthSucceeded = false; - QString entitlementsRequestId; - - QSet stsErrors; - bool stsFailed = false; - - Katabasis::Activity m_activity = Katabasis::Activity::Idle; - enum class AuthStage { - Initial, - UserAuth, - XboxAuth, - MinecraftProfile, - MigrationEligibility, - Skin, - Complete - } m_stage = AuthStage::Initial; - - void setStage(AuthStage stage); -}; diff --git a/launcher/minecraft/auth/flows/AuthFlow.cpp b/launcher/minecraft/auth/flows/AuthFlow.cpp new file mode 100644 index 00000000..4f78e8c3 --- /dev/null +++ b/launcher/minecraft/auth/flows/AuthFlow.cpp @@ -0,0 +1,71 @@ +#include +#include +#include +#include + +#include "AuthFlow.h" +#include "katabasis/Globals.h" + +#include + +AuthFlow::AuthFlow(AccountData * data, QObject *parent) : + AccountTask(data, parent) +{ +} + +void AuthFlow::succeed() { + m_data->validity_ = Katabasis::Validity::Certain; + changeState( + AccountTaskState::STATE_SUCCEEDED, + tr("Finished all authentication steps") + ); +} + +void AuthFlow::executeTask() { + if(m_currentStep) { + return; + } + changeState(AccountTaskState::STATE_WORKING, tr("Initializing")); + nextStep(); +} + +void AuthFlow::nextStep() { + if(m_steps.size() == 0) { + // we got to the end without an incident... assume this is all. + m_currentStep.reset(); + succeed(); + return; + } + m_currentStep = m_steps.front(); + qDebug() << "AuthFlow:" << m_currentStep->describe(); + m_steps.pop_front(); + connect(m_currentStep.get(), &AuthStep::finished, this, &AuthFlow::stepFinished); + connect(m_currentStep.get(), &AuthStep::showVerificationUriAndCode, this, &AuthFlow::showVerificationUriAndCode); + connect(m_currentStep.get(), &AuthStep::hideVerificationUriAndCode, this, &AuthFlow::hideVerificationUriAndCode); + + m_currentStep->perform(); +} + + +QString AuthFlow::getStateMessage() const { + switch (m_taskState) + { + case AccountTaskState::STATE_WORKING: { + if(m_currentStep) { + return m_currentStep->describe(); + } + else { + return tr("Working..."); + } + } + default: { + return AccountTask::getStateMessage(); + } + } +} + +void AuthFlow::stepFinished(AccountTaskState resultingState, QString message) { + if(changeState(resultingState, message)) { + nextStep(); + } +} diff --git a/launcher/minecraft/auth/flows/AuthFlow.h b/launcher/minecraft/auth/flows/AuthFlow.h new file mode 100644 index 00000000..e067cc99 --- /dev/null +++ b/launcher/minecraft/auth/flows/AuthFlow.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +#include "minecraft/auth/Yggdrasil.h" +#include "minecraft/auth/AccountData.h" +#include "minecraft/auth/AccountTask.h" +#include "minecraft/auth/AuthStep.h" + +class AuthFlow : public AccountTask +{ + Q_OBJECT + +public: + explicit AuthFlow(AccountData * data, QObject *parent = 0); + + Katabasis::Validity validity() { + return m_data->validity_; + }; + + QString getStateMessage() const override; + + void executeTask() override; + +signals: + void activityChanged(Katabasis::Activity activity); + +private slots: + void stepFinished(AccountTaskState resultingState, QString message); + +protected: + void succeed(); + void nextStep(); + +protected: + QList m_steps; + AuthStep::Ptr m_currentStep; +}; diff --git a/launcher/minecraft/auth/flows/MSA.cpp b/launcher/minecraft/auth/flows/MSA.cpp new file mode 100644 index 00000000..416b8f2c --- /dev/null +++ b/launcher/minecraft/auth/flows/MSA.cpp @@ -0,0 +1,37 @@ +#include "MSA.h" + +#include "minecraft/auth/steps/MSAStep.h" +#include "minecraft/auth/steps/XboxUserStep.h" +#include "minecraft/auth/steps/XboxAuthorizationStep.h" +#include "minecraft/auth/steps/LauncherLoginStep.h" +#include "minecraft/auth/steps/XboxProfileStep.h" +#include "minecraft/auth/steps/EntitlementsStep.h" +#include "minecraft/auth/steps/MinecraftProfileStep.h" +#include "minecraft/auth/steps/GetSkinStep.h" + +MSASilent::MSASilent(AccountData* data, QObject* parent) : AuthFlow(data, parent) { + m_steps.append(new MSAStep(m_data, MSAStep::Action::Refresh)); + m_steps.append(new XboxUserStep(m_data)); + m_steps.append(new XboxAuthorizationStep(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox")); + m_steps.append(new XboxAuthorizationStep(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang")); + m_steps.append(new LauncherLoginStep(m_data)); + m_steps.append(new XboxProfileStep(m_data)); + m_steps.append(new EntitlementsStep(m_data)); + m_steps.append(new MinecraftProfileStep(m_data)); + m_steps.append(new GetSkinStep(m_data)); +} + +MSAInteractive::MSAInteractive( + AccountData* data, + QObject* parent +) : AuthFlow(data, parent) { + m_steps.append(new MSAStep(m_data, MSAStep::Action::Login)); + m_steps.append(new XboxUserStep(m_data)); + m_steps.append(new XboxAuthorizationStep(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox")); + m_steps.append(new XboxAuthorizationStep(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang")); + m_steps.append(new LauncherLoginStep(m_data)); + m_steps.append(new XboxProfileStep(m_data)); + m_steps.append(new EntitlementsStep(m_data)); + m_steps.append(new MinecraftProfileStep(m_data)); + m_steps.append(new GetSkinStep(m_data)); +} diff --git a/launcher/minecraft/auth/flows/MSA.h b/launcher/minecraft/auth/flows/MSA.h new file mode 100644 index 00000000..14a4ff43 --- /dev/null +++ b/launcher/minecraft/auth/flows/MSA.h @@ -0,0 +1,22 @@ +#pragma once +#include "AuthFlow.h" + +class MSAInteractive : public AuthFlow +{ + Q_OBJECT +public: + explicit MSAInteractive( + AccountData *data, + QObject *parent = 0 + ); +}; + +class MSASilent : public AuthFlow +{ + Q_OBJECT +public: + explicit MSASilent( + AccountData * data, + QObject *parent = 0 + ); +}; diff --git a/launcher/minecraft/auth/flows/MSAInteractive.cpp b/launcher/minecraft/auth/flows/MSAInteractive.cpp deleted file mode 100644 index 525aaf88..00000000 --- a/launcher/minecraft/auth/flows/MSAInteractive.cpp +++ /dev/null @@ -1,22 +0,0 @@ -#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_data = AccountData(); - m_oauth2->login(); -} diff --git a/launcher/minecraft/auth/flows/MSAInteractive.h b/launcher/minecraft/auth/flows/MSAInteractive.h deleted file mode 100644 index 6654e0d6..00000000 --- a/launcher/minecraft/auth/flows/MSAInteractive.h +++ /dev/null @@ -1,13 +0,0 @@ -#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 deleted file mode 100644 index 8ce43c1f..00000000 --- a/launcher/minecraft/auth/flows/MSASilent.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#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 deleted file mode 100644 index a442b49e..00000000 --- a/launcher/minecraft/auth/flows/MSASilent.h +++ /dev/null @@ -1,13 +0,0 @@ -#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/Mojang.cpp b/launcher/minecraft/auth/flows/Mojang.cpp new file mode 100644 index 00000000..4661dbe2 --- /dev/null +++ b/launcher/minecraft/auth/flows/Mojang.cpp @@ -0,0 +1,27 @@ +#include "Mojang.h" + +#include "minecraft/auth/steps/YggdrasilStep.h" +#include "minecraft/auth/steps/MinecraftProfileStep.h" +#include "minecraft/auth/steps/MigrationEligibilityStep.h" +#include "minecraft/auth/steps/GetSkinStep.h" + +MojangRefresh::MojangRefresh( + AccountData *data, + QObject *parent +) : AuthFlow(data, parent) { + m_steps.append(new YggdrasilStep(m_data, QString())); + m_steps.append(new MinecraftProfileStep(m_data)); + m_steps.append(new MigrationEligibilityStep(m_data)); + m_steps.append(new GetSkinStep(m_data)); +} + +MojangLogin::MojangLogin( + AccountData *data, + QString password, + QObject *parent +): AuthFlow(data, parent), m_password(password) { + m_steps.append(new YggdrasilStep(m_data, m_password)); + m_steps.append(new MinecraftProfileStep(m_data)); + m_steps.append(new MigrationEligibilityStep(m_data)); + m_steps.append(new GetSkinStep(m_data)); +} diff --git a/launcher/minecraft/auth/flows/Mojang.h b/launcher/minecraft/auth/flows/Mojang.h new file mode 100644 index 00000000..c09c81a8 --- /dev/null +++ b/launcher/minecraft/auth/flows/Mojang.h @@ -0,0 +1,26 @@ +#pragma once +#include "AuthFlow.h" + +class MojangRefresh : public AuthFlow +{ + Q_OBJECT +public: + explicit MojangRefresh( + AccountData *data, + QObject *parent = 0 + ); +}; + +class MojangLogin : public AuthFlow +{ + Q_OBJECT +public: + explicit MojangLogin( + AccountData *data, + QString password, + QObject *parent = 0 + ); + +private: + QString m_password; +}; diff --git a/launcher/minecraft/auth/flows/MojangLogin.cpp b/launcher/minecraft/auth/flows/MojangLogin.cpp deleted file mode 100644 index 6c217cd1..00000000 --- a/launcher/minecraft/auth/flows/MojangLogin.cpp +++ /dev/null @@ -1,18 +0,0 @@ -#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 deleted file mode 100644 index 5f33752f..00000000 --- a/launcher/minecraft/auth/flows/MojangLogin.h +++ /dev/null @@ -1,17 +0,0 @@ -#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 deleted file mode 100644 index 008c0453..00000000 --- a/launcher/minecraft/auth/flows/MojangRefresh.cpp +++ /dev/null @@ -1,17 +0,0 @@ -#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 deleted file mode 100644 index 06e4e4ce..00000000 --- a/launcher/minecraft/auth/flows/MojangRefresh.h +++ /dev/null @@ -1,10 +0,0 @@ -#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/steps/EntitlementsStep.cpp b/launcher/minecraft/auth/steps/EntitlementsStep.cpp new file mode 100644 index 00000000..f726244f --- /dev/null +++ b/launcher/minecraft/auth/steps/EntitlementsStep.cpp @@ -0,0 +1,53 @@ +#include "EntitlementsStep.h" + +#include +#include + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" + +EntitlementsStep::EntitlementsStep(AccountData* data) : AuthStep(data) {} + +EntitlementsStep::~EntitlementsStep() noexcept = default; + +QString EntitlementsStep::describe() { + return tr("Determining game ownership."); +} + + +void EntitlementsStep::perform() { + auto uuid = QUuid::createUuid(); + m_entitlementsRequestId = uuid.toString().remove('{').remove('}'); + auto url = "https://api.minecraftservices.com/entitlements/license?requestId=" + m_entitlementsRequestId; + QNetworkRequest request = QNetworkRequest(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Accept", "application/json"); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8()); + AuthRequest *requestor = new AuthRequest(this); + connect(requestor, &AuthRequest::finished, this, &EntitlementsStep::onRequestDone); + requestor->get(request); + qDebug() << "Getting entitlements..."; +} + +void EntitlementsStep::rehydrate() { + // NOOP, for now. We only save bools and there's nothing to check. +} + +void EntitlementsStep::onRequestDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers +) { + auto requestor = qobject_cast(QObject::sender()); + requestor->deleteLater(); + +#ifndef NDEBUG + qDebug() << data; +#endif + + // TODO: check presence of same entitlementsRequestId? + // TODO: validate JWTs? + Parsers::parseMinecraftEntitlements(data, m_data->minecraftEntitlement); + + emit finished(AccountTaskState::STATE_WORKING, tr("Got entitlements")); +} diff --git a/launcher/minecraft/auth/steps/EntitlementsStep.h b/launcher/minecraft/auth/steps/EntitlementsStep.h new file mode 100644 index 00000000..9412ae79 --- /dev/null +++ b/launcher/minecraft/auth/steps/EntitlementsStep.h @@ -0,0 +1,25 @@ +#pragma once +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class EntitlementsStep : public AuthStep { + Q_OBJECT + +public: + explicit EntitlementsStep(AccountData *data); + virtual ~EntitlementsStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); + +private: + QString m_entitlementsRequestId; +}; diff --git a/launcher/minecraft/auth/steps/GetSkinStep.cpp b/launcher/minecraft/auth/steps/GetSkinStep.cpp new file mode 100644 index 00000000..3521f8dc --- /dev/null +++ b/launcher/minecraft/auth/steps/GetSkinStep.cpp @@ -0,0 +1,43 @@ + +#include "GetSkinStep.h" + +#include + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" + +GetSkinStep::GetSkinStep(AccountData* data) : AuthStep(data) { + +} + +GetSkinStep::~GetSkinStep() noexcept = default; + +QString GetSkinStep::describe() { + return tr("Getting skin."); +} + +void GetSkinStep::perform() { + auto url = QUrl(m_data->minecraftProfile.skin.url); + QNetworkRequest request = QNetworkRequest(url); + AuthRequest *requestor = new AuthRequest(this); + connect(requestor, &AuthRequest::finished, this, &GetSkinStep::onRequestDone); + requestor->get(request); +} + +void GetSkinStep::rehydrate() { + // NOOP, for now. +} + +void GetSkinStep::onRequestDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers +) { + auto requestor = qobject_cast(QObject::sender()); + requestor->deleteLater(); + + if (error == QNetworkReply::NoError) { + m_data->minecraftProfile.skin.data = data; + } + emit finished(AccountTaskState::STATE_SUCCEEDED, tr("Got skin")); +} diff --git a/launcher/minecraft/auth/steps/GetSkinStep.h b/launcher/minecraft/auth/steps/GetSkinStep.h new file mode 100644 index 00000000..6b97371e --- /dev/null +++ b/launcher/minecraft/auth/steps/GetSkinStep.h @@ -0,0 +1,22 @@ +#pragma once +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class GetSkinStep : public AuthStep { + Q_OBJECT + +public: + explicit GetSkinStep(AccountData *data); + virtual ~GetSkinStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); +}; diff --git a/launcher/minecraft/auth/steps/LauncherLoginStep.cpp b/launcher/minecraft/auth/steps/LauncherLoginStep.cpp new file mode 100644 index 00000000..c978bd07 --- /dev/null +++ b/launcher/minecraft/auth/steps/LauncherLoginStep.cpp @@ -0,0 +1,78 @@ +#include "LauncherLoginStep.h" + +#include + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" +#include "minecraft/auth/AccountTask.h" + +LauncherLoginStep::LauncherLoginStep(AccountData* data) : AuthStep(data) { + +} + +LauncherLoginStep::~LauncherLoginStep() noexcept = default; + +QString LauncherLoginStep::describe() { + return tr("Accessing Mojang services."); +} + +void LauncherLoginStep::perform() { + auto requestURL = "https://api.minecraftservices.com/launcher/login"; + auto uhs = m_data->mojangservicesToken.extra["uhs"].toString(); + auto xToken = m_data->mojangservicesToken.token; + + QString mc_auth_template = R"XXX( +{ + "xtoken": "XBL3.0 x=%1;%2", + "platform": "PC_LAUNCHER" +} +)XXX"; + auto requestBody = mc_auth_template.arg(uhs, xToken); + + QNetworkRequest request = QNetworkRequest(QUrl(requestURL)); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Accept", "application/json"); + AuthRequest *requestor = new AuthRequest(this); + connect(requestor, &AuthRequest::finished, this, &LauncherLoginStep::onRequestDone); + requestor->post(request, requestBody.toUtf8()); + qDebug() << "Getting Minecraft access token..."; +} + +void LauncherLoginStep::rehydrate() { + // TODO: check the token validity +} + +void LauncherLoginStep::onRequestDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers +) { + auto requestor = qobject_cast(QObject::sender()); + requestor->deleteLater(); + + qDebug() << data; + if (error != QNetworkReply::NoError) { + qWarning() << "Reply error:" << error; +#ifndef NDEBUG + qDebug() << data; +#endif + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Failed to get Minecraft access token: %1").arg(requestor->errorString_) + ); + return; + } + + if(!Parsers::parseMojangResponse(data, m_data->yggdrasilToken)) { + qWarning() << "Could not parse login_with_xbox response..."; +#ifndef NDEBUG + qDebug() << data; +#endif + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Failed to parse the Minecraft access token response.") + ); + return; + } + emit finished(AccountTaskState::STATE_WORKING, tr("")); +} diff --git a/launcher/minecraft/auth/steps/LauncherLoginStep.h b/launcher/minecraft/auth/steps/LauncherLoginStep.h new file mode 100644 index 00000000..e06a306f --- /dev/null +++ b/launcher/minecraft/auth/steps/LauncherLoginStep.h @@ -0,0 +1,22 @@ +#pragma once +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class LauncherLoginStep : public AuthStep { + Q_OBJECT + +public: + explicit LauncherLoginStep(AccountData *data); + virtual ~LauncherLoginStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); +}; diff --git a/launcher/minecraft/auth/steps/MSAStep.cpp b/launcher/minecraft/auth/steps/MSAStep.cpp new file mode 100644 index 00000000..be711f7e --- /dev/null +++ b/launcher/minecraft/auth/steps/MSAStep.cpp @@ -0,0 +1,111 @@ +#include "MSAStep.h" + +#include + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" + +#include "Application.h" + +using OAuth2 = Katabasis::DeviceFlow; +using Activity = Katabasis::Activity; + +MSAStep::MSAStep(AccountData* data, Action action) : AuthStep(data), m_action(action) { + OAuth2::Options opts; + opts.scope = "XboxLive.signin offline_access"; + opts.clientIdentifier = APPLICATION->msaClientId(); + opts.authorizationUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode"; + opts.accessTokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"; + + // FIXME: OAuth2 is not aware of our fancy shared pointers + m_oauth2 = new OAuth2(opts, m_data->msaToken, this, APPLICATION->network().get()); + + connect(m_oauth2, &OAuth2::activityChanged, this, &MSAStep::onOAuthActivityChanged); + connect(m_oauth2, &OAuth2::showVerificationUriAndCode, this, &MSAStep::showVerificationUriAndCode); +} + +MSAStep::~MSAStep() noexcept = default; + +QString MSAStep::describe() { + return tr("Logging in with Microsoft account."); +} + + +void MSAStep::rehydrate() { + switch(m_action) { + case Refresh: { + // TODO: check the tokens and see if they are old (older than a day) + return; + } + case Login: { + // NOOP + return; + } + } +} + +void MSAStep::perform() { + switch(m_action) { + case Refresh: { + m_oauth2->refresh(); + return; + } + case Login: { + QVariantMap extraOpts; + extraOpts["prompt"] = "select_account"; + m_oauth2->setExtraRequestParams(extraOpts); + + *m_data = AccountData(); + m_oauth2->login(); + return; + } + } +} + +void MSAStep::onOAuthActivityChanged(Katabasis::Activity activity) { + switch(activity) { + case Katabasis::Activity::Idle: + case Katabasis::Activity::LoggingIn: + case Katabasis::Activity::Refreshing: + case Katabasis::Activity::LoggingOut: { + // We asked it to do something, it's doing it. Nothing to act upon. + return; + } + case Katabasis::Activity::Succeeded: { + // Succeeded or did not invalidate tokens + emit hideVerificationUriAndCode(); + QVariantMap extraTokens = m_oauth2->extraTokens(); +#ifndef NDEBUG + if (!extraTokens.isEmpty()) { + qDebug() << "Extra tokens in response:"; + foreach (QString key, extraTokens.keys()) { + qDebug() << "\t" << key << ":" << extraTokens.value(key); + } + } +#endif + emit finished(AccountTaskState::STATE_WORKING, tr("Got ")); + return; + } + case Katabasis::Activity::FailedSoft: { + // NOTE: soft error in the first step means 'offline' + emit hideVerificationUriAndCode(); + emit finished(AccountTaskState::STATE_OFFLINE, tr("Microsoft user authentication ended with a network error.")); + return; + } + case Katabasis::Activity::FailedGone: { + emit hideVerificationUriAndCode(); + emit finished(AccountTaskState::STATE_FAILED_GONE, tr("Microsoft user authentication failed - user no longer exists.")); + return; + } + case Katabasis::Activity::FailedHard: { + emit hideVerificationUriAndCode(); + emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Microsoft user authentication failed.")); + return; + } + default: { + emit hideVerificationUriAndCode(); + emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Microsoft user authentication completed with an unrecognized result.")); + return; + } + } +} diff --git a/launcher/minecraft/auth/steps/MSAStep.h b/launcher/minecraft/auth/steps/MSAStep.h new file mode 100644 index 00000000..49ba3542 --- /dev/null +++ b/launcher/minecraft/auth/steps/MSAStep.h @@ -0,0 +1,32 @@ + +#pragma once +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + +#include + +class MSAStep : public AuthStep { + Q_OBJECT +public: + enum Action { + Refresh, + Login + }; +public: + explicit MSAStep(AccountData *data, Action action); + virtual ~MSAStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onOAuthActivityChanged(Katabasis::Activity activity); + +private: + Katabasis::DeviceFlow *m_oauth2 = nullptr; + Action m_action; +}; diff --git a/launcher/minecraft/auth/steps/MigrationEligibilityStep.cpp b/launcher/minecraft/auth/steps/MigrationEligibilityStep.cpp new file mode 100644 index 00000000..f5b5637a --- /dev/null +++ b/launcher/minecraft/auth/steps/MigrationEligibilityStep.cpp @@ -0,0 +1,45 @@ +#include "MigrationEligibilityStep.h" + +#include + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" + +MigrationEligibilityStep::MigrationEligibilityStep(AccountData* data) : AuthStep(data) { + +} + +MigrationEligibilityStep::~MigrationEligibilityStep() noexcept = default; + +QString MigrationEligibilityStep::describe() { + return tr("Checking for migration eligibility."); +} + +void MigrationEligibilityStep::perform() { + 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()); + + AuthRequest *requestor = new AuthRequest(this); + connect(requestor, &AuthRequest::finished, this, &MigrationEligibilityStep::onRequestDone); + requestor->get(request); +} + +void MigrationEligibilityStep::rehydrate() { + // NOOP, for now. We only save bools and there's nothing to check. +} + +void MigrationEligibilityStep::onRequestDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers +) { + auto requestor = qobject_cast(QObject::sender()); + requestor->deleteLater(); + + if (error == QNetworkReply::NoError) { + Parsers::parseRolloutResponse(data, m_data->canMigrateToMSA); + } + emit finished(AccountTaskState::STATE_WORKING, tr("Got migration flags")); +} diff --git a/launcher/minecraft/auth/steps/MigrationEligibilityStep.h b/launcher/minecraft/auth/steps/MigrationEligibilityStep.h new file mode 100644 index 00000000..b1bf9cbf --- /dev/null +++ b/launcher/minecraft/auth/steps/MigrationEligibilityStep.h @@ -0,0 +1,22 @@ +#pragma once +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class MigrationEligibilityStep : public AuthStep { + Q_OBJECT + +public: + explicit MigrationEligibilityStep(AccountData *data); + virtual ~MigrationEligibilityStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); +}; diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp new file mode 100644 index 00000000..9fef99b0 --- /dev/null +++ b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp @@ -0,0 +1,83 @@ +#include "MinecraftProfileStep.h" + +#include + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" + +MinecraftProfileStep::MinecraftProfileStep(AccountData* data) : AuthStep(data) { + +} + +MinecraftProfileStep::~MinecraftProfileStep() noexcept = default; + +QString MinecraftProfileStep::describe() { + return tr("Fetching the Minecraft profile."); +} + + +void MinecraftProfileStep::perform() { + auto url = QUrl("https://api.minecraftservices.com/minecraft/profile"); + QNetworkRequest request = QNetworkRequest(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8()); + + AuthRequest *requestor = new AuthRequest(this); + connect(requestor, &AuthRequest::finished, this, &MinecraftProfileStep::onRequestDone); + requestor->get(request); +} + +void MinecraftProfileStep::rehydrate() { + // NOOP, for now. We only save bools and there's nothing to check. +} + +void MinecraftProfileStep::onRequestDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers +) { + auto requestor = qobject_cast(QObject::sender()); + requestor->deleteLater(); + +#ifndef NDEBUG + qDebug() << data; +#endif + if (error == QNetworkReply::ContentNotFoundError) { + // NOTE: Succeed even if we do not have a profile. This is a valid account state. + if(m_data->type == AccountType::Mojang) { + m_data->minecraftEntitlement.canPlayMinecraft = false; + m_data->minecraftEntitlement.ownsMinecraft = false; + } + m_data->minecraftProfile = MinecraftProfile(); + emit finished( + AccountTaskState::STATE_SUCCEEDED, + tr("Account has no Minecraft profile.") + ); + return; + } + if (error != QNetworkReply::NoError) { + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Minecraft Java profile acquisition failed.") + ); + return; + } + if(!Parsers::parseMinecraftProfile(data, m_data->minecraftProfile)) { + m_data->minecraftProfile = MinecraftProfile(); + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Minecraft Java profile response could not be parsed") + ); + return; + } + + if(m_data->type == AccountType::Mojang) { + auto validProfile = m_data->minecraftProfile.validity == Katabasis::Validity::Certain; + m_data->minecraftEntitlement.canPlayMinecraft = validProfile; + m_data->minecraftEntitlement.ownsMinecraft = validProfile; + } + emit finished( + AccountTaskState::STATE_WORKING, + tr("Minecraft Java profile acquisition succeeded.") + ); +} diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStep.h b/launcher/minecraft/auth/steps/MinecraftProfileStep.h new file mode 100644 index 00000000..8ef3395c --- /dev/null +++ b/launcher/minecraft/auth/steps/MinecraftProfileStep.h @@ -0,0 +1,22 @@ +#pragma once +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class MinecraftProfileStep : public AuthStep { + Q_OBJECT + +public: + explicit MinecraftProfileStep(AccountData *data); + virtual ~MinecraftProfileStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); +}; diff --git a/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp b/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp new file mode 100644 index 00000000..07eeb7dc --- /dev/null +++ b/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp @@ -0,0 +1,158 @@ +#include "XboxAuthorizationStep.h" + +#include +#include +#include + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" + +XboxAuthorizationStep::XboxAuthorizationStep(AccountData* data, Katabasis::Token *token, QString relyingParty, QString authorizationKind): + AuthStep(data), + m_token(token), + m_relyingParty(relyingParty), + m_authorizationKind(authorizationKind) +{ +} + +XboxAuthorizationStep::~XboxAuthorizationStep() noexcept = default; + +QString XboxAuthorizationStep::describe() { + return tr("Getting authorization to access %1 services.").arg(m_authorizationKind); +} + +void XboxAuthorizationStep::rehydrate() { + // FIXME: check if the tokens are good? +} + +void XboxAuthorizationStep::perform() { + QString xbox_auth_template = R"XXX( +{ + "Properties": { + "SandboxId": "RETAIL", + "UserTokens": [ + "%1" + ] + }, + "RelyingParty": "%2", + "TokenType": "JWT" +} +)XXX"; + auto xbox_auth_data = xbox_auth_template.arg(m_data->userToken.token, m_relyingParty); +// http://xboxlive.com + QNetworkRequest request = QNetworkRequest(QUrl("https://xsts.auth.xboxlive.com/xsts/authorize")); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Accept", "application/json"); + AuthRequest *requestor = new AuthRequest(this); + connect(requestor, &AuthRequest::finished, this, &XboxAuthorizationStep::onRequestDone); + requestor->post(request, xbox_auth_data.toUtf8()); + qDebug() << "Getting authorization token for " << m_relyingParty; +} + +void XboxAuthorizationStep::onRequestDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers +) { + auto requestor = qobject_cast(QObject::sender()); + requestor->deleteLater(); + +#ifndef NDEBUG + qDebug() << data; +#endif + if (error != QNetworkReply::NoError) { + qWarning() << "Reply error:" << error; + if(!processSTSError(error, data, headers)) { + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Failed to get authorization for %1 services. Error %1.").arg(m_authorizationKind, error) + ); + } + return; + } + + Katabasis::Token temp; + if(!Parsers::parseXTokenResponse(data, temp, m_authorizationKind)) { + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Could not parse authorization response for access to %1 services.").arg(m_authorizationKind) + ); + return; + } + + if(temp.extra["uhs"] != m_data->userToken.extra["uhs"]) { + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Server has changed %1 authorization user hash in the reply. Something is wrong.").arg(m_authorizationKind) + ); + return; + } + auto & token = *m_token; + token = temp; + + emit finished(AccountTaskState::STATE_WORKING, tr("Got authorization to access %1").arg(m_relyingParty)); +} + + +bool XboxAuthorizationStep::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(); + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Cannot parse %1 authorization error response as JSON: %2").arg(m_authorizationKind, jsonError.errorString()) + ); + return true; + } + + int64_t errorCode = -1; + auto obj = doc.object(); + if(!Parsers::getNumber(obj.value("XErr"), errorCode)) { + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("XErr element is missing from %1 authorization error response.").arg(m_authorizationKind) + ); + return true; + } + switch(errorCode) { + case 2148916233:{ + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("This Microsoft account does not have an XBox Live profile. Buy the game on %1 first.") + .arg("minecraft.net") + ); + return true; + } + case 2148916235: { + // NOTE: this is the Grulovia error + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("XBox Live is not available in your country. You've been blocked.") + ); + return true; + } + case 2148916238: { + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + 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") + ); + return true; + } + default: { + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("XSTS authentication ended with unrecognized error(s):\n\n%1").arg(errorCode) + ); + return true; + } + } + } + return false; +} diff --git a/launcher/minecraft/auth/steps/XboxAuthorizationStep.h b/launcher/minecraft/auth/steps/XboxAuthorizationStep.h new file mode 100644 index 00000000..31e43bf0 --- /dev/null +++ b/launcher/minecraft/auth/steps/XboxAuthorizationStep.h @@ -0,0 +1,34 @@ +#pragma once +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class XboxAuthorizationStep : public AuthStep { + Q_OBJECT + +public: + explicit XboxAuthorizationStep(AccountData *data, Katabasis::Token *token, QString relyingParty, QString authorizationKind); + virtual ~XboxAuthorizationStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private: + bool processSTSError( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers + ); + +private slots: + void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); + +private: + Katabasis::Token *m_token; + QString m_relyingParty; + QString m_authorizationKind; +}; diff --git a/launcher/minecraft/auth/steps/XboxProfileStep.cpp b/launcher/minecraft/auth/steps/XboxProfileStep.cpp new file mode 100644 index 00000000..9f50138e --- /dev/null +++ b/launcher/minecraft/auth/steps/XboxProfileStep.cpp @@ -0,0 +1,73 @@ +#include "XboxProfileStep.h" + +#include +#include + + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" + +XboxProfileStep::XboxProfileStep(AccountData* data) : AuthStep(data) { + +} + +XboxProfileStep::~XboxProfileStep() noexcept = default; + +QString XboxProfileStep::describe() { + return tr("Fetching Xbox profile."); +} + +void XboxProfileStep::rehydrate() { + // NOOP, for now. We only save bools and there's nothing to check. +} + +void XboxProfileStep::perform() { + auto url = QUrl("https://profile.xboxlive.com/users/me/profile/settings"); + QUrlQuery q; + q.addQueryItem( + "settings", + "GameDisplayName,AppDisplayName,AppDisplayPicRaw,GameDisplayPicRaw," + "PublicGamerpic,ShowUserAsAvatar,Gamerscore,Gamertag,ModernGamertag,ModernGamertagSuffix," + "UniqueModernGamertag,AccountTier,TenureLevel,XboxOneRep," + "PreferredColor,Location,Bio,Watermarks," + "RealName,RealNameOverride,IsQuarantined" + ); + url.setQuery(q); + + QNetworkRequest request = QNetworkRequest(url); + 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_data->userToken.extra["uhs"].toString(), m_data->xboxApiToken.token).toUtf8()); + AuthRequest *requestor = new AuthRequest(this); + connect(requestor, &AuthRequest::finished, this, &XboxProfileStep::onRequestDone); + requestor->get(request); + qDebug() << "Getting Xbox profile..."; +} + +void XboxProfileStep::onRequestDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers +) { + auto requestor = qobject_cast(QObject::sender()); + requestor->deleteLater(); + + if (error != QNetworkReply::NoError) { + qWarning() << "Reply error:" << error; +#ifndef NDEBUG + qDebug() << data; +#endif + finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Failed to retrieve the Xbox profile.") + ); + return; + } + +#ifndef NDEBUG + qDebug() << "XBox profile: " << data; +#endif + + emit finished(AccountTaskState::STATE_WORKING, tr("Got Xbox profile")); +} diff --git a/launcher/minecraft/auth/steps/XboxProfileStep.h b/launcher/minecraft/auth/steps/XboxProfileStep.h new file mode 100644 index 00000000..7a0c5873 --- /dev/null +++ b/launcher/minecraft/auth/steps/XboxProfileStep.h @@ -0,0 +1,22 @@ +#pragma once +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class XboxProfileStep : public AuthStep { + Q_OBJECT + +public: + explicit XboxProfileStep(AccountData *data); + virtual ~XboxProfileStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); +}; diff --git a/launcher/minecraft/auth/steps/XboxUserStep.cpp b/launcher/minecraft/auth/steps/XboxUserStep.cpp new file mode 100644 index 00000000..a38a28e4 --- /dev/null +++ b/launcher/minecraft/auth/steps/XboxUserStep.cpp @@ -0,0 +1,68 @@ +#include "XboxUserStep.h" + +#include + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" + +XboxUserStep::XboxUserStep(AccountData* data) : AuthStep(data) { + +} + +XboxUserStep::~XboxUserStep() noexcept = default; + +QString XboxUserStep::describe() { + return tr("Logging in as an Xbox user."); +} + + +void XboxUserStep::rehydrate() { + // NOOP, for now. We only save bools and there's nothing to check. +} + +void XboxUserStep::perform() { + QString xbox_auth_template = R"XXX( +{ + "Properties": { + "AuthMethod": "RPS", + "SiteName": "user.auth.xboxlive.com", + "RpsTicket": "d=%1" + }, + "RelyingParty": "http://auth.xboxlive.com", + "TokenType": "JWT" +} +)XXX"; + 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 AuthRequest(this); + connect(requestor, &AuthRequest::finished, this, &XboxUserStep::onRequestDone); + requestor->post(request, xbox_auth_data.toUtf8()); + qDebug() << "First layer of XBox auth ... commencing."; +} + +void XboxUserStep::onRequestDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers +) { + auto requestor = qobject_cast(QObject::sender()); + requestor->deleteLater(); + + if (error != QNetworkReply::NoError) { + qWarning() << "Reply error:" << error; + emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("XBox user authentication failed.")); + return; + } + + Katabasis::Token temp; + if(!Parsers::parseXTokenResponse(data, temp, "UToken")) { + qWarning() << "Could not parse user authentication response..."; + emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("XBox user authentication response could not be understood.")); + return; + } + m_data->userToken = temp; + emit finished(AccountTaskState::STATE_WORKING, tr("Got Xbox user token")); +} diff --git a/launcher/minecraft/auth/steps/XboxUserStep.h b/launcher/minecraft/auth/steps/XboxUserStep.h new file mode 100644 index 00000000..83e9405f --- /dev/null +++ b/launcher/minecraft/auth/steps/XboxUserStep.h @@ -0,0 +1,22 @@ +#pragma once +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class XboxUserStep : public AuthStep { + Q_OBJECT + +public: + explicit XboxUserStep(AccountData *data); + virtual ~XboxUserStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); +}; diff --git a/launcher/minecraft/auth/steps/YggdrasilStep.cpp b/launcher/minecraft/auth/steps/YggdrasilStep.cpp new file mode 100644 index 00000000..ac6ad798 --- /dev/null +++ b/launcher/minecraft/auth/steps/YggdrasilStep.cpp @@ -0,0 +1,51 @@ +#include "YggdrasilStep.h" + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" +#include "minecraft/auth/Yggdrasil.h" + +YggdrasilStep::YggdrasilStep(AccountData* data, QString password) : AuthStep(data), m_password(password) { + m_yggdrasil = new Yggdrasil(m_data, this); + + connect(m_yggdrasil, &Task::failed, this, &YggdrasilStep::onAuthFailed); + connect(m_yggdrasil, &Task::succeeded, this, &YggdrasilStep::onAuthSucceeded); +} + +YggdrasilStep::~YggdrasilStep() noexcept = default; + +QString YggdrasilStep::describe() { + return tr("Logging in with Mojang account."); +} + +void YggdrasilStep::rehydrate() { + // NOOP, for now. +} + +void YggdrasilStep::perform() { + if(m_password.size()) { + m_yggdrasil->login(m_password); + } + else { + m_yggdrasil->refresh(); + } +} + +void YggdrasilStep::onAuthSucceeded() { + emit finished(AccountTaskState::STATE_WORKING, tr("Logged in with Mojang")); +} + +void YggdrasilStep::onAuthFailed() { + // TODO: hook these in again, expand to MSA + // m_error = m_yggdrasil->m_error; + // m_aborted = m_yggdrasil->m_aborted; + + auto state = m_yggdrasil->taskState(); + QString errorMessage = tr("Mojang user authentication failed."); + + // NOTE: soft error in the first step means 'offline' + if(state == AccountTaskState::STATE_FAILED_SOFT) { + state = AccountTaskState::STATE_OFFLINE; + errorMessage = tr("Mojang user authentication ended with a network error."); + } + emit finished(AccountTaskState::STATE_OFFLINE, errorMessage); +} diff --git a/launcher/minecraft/auth/steps/YggdrasilStep.h b/launcher/minecraft/auth/steps/YggdrasilStep.h new file mode 100644 index 00000000..ebafb8e5 --- /dev/null +++ b/launcher/minecraft/auth/steps/YggdrasilStep.h @@ -0,0 +1,28 @@ +#pragma once +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + +class Yggdrasil; + +class YggdrasilStep : public AuthStep { + Q_OBJECT + +public: + explicit YggdrasilStep(AccountData *data, QString password); + virtual ~YggdrasilStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onAuthSucceeded(); + void onAuthFailed(); + +private: + Yggdrasil *m_yggdrasil = nullptr; + QString m_password; +}; diff --git a/launcher/minecraft/services/CapeChange.cpp b/launcher/minecraft/services/CapeChange.cpp index d411965a..e49c166a 100644 --- a/launcher/minecraft/services/CapeChange.cpp +++ b/launcher/minecraft/services/CapeChange.cpp @@ -5,15 +5,15 @@ #include "Application.h" -CapeChange::CapeChange(QObject *parent, AuthSessionPtr session, QString cape) - : Task(parent), m_capeId(cape), m_session(session) +CapeChange::CapeChange(QObject *parent, QString token, QString cape) + : Task(parent), m_capeId(cape), m_token(token) { } 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()); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit()); QNetworkReply *rep = APPLICATION->network()->put(request, requestString.toUtf8()); setStatus(tr("Equipping cape")); @@ -27,7 +27,7 @@ void CapeChange::setCape(QString& cape) { 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()); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit()); QNetworkReply *rep = APPLICATION->network()->deleteResource(request); setStatus(tr("Removing cape")); diff --git a/launcher/minecraft/services/CapeChange.h b/launcher/minecraft/services/CapeChange.h index c04ad8c7..185d69b6 100644 --- a/launcher/minecraft/services/CapeChange.h +++ b/launcher/minecraft/services/CapeChange.h @@ -3,7 +3,6 @@ #include #include #include -#include #include "tasks/Task.h" #include "QObjectPtr.h" @@ -11,7 +10,7 @@ class CapeChange : public Task { Q_OBJECT public: - CapeChange(QObject *parent, AuthSessionPtr session, QString capeId); + CapeChange(QObject *parent, QString token, QString capeId); virtual ~CapeChange() {} private: @@ -20,7 +19,7 @@ private: private: QString m_capeId; - AuthSessionPtr m_session; + QString m_token; shared_qobject_ptr m_reply; protected: diff --git a/launcher/minecraft/services/SkinDelete.cpp b/launcher/minecraft/services/SkinDelete.cpp index a0b0330c..cce8364e 100644 --- a/launcher/minecraft/services/SkinDelete.cpp +++ b/launcher/minecraft/services/SkinDelete.cpp @@ -5,15 +5,15 @@ #include "Application.h" -SkinDelete::SkinDelete(QObject *parent, AuthSessionPtr session) - : Task(parent), m_session(session) +SkinDelete::SkinDelete(QObject *parent, QString token) + : Task(parent), m_token(token) { } void SkinDelete::executeTask() { QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/skins/active")); - request.setRawHeader("Authorization", QString("Bearer %1").arg(m_session->access_token).toLocal8Bit()); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit()); QNetworkReply *rep = APPLICATION->network()->deleteResource(request); m_reply = shared_qobject_ptr(rep); diff --git a/launcher/minecraft/services/SkinDelete.h b/launcher/minecraft/services/SkinDelete.h index 6048b33a..83a84685 100644 --- a/launcher/minecraft/services/SkinDelete.h +++ b/launcher/minecraft/services/SkinDelete.h @@ -2,7 +2,6 @@ #include #include -#include #include "tasks/Task.h" typedef shared_qobject_ptr SkinDeletePtr; @@ -11,11 +10,11 @@ class SkinDelete : public Task { Q_OBJECT public: - SkinDelete(QObject *parent, AuthSessionPtr session); + SkinDelete(QObject *parent, QString token); virtual ~SkinDelete() = default; private: - AuthSessionPtr m_session; + QString m_token; shared_qobject_ptr m_reply; protected: @@ -25,4 +24,3 @@ public slots: void downloadError(QNetworkReply::NetworkError); void downloadFinished(); }; - diff --git a/launcher/minecraft/services/SkinUpload.cpp b/launcher/minecraft/services/SkinUpload.cpp index e58d32d7..7c2e8337 100644 --- a/launcher/minecraft/services/SkinUpload.cpp +++ b/launcher/minecraft/services/SkinUpload.cpp @@ -16,15 +16,15 @@ QByteArray getVariant(SkinUpload::Model model) { } } -SkinUpload::SkinUpload(QObject *parent, AuthSessionPtr session, QByteArray skin, SkinUpload::Model model) - : Task(parent), m_model(model), m_skin(skin), m_session(session) +SkinUpload::SkinUpload(QObject *parent, QString token, QByteArray skin, SkinUpload::Model model) + : Task(parent), m_model(model), m_skin(skin), m_token(token) { } void SkinUpload::executeTask() { QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/skins")); - request.setRawHeader("Authorization", QString("Bearer %1").arg(m_session->access_token).toLocal8Bit()); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit()); QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); QHttpPart skin; diff --git a/launcher/minecraft/services/SkinUpload.h b/launcher/minecraft/services/SkinUpload.h index 2c782e11..2c1f0a2e 100644 --- a/launcher/minecraft/services/SkinUpload.h +++ b/launcher/minecraft/services/SkinUpload.h @@ -3,7 +3,6 @@ #include #include #include -#include #include "tasks/Task.h" typedef shared_qobject_ptr SkinUploadPtr; @@ -19,13 +18,13 @@ public: }; // Note this class takes ownership of the file. - SkinUpload(QObject *parent, AuthSessionPtr session, QByteArray skin, Model model = STEVE); + SkinUpload(QObject *parent, QString token, QByteArray skin, Model model = STEVE); virtual ~SkinUpload() {} private: Model m_model; QByteArray m_skin; - AuthSessionPtr m_session; + QString m_token; shared_qobject_ptr m_reply; protected: virtual void executeTask(); diff --git a/launcher/ui/dialogs/LoginDialog.cpp b/launcher/ui/dialogs/LoginDialog.cpp index bf0806e1..194315a7 100644 --- a/launcher/ui/dialogs/LoginDialog.cpp +++ b/launcher/ui/dialogs/LoginDialog.cpp @@ -43,7 +43,7 @@ void LoginDialog::accept() // Setup the login task and start it m_account = MinecraftAccount::createFromUsername(ui->userTextBox->text()); - m_loginTask = m_account->login(nullptr, ui->passTextBox->text()); + m_loginTask = m_account->login(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::status, this, &LoginDialog::onTaskStatus); diff --git a/launcher/ui/dialogs/MSALoginDialog.cpp b/launcher/ui/dialogs/MSALoginDialog.cpp index 15c04061..f46aa3b9 100644 --- a/launcher/ui/dialogs/MSALoginDialog.cpp +++ b/launcher/ui/dialogs/MSALoginDialog.cpp @@ -37,7 +37,7 @@ int MSALoginDialog::exec() { // Setup the login task and start it m_account = MinecraftAccount::createBlankMSA(); - m_loginTask = m_account->loginMSA(nullptr); + m_loginTask = m_account->loginMSA(); 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); diff --git a/launcher/ui/dialogs/ProfileSetupDialog.cpp b/launcher/ui/dialogs/ProfileSetupDialog.cpp index d3e2b9a4..76b6af49 100644 --- a/launcher/ui/dialogs/ProfileSetupDialog.cpp +++ b/launcher/ui/dialogs/ProfileSetupDialog.cpp @@ -25,8 +25,8 @@ #include "ui/dialogs/ProgressDialog.h" #include -#include "minecraft/auth/flows/AuthRequest.h" -#include "minecraft/auth/flows/Parsers.h" +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" ProfileSetupDialog::ProfileSetupDialog(MinecraftAccountPtr accountToSetup, QWidget *parent) @@ -150,6 +150,9 @@ void ProfileSetupDialog::checkFinished( QByteArray data, QList headers ) { + auto requestor = qobject_cast(QObject::sender()); + requestor->deleteLater(); + if(error == QNetworkReply::NoError) { auto doc = QJsonDocument::fromJson(data); auto root = doc.object(); @@ -231,6 +234,9 @@ void ProfileSetupDialog::setupProfileFinished( QByteArray data, QList headers ) { + auto requestor = qobject_cast(QObject::sender()); + requestor->deleteLater(); + isWorking = false; if(error == QNetworkReply::NoError) { /* diff --git a/launcher/ui/dialogs/SkinUploadDialog.cpp b/launcher/ui/dialogs/SkinUploadDialog.cpp index 4e6142fa..6a5a324f 100644 --- a/launcher/ui/dialogs/SkinUploadDialog.cpp +++ b/launcher/ui/dialogs/SkinUploadDialog.cpp @@ -20,16 +20,6 @@ void SkinUploadDialog::on_buttonBox_rejected() void SkinUploadDialog::on_buttonBox_accepted() { - AuthSessionPtr session = std::make_shared(); - auto login = m_acct->refresh(session); - ProgressDialog prog(this); - if (prog.execWithTask((Task*)login.get()) != QDialog::Accepted) - { - //FIXME: recover with password prompt - CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Failed to login!"), QMessageBox::Warning)->exec(); - close(); - return; - } QString fileName; QString input = ui->skinPathTextBox->text(); QRegExp urlPrefixMatcher("^([a-z]+)://.+$"); @@ -91,11 +81,12 @@ void SkinUploadDialog::on_buttonBox_accepted() { model = SkinUpload::ALEX; } + ProgressDialog prog(this); SequentialTask skinUpload; - skinUpload.addTask(shared_qobject_ptr(new SkinUpload(this, session, FS::read(fileName), model))); + skinUpload.addTask(shared_qobject_ptr(new SkinUpload(this, m_acct->accessToken(), FS::read(fileName), model))); auto selectedCape = ui->capeCombo->currentData().toString(); if(selectedCape != m_acct->accountData()->minecraftProfile.currentCape) { - skinUpload.addTask(shared_qobject_ptr(new CapeChange(this, session, selectedCape))); + skinUpload.addTask(shared_qobject_ptr(new CapeChange(this, m_acct->accessToken(), selectedCape))); } if (prog.execWithTask(&skinUpload) != QDialog::Accepted) { diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp index 816dce47..d3eb2655 100644 --- a/launcher/ui/pages/global/AccountListPage.cpp +++ b/launcher/ui/pages/global/AccountListPage.cpp @@ -170,13 +170,7 @@ void AccountListPage::on_actionRefresh_triggered() { 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 - } + m_accounts->requestRefresh(account->internalId()); } } @@ -244,15 +238,9 @@ void AccountListPage::on_actionDeleteSkin_triggered() return; QModelIndex selected = selection.first(); - AuthSessionPtr session = std::make_shared(); 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(); - return; - } - auto deleteSkinTask = std::make_shared(this, session); + auto deleteSkinTask = std::make_shared(this, account->accessToken()); if (prog.execWithTask((Task*)deleteSkinTask.get()) != QDialog::Accepted) { CustomMessageBox::selectable(this, tr("Skin Delete"), tr("Failed to delete current skin!"), QMessageBox::Warning)->exec(); return; diff --git a/launcher/ui/widgets/ErrorFrame.cpp b/launcher/ui/widgets/ErrorFrame.cpp new file mode 100644 index 00000000..b3e41036 --- /dev/null +++ b/launcher/ui/widgets/ErrorFrame.cpp @@ -0,0 +1,134 @@ +/* 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 +#include + +#include "ErrorFrame.h" +#include "ui_ErrorFrame.h" + +#include "ui/dialogs/CustomMessageBox.h" + +void ErrorFrame::clear() +{ + setTitle(QString()); + setDescription(QString()); +} + +ErrorFrame::ErrorFrame(QWidget *parent) : + QFrame(parent), + ui(new Ui::ErrorFrame) +{ + ui->setupUi(this); + ui->label_Description->setHidden(true); + ui->label_Title->setHidden(true); + updateHiddenState(); +} + +ErrorFrame::~ErrorFrame() +{ + delete ui; +} + +void ErrorFrame::updateHiddenState() +{ + if(ui->label_Description->isHidden() && ui->label_Title->isHidden()) + { + setHidden(true); + } + else + { + setHidden(false); + } +} + +void ErrorFrame::setTitle(QString text) +{ + if(text.isEmpty()) + { + ui->label_Title->setHidden(true); + } + else + { + ui->label_Title->setText(text); + ui->label_Title->setHidden(false); + } + updateHiddenState(); +} + +void ErrorFrame::setDescription(QString text) +{ + if(text.isEmpty()) + { + ui->label_Description->setHidden(true); + updateHiddenState(); + return; + } + else + { + ui->label_Description->setHidden(false); + updateHiddenState(); + } + ui->label_Description->setToolTip(""); + QString intermediatetext = text.trimmed(); + bool prev(false); + QChar rem('\n'); + QString finaltext; + finaltext.reserve(intermediatetext.size()); + foreach(const QChar& c, intermediatetext) + { + if(c == rem && prev){ + continue; + } + prev = c == rem; + finaltext += c; + } + QString labeltext; + labeltext.reserve(300); + if(finaltext.length() > 290) + { + ui->label_Description->setOpenExternalLinks(false); + ui->label_Description->setTextFormat(Qt::TextFormat::RichText); + desc = text; + // This allows injecting HTML here. + labeltext.append("" + finaltext.left(287) + "..."); + QObject::connect(ui->label_Description, &QLabel::linkActivated, this, &ErrorFrame::ellipsisHandler); + } + else + { + ui->label_Description->setTextFormat(Qt::TextFormat::PlainText); + labeltext.append(finaltext); + } + ui->label_Description->setText(labeltext); +} + +void ErrorFrame::ellipsisHandler(const QString &link) +{ + if(!currentBox) + { + currentBox = CustomMessageBox::selectable(this, QString(), desc); + connect(currentBox, &QMessageBox::finished, this, &ErrorFrame::boxClosed); + currentBox->show(); + } + else + { + currentBox->setText(desc); + } +} + +void ErrorFrame::boxClosed(int result) +{ + currentBox = nullptr; +} diff --git a/launcher/ui/widgets/ErrorFrame.h b/launcher/ui/widgets/ErrorFrame.h new file mode 100644 index 00000000..d5069a14 --- /dev/null +++ b/launcher/ui/widgets/ErrorFrame.h @@ -0,0 +1,49 @@ +/* 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 + +namespace Ui +{ +class ErrorFrame; +} + +class ErrorFrame : public QFrame +{ + Q_OBJECT + +public: + explicit ErrorFrame(QWidget *parent = 0); + ~ErrorFrame(); + + void setTitle(QString text); + void setDescription(QString text); + + void clear(); + +public slots: + void ellipsisHandler(const QString& link ); + void boxClosed(int result); + +private: + void updateHiddenState(); + +private: + Ui::ErrorFrame *ui; + QString desc; + class QMessageBox * currentBox = nullptr; +}; diff --git a/launcher/ui/widgets/ErrorFrame.ui b/launcher/ui/widgets/ErrorFrame.ui new file mode 100644 index 00000000..0bb56743 --- /dev/null +++ b/launcher/ui/widgets/ErrorFrame.ui @@ -0,0 +1,92 @@ + + + ErrorFrame + + + + 0 + 0 + 527 + 113 + + + + + 0 + 0 + + + + + 16777215 + 120 + + + + + 6 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + Qt::RichText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + true + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + + + + + + Qt::RichText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + true + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + +