diff options
Diffstat (limited to 'launcher/minecraft/auth/flows')
-rw-r--r-- | launcher/minecraft/auth/flows/AuthContext.cpp | 466 | ||||
-rw-r--r-- | launcher/minecraft/auth/flows/AuthContext.h | 13 | ||||
-rw-r--r-- | launcher/minecraft/auth/flows/AuthRequest.cpp | 6 | ||||
-rw-r--r-- | launcher/minecraft/auth/flows/AuthRequest.h | 1 | ||||
-rw-r--r-- | launcher/minecraft/auth/flows/MSAInteractive.cpp | 8 | ||||
-rw-r--r-- | launcher/minecraft/auth/flows/MSAInteractive.h | 5 | ||||
-rw-r--r-- | launcher/minecraft/auth/flows/MSASilent.h | 5 | ||||
-rw-r--r-- | launcher/minecraft/auth/flows/MojangLogin.cpp | 6 | ||||
-rw-r--r-- | launcher/minecraft/auth/flows/MojangLogin.h | 6 | ||||
-rw-r--r-- | launcher/minecraft/auth/flows/MojangRefresh.cpp | 5 | ||||
-rw-r--r-- | launcher/minecraft/auth/flows/MojangRefresh.h | 2 | ||||
-rw-r--r-- | launcher/minecraft/auth/flows/Parsers.cpp | 316 | ||||
-rw-r--r-- | launcher/minecraft/auth/flows/Parsers.h | 19 | ||||
-rw-r--r-- | launcher/minecraft/auth/flows/Yggdrasil.cpp | 72 | ||||
-rw-r--r-- | launcher/minecraft/auth/flows/Yggdrasil.h | 6 |
15 files changed, 519 insertions, 417 deletions
diff --git a/launcher/minecraft/auth/flows/AuthContext.cpp b/launcher/minecraft/auth/flows/AuthContext.cpp index 9fb3ec48..00957fd4 100644 --- a/launcher/minecraft/auth/flows/AuthContext.cpp +++ b/launcher/minecraft/auth/flows/AuthContext.cpp @@ -4,25 +4,21 @@ #include <QDesktopServices> #include <QMetaEnum> #include <QDebug> - #include <QJsonDocument> #include <QJsonObject> #include <QJsonArray> - +#include <QUuid> #include <QUrlQuery> -#include <QPixmap> -#include <QPainter> - #include "AuthContext.h" #include "katabasis/Globals.h" #include "AuthRequest.h" -#include "Secrets.h" +#include "Parsers.h" -#include "Env.h" +#include <Application.h> -using OAuth2 = Katabasis::OAuth2; +using OAuth2 = Katabasis::DeviceFlow; using Activity = Katabasis::Activity; AuthContext::AuthContext(AccountData * data, QObject *parent) : @@ -54,25 +50,17 @@ void AuthContext::initMSA() { return; } - auto clientId = Secrets::getMSAClientID('-'); - if(clientId.isEmpty()) { - return; - } - - Katabasis::OAuth2::Options opts; + OAuth2::Options opts; opts.scope = "XboxLive.signin offline_access"; - opts.clientIdentifier = clientId; + 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"; - opts.listenerPorts = {28562, 28563, 28564, 28565, 28566}; - m_oauth2 = new OAuth2(opts, m_data->msaToken, this, &ENV.qnam()); - m_oauth2->setGrantFlow(Katabasis::OAuth2::GrantFlowDevice); + // 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::linkingFailed, this, &AuthContext::onOAuthLinkingFailed); - connect(m_oauth2, &OAuth2::linkingSucceeded, this, &AuthContext::onOAuthLinkingSucceeded); - connect(m_oauth2, &OAuth2::showVerificationUriAndCode, this, &AuthContext::showVerificationUriAndCode); connect(m_oauth2, &OAuth2::activityChanged, this, &AuthContext::onOAuthActivityChanged); + connect(m_oauth2, &OAuth2::showVerificationUriAndCode, this, &AuthContext::showVerificationUriAndCode); } void AuthContext::initMojang() { @@ -97,50 +85,56 @@ void AuthContext::onMojangFailed() { changeState(m_yggdrasil->accountState(), tr("Mojang user authentication failed.")); } -/* -bool AuthContext::signOut() { - if(isBusy()) { - return false; - } - - start(); - - beginActivity(Activity::LoggingOut); - m_oauth2->unlink(); - m_account = AccountData(); - finishActivity(); - return true; -} -*/ - -void AuthContext::onOAuthLinkingFailed() { - emit hideVerificationUriAndCode(); - finishActivity(); - changeState(STATE_FAILED_HARD, tr("Microsoft user authentication failed.")); -} - -void AuthContext::onOAuthLinkingSucceeded() { - emit hideVerificationUriAndCode(); - auto *o2t = qobject_cast<OAuth2 *>(sender()); - if (!o2t->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 = o2t->extraTokens(); -#ifndef NDEBUG - if (!extraTokens.isEmpty()) { - qDebug() << "Extra tokens in response:"; - foreach (QString key, extraTokens.keys()) { - qDebug() << "\t" << key << ":" << extraTokens.value(key); +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(); -} + 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::onOAuthActivityChanged(Katabasis::Activity activity) { - // respond to activity change here + } } void AuthContext::doUserAuth() { @@ -169,137 +163,6 @@ void AuthContext::doUserAuth() { qDebug() << "First layer of XBox auth ... commencing."; } -namespace { -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; -} - -} - void AuthContext::onUserAuthDone( QNetworkReply::NetworkError error, QByteArray replyData, @@ -313,7 +176,7 @@ void AuthContext::onUserAuthDone( } Katabasis::Token temp; - if(!parseXTokenResponse(replyData, temp, "UToken")) { + 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.")); @@ -365,7 +228,7 @@ void AuthContext::doSTSAuthMinecraft() { void AuthContext::processSTSError(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers) { if(error == QNetworkReply::AuthenticationRequiredError) { - QJsonParseError jsonError; + QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); if(jsonError.error) { qWarning() << "Cannot parse error XSTS response as JSON: " << jsonError.errorString(); @@ -374,7 +237,7 @@ void AuthContext::processSTSError(QNetworkReply::NetworkError error, QByteArray int64_t errorCode = -1; auto obj = doc.object(); - if(!getNumber(obj.value("XErr"), errorCode)) { + if(!Parsers::getNumber(obj.value("XErr"), errorCode)) { qWarning() << "XErr is not a number"; return; } @@ -400,7 +263,7 @@ void AuthContext::onSTSAuthMinecraftDone( } Katabasis::Token temp; - if(!parseXTokenResponse(replyData, temp, "STSAuthMinecraft")) { + if(!Parsers::parseXTokenResponse(replyData, temp, "STSAuthMinecraft")) { qWarning() << "Could not parse authorization response for access to mojang services..."; failResult(m_mcAuthSucceeded); return; @@ -417,67 +280,33 @@ void AuthContext::onSTSAuthMinecraftDone( } 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( { - "identityToken": "XBL3.0 x=%1;%2" + "xtoken": "XBL3.0 x=%1;%2", + "platform": "PC_LAUNCHER" } )XXX"; - auto data = mc_auth_template.arg(m_data->mojangservicesToken.extra["uhs"].toString(), m_data->mojangservicesToken.token); + auto requestBody = mc_auth_template.arg(uhs, xToken); - QNetworkRequest request = QNetworkRequest(QUrl("https://api.minecraftservices.com/authentication/login_with_xbox")); + 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, data.toUtf8()); + requestor->post(request, requestBody.toUtf8()); qDebug() << "Getting Minecraft access token..."; } -namespace { -bool parseMojangResponse(QByteArray & data, Katabasis::Token &output) { - QJsonParseError jsonError; - qDebug() << "Parsing Mojang response..."; -#ifndef NDEBUG - qDebug() << data; -#endif - QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); - if(jsonError.error) { - qWarning() << "Failed to parse response from api.minecraftservices.com/authentication/login_with_xbox 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; -} -} - void AuthContext::onMinecraftAuthDone( QNetworkReply::NetworkError error, QByteArray replyData, QList<QNetworkReply::RawHeaderPair> headers ) { + qDebug() << replyData; if (error != QNetworkReply::NoError) { qWarning() << "Reply error:" << error; #ifndef NDEBUG @@ -487,7 +316,7 @@ void AuthContext::onMinecraftAuthDone( return; } - if(!parseMojangResponse(replyData, m_data->yggdrasilToken)) { + if(!Parsers::parseMojangResponse(replyData, m_data->yggdrasilToken)) { qWarning() << "Could not parse login_with_xbox response..."; #ifndef NDEBUG qDebug() << replyData; @@ -539,7 +368,7 @@ void AuthContext::onSTSAuthGenericDone( } Katabasis::Token temp; - if(!parseXTokenResponse(replyData, temp, "STSAuthGeneric")) { + if(!Parsers::parseXTokenResponse(replyData, temp, "STSAuthGeneric")) { qWarning() << "Could not parse authorization response for access to xbox API..."; failResult(m_xboxProfileSucceeded); return; @@ -619,7 +448,7 @@ void AuthContext::checkResult() { return; } if(m_mcAuthSucceeded && m_xboxProfileSucceeded) { - doMinecraftProfile(); + doEntitlements(); } else { finishActivity(); @@ -662,84 +491,33 @@ void AuthContext::checkResult() { } } -namespace { -bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) { - qDebug() << "Parsing Minecraft profile..."; +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 - - 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; -} + // TODO: check presence of same entitlementsRequestId? + // TODO: validate JWTs? + Parsers::parseMinecraftEntitlements(data, m_data->minecraftEntitlement); + doMinecraftProfile(); } void AuthContext::doMinecraftProfile() { @@ -766,9 +544,13 @@ void AuthContext::onMinecraftProfileDone( 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(); - finishActivity(); - changeState(STATE_FAILED_HARD, tr("Account is missing a Minecraft Java profile.\n\nWhile the Microsoft account is valid, it does not own the game.\n\nYou might own Bedrock on this account, but that does not give you access to Java currently.")); + succeed(); return; } if (error != QNetworkReply::NoError) { @@ -776,7 +558,7 @@ void AuthContext::onMinecraftProfileDone( changeState(STATE_FAILED_HARD, tr("Minecraft Java profile acquisition failed.")); return; } - if(!parseMinecraftProfile(data, m_data->minecraftProfile)) { + 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")); @@ -784,6 +566,9 @@ void AuthContext::onMinecraftProfileDone( } 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 { @@ -805,43 +590,13 @@ void AuthContext::doMigrationEligibilityCheck() { requestor->get(request); } -bool parseRolloutResponse(QByteArray & data, bool& result) { - qDebug() << "Parsing Rollout response..."; -#ifndef NDEBUG - qDebug() << data; -#endif - - QJsonParseError jsonError; - QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); - if(jsonError.error) { - qWarning() << "Failed to parse response from https://api.minecraftservices.com/rollout/v1/msamigration as JSON: " << jsonError.errorString(); - return false; - } - - auto obj = doc.object(); - QString feature; - if(!getString(obj.value("feature"), feature)) { - qWarning() << "Rollout feature is not a string"; - return false; - } - if(feature != "msamigration") { - qWarning() << "Rollout feature is not what we expected (msamigration), but is instead \"" << feature << "\""; - return false; - } - if(!getBool(obj.value("rollout"), result)) { - qWarning() << "Rollout feature is not a string"; - return false; - } - return true; -} - void AuthContext::onMigrationEligibilityCheckDone( QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers ) { if (error == QNetworkReply::NoError) { - parseRolloutResponse(data, m_data->canMigrateToMSA); + Parsers::parseRolloutResponse(data, m_data->canMigrateToMSA); } doGetSkin(); } @@ -865,6 +620,11 @@ void AuthContext::onSkinDone( 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")); diff --git a/launcher/minecraft/auth/flows/AuthContext.h b/launcher/minecraft/auth/flows/AuthContext.h index dc7552ac..5e4e9edc 100644 --- a/launcher/minecraft/auth/flows/AuthContext.h +++ b/launcher/minecraft/auth/flows/AuthContext.h @@ -7,7 +7,7 @@ #include <QNetworkReply> #include <QImage> -#include <katabasis/OAuth2.h> +#include <katabasis/DeviceFlow.h> #include "Yggdrasil.h" #include "../AccountData.h" #include "../AccountTask.h" @@ -35,9 +35,6 @@ signals: private slots: // OAuth-specific callbacks - void onOAuthLinkingSucceeded(); - void onOAuthLinkingFailed(); - void onOAuthActivityChanged(Katabasis::Activity activity); // Yggdrasil specific callbacks @@ -63,6 +60,9 @@ protected: 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>); @@ -72,6 +72,8 @@ protected: void doGetSkin(); Q_SLOT void onSkinDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>); + void succeed(); + void failResult(bool & flag); void succeedResult(bool & flag); void checkResult(); @@ -82,12 +84,13 @@ protected: void clearTokens(); protected: - Katabasis::OAuth2 *m_oauth2 = nullptr; + 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; diff --git a/launcher/minecraft/auth/flows/AuthRequest.cpp b/launcher/minecraft/auth/flows/AuthRequest.cpp index 77558fd3..82dba591 100644 --- a/launcher/minecraft/auth/flows/AuthRequest.cpp +++ b/launcher/minecraft/auth/flows/AuthRequest.cpp @@ -5,9 +5,9 @@ #include <QBuffer> #include <QUrlQuery> +#include "Application.h" #include "AuthRequest.h" #include "katabasis/Globals.h" -#include "Env.h" AuthRequest::AuthRequest(QObject *parent): QObject(parent) { } @@ -17,7 +17,7 @@ AuthRequest::~AuthRequest() { void AuthRequest::get(const QNetworkRequest &req, int timeout/* = 60*1000*/) { setup(req, QNetworkAccessManager::GetOperation); - reply_ = ENV.qnam().get(request_); + 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))); @@ -29,7 +29,7 @@ void AuthRequest::post(const QNetworkRequest &req, const QByteArray &data, int t setup(req, QNetworkAccessManager::PostOperation); data_ = data; status_ = Requesting; - reply_ = ENV.qnam().post(request_, data_); + 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())); diff --git a/launcher/minecraft/auth/flows/AuthRequest.h b/launcher/minecraft/auth/flows/AuthRequest.h index 6a45a0bd..a547aea4 100644 --- a/launcher/minecraft/auth/flows/AuthRequest.h +++ b/launcher/minecraft/auth/flows/AuthRequest.h @@ -5,7 +5,6 @@ #include <QNetworkAccessManager> #include <QUrl> #include <QByteArray> -#include <QHttpMultiPart> #include "katabasis/Reply.h" diff --git a/launcher/minecraft/auth/flows/MSAInteractive.cpp b/launcher/minecraft/auth/flows/MSAInteractive.cpp index 03beb279..525aaf88 100644 --- a/launcher/minecraft/auth/flows/MSAInteractive.cpp +++ b/launcher/minecraft/auth/flows/MSAInteractive.cpp @@ -1,6 +1,9 @@ #include "MSAInteractive.h" -MSAInteractive::MSAInteractive(AccountData* data, QObject* parent) : AuthContext(data, parent) {} +MSAInteractive::MSAInteractive( + AccountData* data, + QObject* parent +) : AuthContext(data, parent) {} void MSAInteractive::executeTask() { m_requestsDone = 0; @@ -14,7 +17,6 @@ void MSAInteractive::executeTask() { m_oauth2->setExtraRequestParams(extraOpts); beginActivity(Katabasis::Activity::LoggingIn); - m_oauth2->unlink(); *m_data = AccountData(); - m_oauth2->link(); + m_oauth2->login(); } diff --git a/launcher/minecraft/auth/flows/MSAInteractive.h b/launcher/minecraft/auth/flows/MSAInteractive.h index 9556f254..6654e0d6 100644 --- a/launcher/minecraft/auth/flows/MSAInteractive.h +++ b/launcher/minecraft/auth/flows/MSAInteractive.h @@ -5,6 +5,9 @@ class MSAInteractive : public AuthContext { Q_OBJECT public: - explicit MSAInteractive(AccountData * data, QObject *parent = 0); + explicit MSAInteractive( + AccountData *data, + QObject *parent = 0 + ); void executeTask() override; }; diff --git a/launcher/minecraft/auth/flows/MSASilent.h b/launcher/minecraft/auth/flows/MSASilent.h index e1b3d43d..a442b49e 100644 --- a/launcher/minecraft/auth/flows/MSASilent.h +++ b/launcher/minecraft/auth/flows/MSASilent.h @@ -5,6 +5,9 @@ class MSASilent : public AuthContext { Q_OBJECT public: - explicit MSASilent(AccountData * data, QObject *parent = 0); + explicit MSASilent( + AccountData * data, + QObject *parent = 0 + ); void executeTask() override; }; diff --git a/launcher/minecraft/auth/flows/MojangLogin.cpp b/launcher/minecraft/auth/flows/MojangLogin.cpp index cca911b5..6c217cd1 100644 --- a/launcher/minecraft/auth/flows/MojangLogin.cpp +++ b/launcher/minecraft/auth/flows/MojangLogin.cpp @@ -1,6 +1,10 @@ #include "MojangLogin.h" -MojangLogin::MojangLogin(AccountData* data, QString password, QObject* parent) : AuthContext(data, parent), m_password(password) {} +MojangLogin::MojangLogin( + AccountData *data, + QString password, + QObject *parent +): AuthContext(data, parent), m_password(password) {} void MojangLogin::executeTask() { m_requestsDone = 0; diff --git a/launcher/minecraft/auth/flows/MojangLogin.h b/launcher/minecraft/auth/flows/MojangLogin.h index 2e765ae8..5f33752f 100644 --- a/launcher/minecraft/auth/flows/MojangLogin.h +++ b/launcher/minecraft/auth/flows/MojangLogin.h @@ -5,7 +5,11 @@ class MojangLogin : public AuthContext { Q_OBJECT public: - explicit MojangLogin(AccountData * data, QString password, QObject *parent = 0); + explicit MojangLogin( + AccountData *data, + QString password, + QObject *parent = 0 + ); void executeTask() override; private: diff --git a/launcher/minecraft/auth/flows/MojangRefresh.cpp b/launcher/minecraft/auth/flows/MojangRefresh.cpp index af99175c..008c0453 100644 --- a/launcher/minecraft/auth/flows/MojangRefresh.cpp +++ b/launcher/minecraft/auth/flows/MojangRefresh.cpp @@ -1,6 +1,9 @@ #include "MojangRefresh.h" -MojangRefresh::MojangRefresh(AccountData* data, QObject* parent) : AuthContext(data, parent) {} +MojangRefresh::MojangRefresh( + AccountData *data, + QObject *parent +) : AuthContext(data, parent) {} void MojangRefresh::executeTask() { m_requestsDone = 0; diff --git a/launcher/minecraft/auth/flows/MojangRefresh.h b/launcher/minecraft/auth/flows/MojangRefresh.h index fb4facd5..06e4e4ce 100644 --- a/launcher/minecraft/auth/flows/MojangRefresh.h +++ b/launcher/minecraft/auth/flows/MojangRefresh.h @@ -5,6 +5,6 @@ class MojangRefresh : public AuthContext { Q_OBJECT public: - explicit MojangRefresh(AccountData * data, QObject *parent = 0); + 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 new file mode 100644 index 00000000..ecb11cf9 --- /dev/null +++ b/launcher/minecraft/auth/flows/Parsers.cpp @@ -0,0 +1,316 @@ +#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 new file mode 100644 index 00000000..b484a073 --- /dev/null +++ b/launcher/minecraft/auth/flows/Parsers.h @@ -0,0 +1,19 @@ +#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 index 20ca63d0..5ea168e8 100644 --- a/launcher/minecraft/auth/flows/Yggdrasil.cpp +++ b/launcher/minecraft/auth/flows/Yggdrasil.cpp @@ -23,12 +23,10 @@ #include <QNetworkReply> #include <QByteArray> -#include <Env.h> - -#include <BuildConfig.h> - #include <QDebug> +#include "Application.h" + Yggdrasil::Yggdrasil(AccountData *data, QObject *parent) : AccountTask(data, parent) { @@ -40,7 +38,7 @@ void Yggdrasil::sendRequest(QUrl endpoint, QByteArray content) { QNetworkRequest netRequest(endpoint); netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - m_netReply = ENV.qnam().post(netRequest, content); + 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); @@ -86,7 +84,7 @@ void Yggdrasil::refresh() { req.insert("requestUser", false); QJsonDocument doc(req); - QUrl reqUrl(BuildConfig.AUTH_BASE + "refresh"); + QUrl reqUrl("https://authserver.mojang.com/refresh"); QByteArray requestData = doc.toJson(); sendRequest(reqUrl, requestData); @@ -131,7 +129,7 @@ void Yggdrasil::login(QString password) { QJsonDocument doc(req); - QUrl reqUrl(BuildConfig.AUTH_BASE + "authenticate"); + QUrl reqUrl("https://authserver.mojang.com/authenticate"); QNetworkRequest netRequest(reqUrl); QByteArray requestData = doc.toJson(); @@ -140,20 +138,18 @@ void Yggdrasil::login(QString password) { -void Yggdrasil::refreshTimers(qint64, qint64) -{ +void Yggdrasil::refreshTimers(qint64, qint64) { timeout_keeper.stop(); timeout_keeper.start(timeout_max); progress(count = 0, timeout_max); } -void Yggdrasil::heartbeat() -{ + +void Yggdrasil::heartbeat() { count += time_step; progress(count, timeout_max); } -bool Yggdrasil::abort() -{ +bool Yggdrasil::abort() { progress(timeout_max, timeout_max); // TODO: actually use this in a meaningful way m_aborted = Yggdrasil::BY_USER; @@ -161,19 +157,16 @@ bool Yggdrasil::abort() return true; } -void Yggdrasil::abortByTimeout() -{ +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) -{ +void Yggdrasil::sslErrors(QList<QSslError> errors) { int i = 1; - for (auto error : errors) - { + for (auto error : errors) { qCritical() << "LOGIN SSL Error #" << i << " : " << error.errorString(); auto cert = error.certificate(); qCritical() << "Certificate in question:\n" << cert.toText(); @@ -181,8 +174,7 @@ void Yggdrasil::sslErrors(QList<QSslError> errors) } } -void Yggdrasil::processResponse(QJsonObject responseData) -{ +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."; @@ -191,8 +183,7 @@ void Yggdrasil::processResponse(QJsonObject 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()) - { + 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; @@ -208,8 +199,7 @@ void Yggdrasil::processResponse(QJsonObject responseData) // Now, we set the access token. qDebug() << "Getting access token."; QString accessToken = responseData.value("accessToken").toString(""); - if (accessToken.isEmpty()) - { + 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; @@ -217,6 +207,7 @@ void Yggdrasil::processResponse(QJsonObject responseData) // 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. @@ -224,8 +215,7 @@ void Yggdrasil::processResponse(QJsonObject responseData) changeState(STATE_SUCCEEDED); } -void Yggdrasil::processReply() -{ +void Yggdrasil::processReply() { changeState(STATE_WORKING); switch (m_netReply->error()) @@ -247,9 +237,9 @@ void Yggdrasil::processReply() "<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 %1 log file for details</li>" + "<li>Possibly something else. Check the log file for details</li>" "</ul>" - ).arg(BuildConfig.LAUNCHER_NAME) + ) ); return; // used for invalid credentials and similar errors. Fall through. @@ -278,19 +268,16 @@ void Yggdrasil::processReply() // Check the response code. int responseCode = m_netReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - if (responseCode == 200) - { + 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) - { + if (jsonError.error == QJsonParseError::NoError || replyData.size() == 0) { processResponse(replyData.size() > 0 ? doc.object() : QJsonObject()); return; } - else - { + else { changeState( STATE_FAILED_SOFT, tr("Failed to parse authentication server response JSON response: %1 at offset %2.").arg(jsonError.errorString()).arg(jsonError.offset) @@ -304,16 +291,14 @@ void Yggdrasil::processReply() // 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) - { + 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 - { + 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."; @@ -324,14 +309,12 @@ void Yggdrasil::processReply() } } -void Yggdrasil::processError(QJsonObject responseData) -{ +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()) - { + if (errorVal.isString() && errorMessageValue.isString()) { m_error = std::shared_ptr<Error>( new Error { errorVal.toString(""), @@ -341,8 +324,7 @@ void Yggdrasil::processError(QJsonObject responseData) ); changeState(STATE_FAILED_HARD, m_error->m_errorMessageVerbose); } - else - { + 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 index e709cb9f..b9670ec7 100644 --- a/launcher/minecraft/auth/flows/Yggdrasil.h +++ b/launcher/minecraft/auth/flows/Yggdrasil.h @@ -24,6 +24,7 @@ #include "../MinecraftAccount.h" +class QNetworkAccessManager; class QNetworkReply; /** @@ -33,7 +34,10 @@ class Yggdrasil : public AccountTask { Q_OBJECT public: - explicit Yggdrasil(AccountData * data, QObject *parent = 0); + explicit Yggdrasil( + AccountData *data, + QObject *parent = 0 + ); virtual ~Yggdrasil() {}; void refresh(); |