diff options
Diffstat (limited to 'launcher/minecraft/auth/flows')
22 files changed, 228 insertions, 1844 deletions
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 <QNetworkAccessManager> -#include <QNetworkRequest> -#include <QNetworkReply> -#include <QDesktopServices> -#include <QMetaEnum> -#include <QDebug> -#include <QJsonDocument> -#include <QJsonObject> -#include <QJsonArray> -#include <QUuid> -#include <QUrlQuery> - -#include "AuthContext.h" -#include "katabasis/Globals.h" -#include "AuthRequest.h" - -#include "Parsers.h" - -#include <Application.h> - -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<QNetworkReply::RawHeaderPair> 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<QNetworkReply::RawHeaderPair> 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<QNetworkReply::RawHeaderPair> 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<QNetworkReply::RawHeaderPair> 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<QNetworkReply::RawHeaderPair> 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<QNetworkReply::RawHeaderPair> 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("<a href=\"https://www.minecraft.net/en-us/store/minecraft-java-edition\">minecraft.net</a>") - ); - } - 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("<a href=\"https://help.minecraft.net/hc/en-us/articles/4403181904525\">help.minecraft.net</a>") - ); - } - 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<QNetworkReply::RawHeaderPair> 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<QNetworkReply::RawHeaderPair> 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<QNetworkReply::RawHeaderPair> 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<QNetworkReply::RawHeaderPair> -) { - 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 <QObject> -#include <QList> -#include <QVector> -#include <QSet> -#include <QNetworkReply> -#include <QImage> - -#include <katabasis/DeviceFlow.h> -#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<QNetworkReply::RawHeaderPair>); - - void processSTSError(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); - - void doSTSAuthMinecraft(); - Q_SLOT void onSTSAuthMinecraftDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); - void doMinecraftAuth(); - Q_SLOT void onMinecraftAuthDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); - - void doSTSAuthGeneric(); - Q_SLOT void onSTSAuthGenericDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); - void doXBoxProfile(); - Q_SLOT void onXBoxProfileDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); - - void doEntitlements(); - Q_SLOT void onEntitlementsDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); - - void doMinecraftProfile(); - Q_SLOT void onMinecraftProfileDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); - - void doMigrationEligibilityCheck(); - Q_SLOT void onMigrationEligibilityCheckDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); - - void doGetSkin(); - Q_SLOT void onSkinDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); - - 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<int64_t> 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 <QNetworkAccessManager> +#include <QNetworkRequest> +#include <QNetworkReply> +#include <QDebug> + +#include "AuthFlow.h" +#include "katabasis/Globals.h" + +#include <Application.h> + +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 <QObject> +#include <QList> +#include <QVector> +#include <QSet> +#include <QNetworkReply> +#include <QImage> + +#include <katabasis/DeviceFlow.h> + +#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<AuthStep::Ptr> m_steps; + AuthStep::Ptr m_currentStep; +}; diff --git a/launcher/minecraft/auth/flows/AuthRequest.cpp b/launcher/minecraft/auth/flows/AuthRequest.cpp deleted file mode 100644 index 82dba591..00000000 --- a/launcher/minecraft/auth/flows/AuthRequest.cpp +++ /dev/null @@ -1,121 +0,0 @@ -#include <cassert> - -#include <QDebug> -#include <QTimer> -#include <QBuffer> -#include <QUrlQuery> - -#include "Application.h" -#include "AuthRequest.h" -#include "katabasis/Globals.h" - -AuthRequest::AuthRequest(QObject *parent): QObject(parent) { -} - -AuthRequest::~AuthRequest() { -} - -void AuthRequest::get(const QNetworkRequest &req, int timeout/* = 60*1000*/) { - setup(req, QNetworkAccessManager::GetOperation); - reply_ = APPLICATION->network()->get(request_); - status_ = Requesting; - timedReplies_.add(new Katabasis::Reply(reply_, timeout)); - connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError))); - connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished())); - connect(reply_, &QNetworkReply::sslErrors, this, &AuthRequest::onSslErrors); -} - -void AuthRequest::post(const QNetworkRequest &req, const QByteArray &data, int timeout/* = 60*1000*/) { - setup(req, QNetworkAccessManager::PostOperation); - data_ = data; - status_ = Requesting; - reply_ = APPLICATION->network()->post(request_, data_); - timedReplies_.add(new Katabasis::Reply(reply_, timeout)); - connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError))); - connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished())); - connect(reply_, &QNetworkReply::sslErrors, this, &AuthRequest::onSslErrors); - connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64))); -} - -void AuthRequest::onRequestFinished() { - if (status_ == Idle) { - return; - } - if (reply_ != qobject_cast<QNetworkReply *>(sender())) { - return; - } - finish(); -} - -void AuthRequest::onRequestError(QNetworkReply::NetworkError error) { - qWarning() << "AuthRequest::onRequestError: Error" << (int)error; - if (status_ == Idle) { - return; - } - if (reply_ != qobject_cast<QNetworkReply *>(sender())) { - return; - } - qWarning() << "AuthRequest::onRequestError: Error string: " << reply_->errorString(); - int httpStatus = reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - qWarning() << "AuthRequest::onRequestError: HTTP status" << httpStatus << reply_->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); - error_ = error; - - // QTimer::singleShot(10, this, SLOT(finish())); -} - -void AuthRequest::onSslErrors(QList<QSslError> errors) { - int i = 1; - for (auto error : errors) { - qCritical() << "LOGIN SSL Error #" << i << " : " << error.errorString(); - auto cert = error.certificate(); - qCritical() << "Certificate in question:\n" << cert.toText(); - i++; - } -} - -void AuthRequest::onUploadProgress(qint64 uploaded, qint64 total) { - if (status_ == Idle) { - qWarning() << "AuthRequest::onUploadProgress: No pending request"; - return; - } - if (reply_ != qobject_cast<QNetworkReply *>(sender())) { - return; - } - // Restart timeout because request in progress - Katabasis::Reply *o2Reply = timedReplies_.find(reply_); - if(o2Reply) { - o2Reply->start(); - } - emit uploadProgress(uploaded, total); -} - -void AuthRequest::setup(const QNetworkRequest &req, QNetworkAccessManager::Operation operation, const QByteArray &verb) { - request_ = req; - operation_ = operation; - url_ = req.url(); - - QUrl url = url_; - request_.setUrl(url); - - if (!verb.isEmpty()) { - request_.setRawHeader(Katabasis::HTTP_HTTP_HEADER, verb); - } - - status_ = Requesting; - error_ = QNetworkReply::NoError; -} - -void AuthRequest::finish() { - QByteArray data; - if (status_ == Idle) { - qWarning() << "AuthRequest::finish: No pending request"; - return; - } - data = reply_->readAll(); - status_ = Idle; - timedReplies_.remove(reply_); - reply_->disconnect(this); - reply_->deleteLater(); - QList<QNetworkReply::RawHeaderPair> headers = reply_->rawHeaderPairs(); - emit finished(error_, data, headers); -} diff --git a/launcher/minecraft/auth/flows/AuthRequest.h b/launcher/minecraft/auth/flows/AuthRequest.h deleted file mode 100644 index a547aea4..00000000 --- a/launcher/minecraft/auth/flows/AuthRequest.h +++ /dev/null @@ -1,64 +0,0 @@ -#pragma once -#include <QObject> -#include <QNetworkRequest> -#include <QNetworkReply> -#include <QNetworkAccessManager> -#include <QUrl> -#include <QByteArray> - -#include "katabasis/Reply.h" - -/// Makes authentication requests. -class AuthRequest: public QObject { - Q_OBJECT - -public: - explicit AuthRequest(QObject *parent = 0); - ~AuthRequest(); - -public slots: - void get(const QNetworkRequest &req, int timeout = 60*1000); - void post(const QNetworkRequest &req, const QByteArray &data, int timeout = 60*1000); - - -signals: - - /// Emitted when a request has been completed or failed. - void finished(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers); - - /// Emitted when an upload has progressed. - void uploadProgress(qint64 bytesSent, qint64 bytesTotal); - -protected slots: - - /// Handle request finished. - void onRequestFinished(); - - /// Handle request error. - void onRequestError(QNetworkReply::NetworkError error); - - /// Handle ssl errors. - void onSslErrors(QList<QSslError> errors); - - /// Finish the request, emit finished() signal. - void finish(); - - /// Handle upload progress. - void onUploadProgress(qint64 uploaded, qint64 total); - -protected: - void setup(const QNetworkRequest &request, QNetworkAccessManager::Operation operation, const QByteArray &verb = QByteArray()); - - enum Status { - Idle, Requesting, ReRequesting - }; - - QNetworkRequest request_; - QByteArray data_; - QNetworkReply *reply_; - Status status_; - QNetworkAccessManager::Operation operation_; - QUrl url_; - Katabasis::ReplyList timedReplies_; - QNetworkReply::NetworkError error_; -}; diff --git a/launcher/minecraft/auth/flows/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/flows/Parsers.cpp b/launcher/minecraft/auth/flows/Parsers.cpp deleted file mode 100644 index ecb11cf9..00000000 --- a/launcher/minecraft/auth/flows/Parsers.cpp +++ /dev/null @@ -1,316 +0,0 @@ -#include "Parsers.h" - -#include <QJsonDocument> -#include <QJsonArray> -#include <QDebug> - -namespace Parsers { - -bool getDateTime(QJsonValue value, QDateTime & out) { - if(!value.isString()) { - return false; - } - out = QDateTime::fromString(value.toString(), Qt::ISODate); - return out.isValid(); -} - -bool getString(QJsonValue value, QString & out) { - if(!value.isString()) { - return false; - } - out = value.toString(); - return true; -} - -bool getNumber(QJsonValue value, double & out) { - if(!value.isDouble()) { - return false; - } - out = value.toDouble(); - return true; -} - -bool getNumber(QJsonValue value, int64_t & out) { - if(!value.isDouble()) { - return false; - } - out = (int64_t) value.toDouble(); - return true; -} - -bool getBool(QJsonValue value, bool & out) { - if(!value.isBool()) { - return false; - } - out = value.toBool(); - return true; -} - -/* -{ - "IssueInstant":"2020-12-07T19:52:08.4463796Z", - "NotAfter":"2020-12-21T19:52:08.4463796Z", - "Token":"token", - "DisplayClaims":{ - "xui":[ - { - "uhs":"userhash" - } - ] - } - } -*/ -// TODO: handle error responses ... -/* -{ - "Identity":"0", - "XErr":2148916238, - "Message":"", - "Redirect":"https://start.ui.xboxlive.com/AddChildToFamily" -} -// 2148916233 = missing XBox account -// 2148916238 = child account not linked to a family -*/ - -bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output, const char * name) { - qDebug() << "Parsing" << name <<":"; -#ifndef NDEBUG - qDebug() << data; -#endif - QJsonParseError jsonError; - QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); - if(jsonError.error) { - qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString(); - return false; - } - - auto obj = doc.object(); - if(!getDateTime(obj.value("IssueInstant"), output.issueInstant)) { - qWarning() << "User IssueInstant is not a timestamp"; - return false; - } - if(!getDateTime(obj.value("NotAfter"), output.notAfter)) { - qWarning() << "User NotAfter is not a timestamp"; - return false; - } - if(!getString(obj.value("Token"), output.token)) { - qWarning() << "User Token is not a timestamp"; - return false; - } - auto arrayVal = obj.value("DisplayClaims").toObject().value("xui"); - if(!arrayVal.isArray()) { - qWarning() << "Missing xui claims array"; - return false; - } - bool foundUHS = false; - for(auto item: arrayVal.toArray()) { - if(!item.isObject()) { - continue; - } - auto obj = item.toObject(); - if(obj.contains("uhs")) { - foundUHS = true; - } else { - continue; - } - // consume all 'display claims' ... whatever that means - for(auto iter = obj.begin(); iter != obj.end(); iter++) { - QString claim; - if(!getString(obj.value(iter.key()), claim)) { - qWarning() << "display claim " << iter.key() << " is not a string..."; - return false; - } - output.extra[iter.key()] = claim; - } - - break; - } - if(!foundUHS) { - qWarning() << "Missing uhs"; - return false; - } - output.validity = Katabasis::Validity::Certain; - qDebug() << name << "is valid."; - return true; -} - -bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) { - qDebug() << "Parsing Minecraft profile..."; -#ifndef NDEBUG - qDebug() << data; -#endif - - QJsonParseError jsonError; - QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); - if(jsonError.error) { - qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString(); - return false; - } - - auto obj = doc.object(); - if(!getString(obj.value("id"), output.id)) { - qWarning() << "Minecraft profile id is not a string"; - return false; - } - - if(!getString(obj.value("name"), output.name)) { - qWarning() << "Minecraft profile name is not a string"; - return false; - } - - auto skinsArray = obj.value("skins").toArray(); - for(auto skin: skinsArray) { - auto skinObj = skin.toObject(); - Skin skinOut; - if(!getString(skinObj.value("id"), skinOut.id)) { - continue; - } - QString state; - if(!getString(skinObj.value("state"), state)) { - continue; - } - if(state != "ACTIVE") { - continue; - } - if(!getString(skinObj.value("url"), skinOut.url)) { - continue; - } - if(!getString(skinObj.value("variant"), skinOut.variant)) { - continue; - } - // we deal with only the active skin - output.skin = skinOut; - break; - } - auto capesArray = obj.value("capes").toArray(); - - QString currentCape; - for(auto cape: capesArray) { - auto capeObj = cape.toObject(); - Cape capeOut; - if(!getString(capeObj.value("id"), capeOut.id)) { - continue; - } - QString state; - if(!getString(capeObj.value("state"), state)) { - continue; - } - if(state == "ACTIVE") { - currentCape = capeOut.id; - } - if(!getString(capeObj.value("url"), capeOut.url)) { - continue; - } - if(!getString(capeObj.value("alias"), capeOut.alias)) { - continue; - } - - output.capes[capeOut.id] = capeOut; - } - output.currentCape = currentCape; - output.validity = Katabasis::Validity::Certain; - return true; -} - -bool parseMinecraftEntitlements(QByteArray & data, MinecraftEntitlement &output) { - qDebug() << "Parsing Minecraft entitlements..."; -#ifndef NDEBUG - qDebug() << data; -#endif - - QJsonParseError jsonError; - QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); - if(jsonError.error) { - qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString(); - return false; - } - - auto obj = doc.object(); - - auto itemsArray = obj.value("items").toArray(); - for(auto item: itemsArray) { - auto itemObj = item.toObject(); - QString name; - if(!getString(itemObj.value("name"), name)) { - continue; - } - if(name == "game_minecraft") { - output.canPlayMinecraft = true; - } - if(name == "product_minecraft") { - output.ownsMinecraft = true; - } - } - output.validity = Katabasis::Validity::Certain; - return true; -} - -bool parseRolloutResponse(QByteArray & data, bool& result) { - qDebug() << "Parsing Rollout response..."; -#ifndef NDEBUG - qDebug() << data; -#endif - - QJsonParseError jsonError; - QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); - if(jsonError.error) { - qWarning() << "Failed to parse response from https://api.minecraftservices.com/rollout/v1/msamigration as JSON: " << jsonError.errorString(); - return false; - } - - auto obj = doc.object(); - QString feature; - if(!getString(obj.value("feature"), feature)) { - qWarning() << "Rollout feature is not a string"; - return false; - } - if(feature != "msamigration") { - qWarning() << "Rollout feature is not what we expected (msamigration), but is instead \"" << feature << "\""; - return false; - } - if(!getBool(obj.value("rollout"), result)) { - qWarning() << "Rollout feature is not a string"; - return false; - } - return true; -} - -bool parseMojangResponse(QByteArray & data, Katabasis::Token &output) { - QJsonParseError jsonError; - qDebug() << "Parsing Mojang response..."; -#ifndef NDEBUG - qDebug() << data; -#endif - QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); - if(jsonError.error) { - qWarning() << "Failed to parse response from api.minecraftservices.com/launcher/login as JSON: " << jsonError.errorString(); - return false; - } - - auto obj = doc.object(); - double expires_in = 0; - if(!getNumber(obj.value("expires_in"), expires_in)) { - qWarning() << "expires_in is not a valid number"; - return false; - } - auto currentTime = QDateTime::currentDateTimeUtc(); - output.issueInstant = currentTime; - output.notAfter = currentTime.addSecs(expires_in); - - QString username; - if(!getString(obj.value("username"), username)) { - qWarning() << "username is not valid"; - return false; - } - - // TODO: it's a JWT... validate it? - if(!getString(obj.value("access_token"), output.token)) { - qWarning() << "access_token is not valid"; - return false; - } - output.validity = Katabasis::Validity::Certain; - qDebug() << "Mojang response is valid."; - return true; -} - -} diff --git a/launcher/minecraft/auth/flows/Parsers.h b/launcher/minecraft/auth/flows/Parsers.h deleted file mode 100644 index b484a073..00000000 --- a/launcher/minecraft/auth/flows/Parsers.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include "../AccountData.h" - -namespace Parsers -{ - bool getDateTime(QJsonValue value, QDateTime & out); - bool getString(QJsonValue value, QString & out); - bool getNumber(QJsonValue value, double & out); - bool getNumber(QJsonValue value, int64_t & out); - bool getBool(QJsonValue value, bool & out); - - bool parseXTokenResponse(QByteArray &data, Katabasis::Token &output, const char * name); - bool parseMojangResponse(QByteArray &data, Katabasis::Token &output); - - bool parseMinecraftProfile(QByteArray &data, MinecraftProfile &output); - bool parseMinecraftEntitlements(QByteArray &data, MinecraftEntitlement &output); - bool parseRolloutResponse(QByteArray &data, bool& result); -} diff --git a/launcher/minecraft/auth/flows/Yggdrasil.cpp b/launcher/minecraft/auth/flows/Yggdrasil.cpp deleted file mode 100644 index 5ea168e8..00000000 --- a/launcher/minecraft/auth/flows/Yggdrasil.cpp +++ /dev/null @@ -1,331 +0,0 @@ -/* Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Yggdrasil.h" -#include "../AccountData.h" - -#include <QObject> -#include <QString> -#include <QJsonObject> -#include <QJsonDocument> -#include <QNetworkReply> -#include <QByteArray> - -#include <QDebug> - -#include "Application.h" - -Yggdrasil::Yggdrasil(AccountData *data, QObject *parent) - : AccountTask(data, parent) -{ - changeState(STATE_CREATED); -} - -void Yggdrasil::sendRequest(QUrl endpoint, QByteArray content) { - changeState(STATE_WORKING); - - QNetworkRequest netRequest(endpoint); - netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - m_netReply = APPLICATION->network()->post(netRequest, content); - connect(m_netReply, &QNetworkReply::finished, this, &Yggdrasil::processReply); - connect(m_netReply, &QNetworkReply::uploadProgress, this, &Yggdrasil::refreshTimers); - connect(m_netReply, &QNetworkReply::downloadProgress, this, &Yggdrasil::refreshTimers); - connect(m_netReply, &QNetworkReply::sslErrors, this, &Yggdrasil::sslErrors); - timeout_keeper.setSingleShot(true); - timeout_keeper.start(timeout_max); - counter.setSingleShot(false); - counter.start(time_step); - progress(0, timeout_max); - connect(&timeout_keeper, &QTimer::timeout, this, &Yggdrasil::abortByTimeout); - connect(&counter, &QTimer::timeout, this, &Yggdrasil::heartbeat); -} - -void Yggdrasil::executeTask() { -} - -void Yggdrasil::refresh() { - start(); - /* - * { - * "clientToken": "client identifier" - * "accessToken": "current access token to be refreshed" - * "selectedProfile": // specifying this causes errors - * { - * "id": "profile ID" - * "name": "profile name" - * } - * "requestUser": true/false // request the user structure - * } - */ - QJsonObject req; - req.insert("clientToken", m_data->clientToken()); - req.insert("accessToken", m_data->accessToken()); - /* - { - auto currentProfile = m_account->currentProfile(); - QJsonObject profile; - profile.insert("id", currentProfile->id()); - profile.insert("name", currentProfile->name()); - req.insert("selectedProfile", profile); - } - */ - req.insert("requestUser", false); - QJsonDocument doc(req); - - QUrl reqUrl("https://authserver.mojang.com/refresh"); - QByteArray requestData = doc.toJson(); - - sendRequest(reqUrl, requestData); -} - -void Yggdrasil::login(QString password) { - start(); - /* - * { - * "agent": { // optional - * "name": "Minecraft", // So far this is the only encountered value - * "version": 1 // This number might be increased - * // by the vanilla client in the future - * }, - * "username": "mojang account name", // Can be an email address or player name for - * // unmigrated accounts - * "password": "mojang account password", - * "clientToken": "client identifier", // optional - * "requestUser": true/false // request the user structure - * } - */ - QJsonObject req; - - { - QJsonObject agent; - // C++ makes string literals void* for some stupid reason, so we have to tell it - // QString... Thanks Obama. - agent.insert("name", QString("Minecraft")); - agent.insert("version", 1); - req.insert("agent", agent); - } - - req.insert("username", m_data->userName()); - req.insert("password", password); - req.insert("requestUser", false); - - // If we already have a client token, give it to the server. - // Otherwise, let the server give us one. - - m_data->generateClientTokenIfMissing(); - req.insert("clientToken", m_data->clientToken()); - - QJsonDocument doc(req); - - QUrl reqUrl("https://authserver.mojang.com/authenticate"); - QNetworkRequest netRequest(reqUrl); - QByteArray requestData = doc.toJson(); - - sendRequest(reqUrl, requestData); -} - - - -void Yggdrasil::refreshTimers(qint64, qint64) { - timeout_keeper.stop(); - timeout_keeper.start(timeout_max); - progress(count = 0, timeout_max); -} - -void Yggdrasil::heartbeat() { - count += time_step; - progress(count, timeout_max); -} - -bool Yggdrasil::abort() { - progress(timeout_max, timeout_max); - // TODO: actually use this in a meaningful way - m_aborted = Yggdrasil::BY_USER; - m_netReply->abort(); - return true; -} - -void Yggdrasil::abortByTimeout() { - progress(timeout_max, timeout_max); - // TODO: actually use this in a meaningful way - m_aborted = Yggdrasil::BY_TIMEOUT; - m_netReply->abort(); -} - -void Yggdrasil::sslErrors(QList<QSslError> errors) { - int i = 1; - for (auto error : errors) { - qCritical() << "LOGIN SSL Error #" << i << " : " << error.errorString(); - auto cert = error.certificate(); - qCritical() << "Certificate in question:\n" << cert.toText(); - i++; - } -} - -void Yggdrasil::processResponse(QJsonObject responseData) { - // Read the response data. We need to get the client token, access token, and the selected - // profile. - qDebug() << "Processing authentication response."; - - // qDebug() << responseData; - // If we already have a client token, make sure the one the server gave us matches our - // existing one. - QString clientToken = responseData.value("clientToken").toString(""); - if (clientToken.isEmpty()) { - // Fail if the server gave us an empty client token - changeState(STATE_FAILED_HARD, tr("Authentication server didn't send a client token.")); - return; - } - if(m_data->clientToken().isEmpty()) { - m_data->setClientToken(clientToken); - } - else if(clientToken != m_data->clientToken()) { - changeState(STATE_FAILED_HARD, tr("Authentication server attempted to change the client token. This isn't supported.")); - return; - } - - // Now, we set the access token. - qDebug() << "Getting access token."; - QString accessToken = responseData.value("accessToken").toString(""); - if (accessToken.isEmpty()) { - // Fail if the server didn't give us an access token. - changeState(STATE_FAILED_HARD, tr("Authentication server didn't send an access token.")); - return; - } - // Set the access token. - m_data->yggdrasilToken.token = accessToken; - m_data->yggdrasilToken.validity = Katabasis::Validity::Certain; - m_data->yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc(); - - // We've made it through the minefield of possible errors. Return true to indicate that - // we've succeeded. - qDebug() << "Finished reading authentication response."; - changeState(STATE_SUCCEEDED); -} - -void Yggdrasil::processReply() { - changeState(STATE_WORKING); - - switch (m_netReply->error()) - { - case QNetworkReply::NoError: - break; - case QNetworkReply::TimeoutError: - changeState(STATE_FAILED_SOFT, tr("Authentication operation timed out.")); - return; - case QNetworkReply::OperationCanceledError: - changeState(STATE_FAILED_SOFT, tr("Authentication operation cancelled.")); - return; - case QNetworkReply::SslHandshakeFailedError: - changeState( - STATE_FAILED_SOFT, - tr( - "<b>SSL Handshake failed.</b><br/>There might be a few causes for it:<br/>" - "<ul>" - "<li>You use Windows and need to update your root certificates, please install any outstanding updates.</li>" - "<li>Some device on your network is interfering with SSL traffic. In that case, " - "you have bigger worries than Minecraft not starting.</li>" - "<li>Possibly something else. Check the log file for details</li>" - "</ul>" - ) - ); - return; - // used for invalid credentials and similar errors. Fall through. - case QNetworkReply::ContentAccessDenied: - case QNetworkReply::ContentOperationNotPermittedError: - break; - case QNetworkReply::ContentGoneError: { - changeState( - STATE_FAILED_GONE, - tr("The Mojang account no longer exists. It may have been migrated to a Microsoft account.") - ); - } - default: - changeState( - STATE_FAILED_SOFT, - tr("Authentication operation failed due to a network error: %1 (%2)").arg(m_netReply->errorString()).arg(m_netReply->error()) - ); - return; - } - - // Try to parse the response regardless of the response code. - // Sometimes the auth server will give more information and an error code. - QJsonParseError jsonError; - QByteArray replyData = m_netReply->readAll(); - QJsonDocument doc = QJsonDocument::fromJson(replyData, &jsonError); - // Check the response code. - int responseCode = m_netReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - if (responseCode == 200) { - // If the response code was 200, then there shouldn't be an error. Make sure - // anyways. - // Also, sometimes an empty reply indicates success. If there was no data received, - // pass an empty json object to the processResponse function. - if (jsonError.error == QJsonParseError::NoError || replyData.size() == 0) { - processResponse(replyData.size() > 0 ? doc.object() : QJsonObject()); - return; - } - else { - changeState( - STATE_FAILED_SOFT, - tr("Failed to parse authentication server response JSON response: %1 at offset %2.").arg(jsonError.errorString()).arg(jsonError.offset) - ); - qCritical() << replyData; - } - return; - } - - // If the response code was not 200, then Yggdrasil may have given us information - // about the error. - // If we can parse the response, then get information from it. Otherwise just say - // there was an unknown error. - if (jsonError.error == QJsonParseError::NoError) { - // We were able to parse the server's response. Woo! - // Call processError. If a subclass has overridden it then they'll handle their - // stuff there. - qDebug() << "The request failed, but the server gave us an error message. Processing error."; - processError(doc.object()); - } - else { - // The server didn't say anything regarding the error. Give the user an unknown - // error. - qDebug() << "The request failed and the server gave no error message. Unknown error."; - changeState( - STATE_FAILED_SOFT, - tr("An unknown error occurred when trying to communicate with the authentication server: %1").arg(m_netReply->errorString()) - ); - } -} - -void Yggdrasil::processError(QJsonObject responseData) { - QJsonValue errorVal = responseData.value("error"); - QJsonValue errorMessageValue = responseData.value("errorMessage"); - QJsonValue causeVal = responseData.value("cause"); - - if (errorVal.isString() && errorMessageValue.isString()) { - m_error = std::shared_ptr<Error>( - new Error { - errorVal.toString(""), - errorMessageValue.toString(""), - causeVal.toString("") - } - ); - changeState(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.")); - } -} diff --git a/launcher/minecraft/auth/flows/Yggdrasil.h b/launcher/minecraft/auth/flows/Yggdrasil.h deleted file mode 100644 index b9670ec7..00000000 --- a/launcher/minecraft/auth/flows/Yggdrasil.h +++ /dev/null @@ -1,86 +0,0 @@ -/* Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#include "../AccountTask.h" - -#include <QString> -#include <QJsonObject> -#include <QTimer> -#include <qsslerror.h> - -#include "../MinecraftAccount.h" - -class QNetworkAccessManager; -class QNetworkReply; - -/** - * A Yggdrasil task is a task that performs an operation on a given mojang account. - */ -class Yggdrasil : public AccountTask -{ - Q_OBJECT -public: - explicit Yggdrasil( - AccountData *data, - QObject *parent = 0 - ); - virtual ~Yggdrasil() {}; - - void refresh(); - void login(QString password); -protected: - void executeTask() override; - - /** - * Processes the response received from the server. - * If an error occurred, this should emit a failed signal. - * If Yggdrasil gave an error response, it should call setError() first, and then return false. - * Otherwise, it should return true. - * Note: If the response from the server was blank, and the HTTP code was 200, this function is called with - * an empty QJsonObject. - */ - void processResponse(QJsonObject responseData); - - /** - * Processes an error response received from the server. - * The default implementation will read data from Yggdrasil's standard error response format and set it as this task's Error. - * \returns a QString error message that will be passed to emitFailed. - */ - virtual void processError(QJsonObject responseData); - -protected slots: - void processReply(); - void refreshTimers(qint64, qint64); - void heartbeat(); - void sslErrors(QList<QSslError>); - void abortByTimeout(); - -public slots: - virtual bool abort() override; - -private: - void sendRequest(QUrl endpoint, QByteArray content); - -protected: - QNetworkReply *m_netReply = nullptr; - QTimer timeout_keeper; - QTimer counter; - int count = 0; // num msec since time reset - - const int timeout_max = 30000; - const int time_step = 50; -}; |