From 3a53349e332599221bc325f7fac9dc7927194bc2 Mon Sep 17 00:00:00 2001 From: Petr Mrázek Date: Mon, 26 Jul 2021 21:44:11 +0200 Subject: GH-3392 dirty initial MSA support that shares logic with Mojang flows Both act as the first step of AuthContext. --- launcher/minecraft/MinecraftInstance.cpp | 16 +- launcher/minecraft/auth-msa/BuildConfig.cpp.in | 9 - launcher/minecraft/auth-msa/BuildConfig.h | 11 - launcher/minecraft/auth-msa/CMakeLists.txt | 28 - launcher/minecraft/auth-msa/context.cpp | 938 --------------------- launcher/minecraft/auth-msa/context.h | 128 --- launcher/minecraft/auth-msa/main.cpp | 100 --- launcher/minecraft/auth-msa/mainwindow.cpp | 97 --- launcher/minecraft/auth-msa/mainwindow.h | 34 - launcher/minecraft/auth-msa/mainwindow.ui | 72 -- launcher/minecraft/auth/AccountData.cpp | 387 +++++++++ launcher/minecraft/auth/AccountData.h | 73 ++ launcher/minecraft/auth/AccountList.cpp | 519 ++++++++++++ launcher/minecraft/auth/AccountList.h | 118 +++ launcher/minecraft/auth/AccountTask.cpp | 69 ++ launcher/minecraft/auth/AccountTask.h | 103 +++ launcher/minecraft/auth/AuthSession.cpp | 2 + launcher/minecraft/auth/AuthSession.h | 13 +- launcher/minecraft/auth/MinecraftAccount.cpp | 303 +++++++ launcher/minecraft/auth/MinecraftAccount.h | 174 ++++ launcher/minecraft/auth/MojangAccount.cpp | 315 ------- launcher/minecraft/auth/MojangAccount.h | 180 ---- launcher/minecraft/auth/MojangAccountList.cpp | 468 ---------- launcher/minecraft/auth/MojangAccountList.h | 199 ----- launcher/minecraft/auth/YggdrasilTask.cpp | 255 ------ launcher/minecraft/auth/YggdrasilTask.h | 151 ---- launcher/minecraft/auth/flows/AuthContext.cpp | 752 +++++++++++++++++ launcher/minecraft/auth/flows/AuthContext.h | 94 +++ launcher/minecraft/auth/flows/AuthenticateTask.cpp | 202 ----- launcher/minecraft/auth/flows/AuthenticateTask.h | 46 - launcher/minecraft/auth/flows/MSAHelper.txt | 51 ++ launcher/minecraft/auth/flows/MSAInteractive.cpp | 20 + launcher/minecraft/auth/flows/MSAInteractive.h | 10 + launcher/minecraft/auth/flows/MSASilent.cpp | 16 + launcher/minecraft/auth/flows/MSASilent.h | 10 + launcher/minecraft/auth/flows/MojangLogin.cpp | 14 + launcher/minecraft/auth/flows/MojangLogin.h | 13 + launcher/minecraft/auth/flows/MojangRefresh.cpp | 14 + launcher/minecraft/auth/flows/MojangRefresh.h | 10 + launcher/minecraft/auth/flows/RefreshTask.cpp | 144 ---- launcher/minecraft/auth/flows/RefreshTask.h | 44 - launcher/minecraft/auth/flows/ValidateTask.cpp | 61 -- launcher/minecraft/auth/flows/ValidateTask.h | 47 -- launcher/minecraft/auth/flows/Yggdrasil.cpp | 337 ++++++++ launcher/minecraft/auth/flows/Yggdrasil.h | 82 ++ launcher/minecraft/launch/ClaimAccount.h | 4 +- 46 files changed, 3180 insertions(+), 3553 deletions(-) delete mode 100644 launcher/minecraft/auth-msa/BuildConfig.cpp.in delete mode 100644 launcher/minecraft/auth-msa/BuildConfig.h delete mode 100644 launcher/minecraft/auth-msa/CMakeLists.txt delete mode 100644 launcher/minecraft/auth-msa/context.cpp delete mode 100644 launcher/minecraft/auth-msa/context.h delete mode 100644 launcher/minecraft/auth-msa/main.cpp delete mode 100644 launcher/minecraft/auth-msa/mainwindow.cpp delete mode 100644 launcher/minecraft/auth-msa/mainwindow.h delete mode 100644 launcher/minecraft/auth-msa/mainwindow.ui create mode 100644 launcher/minecraft/auth/AccountData.cpp create mode 100644 launcher/minecraft/auth/AccountData.h create mode 100644 launcher/minecraft/auth/AccountList.cpp create mode 100644 launcher/minecraft/auth/AccountList.h create mode 100644 launcher/minecraft/auth/AccountTask.cpp create mode 100644 launcher/minecraft/auth/AccountTask.h create mode 100644 launcher/minecraft/auth/MinecraftAccount.cpp create mode 100644 launcher/minecraft/auth/MinecraftAccount.h delete mode 100644 launcher/minecraft/auth/MojangAccount.cpp delete mode 100644 launcher/minecraft/auth/MojangAccount.h delete mode 100644 launcher/minecraft/auth/MojangAccountList.cpp delete mode 100644 launcher/minecraft/auth/MojangAccountList.h delete mode 100644 launcher/minecraft/auth/YggdrasilTask.cpp delete mode 100644 launcher/minecraft/auth/YggdrasilTask.h create mode 100644 launcher/minecraft/auth/flows/AuthContext.cpp create mode 100644 launcher/minecraft/auth/flows/AuthContext.h delete mode 100644 launcher/minecraft/auth/flows/AuthenticateTask.cpp delete mode 100644 launcher/minecraft/auth/flows/AuthenticateTask.h create mode 100644 launcher/minecraft/auth/flows/MSAHelper.txt create mode 100644 launcher/minecraft/auth/flows/MSAInteractive.cpp create mode 100644 launcher/minecraft/auth/flows/MSAInteractive.h create mode 100644 launcher/minecraft/auth/flows/MSASilent.cpp create mode 100644 launcher/minecraft/auth/flows/MSASilent.h create mode 100644 launcher/minecraft/auth/flows/MojangLogin.cpp create mode 100644 launcher/minecraft/auth/flows/MojangLogin.h create mode 100644 launcher/minecraft/auth/flows/MojangRefresh.cpp create mode 100644 launcher/minecraft/auth/flows/MojangRefresh.h delete mode 100644 launcher/minecraft/auth/flows/RefreshTask.cpp delete mode 100644 launcher/minecraft/auth/flows/RefreshTask.h delete mode 100644 launcher/minecraft/auth/flows/ValidateTask.cpp delete mode 100644 launcher/minecraft/auth/flows/ValidateTask.h create mode 100644 launcher/minecraft/auth/flows/Yggdrasil.cpp create mode 100644 launcher/minecraft/auth/flows/Yggdrasil.h (limited to 'launcher/minecraft') diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index dbf9f816..5f3c7244 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -423,7 +423,7 @@ QStringList MinecraftInstance::processMinecraftArgs( // yggdrasil! if(session) { - token_mapping["auth_username"] = session->username; + // token_mapping["auth_username"] = session->username; token_mapping["auth_session"] = session->session; token_mapping["auth_access_token"] = session->access_token; token_mapping["auth_player_name"] = session->player_name; @@ -691,19 +691,11 @@ QMap MinecraftInstance::createCensorFilterFromSession(AuthSess addToFilter(sessionRef.session, tr("")); } addToFilter(sessionRef.access_token, tr("")); - addToFilter(sessionRef.client_token, tr("")); + if(sessionRef.client_token.size()) { + addToFilter(sessionRef.client_token, tr("")); + } addToFilter(sessionRef.uuid, tr("")); - auto i = sessionRef.u.properties.begin(); - while (i != sessionRef.u.properties.end()) - { - if(i.value().length() <= 3) { - ++i; - continue; - } - addToFilter(i.value(), "<" + i.key().toUpper() + ">"); - ++i; - } return filter; } diff --git a/launcher/minecraft/auth-msa/BuildConfig.cpp.in b/launcher/minecraft/auth-msa/BuildConfig.cpp.in deleted file mode 100644 index 8f470e25..00000000 --- a/launcher/minecraft/auth-msa/BuildConfig.cpp.in +++ /dev/null @@ -1,9 +0,0 @@ -#include "BuildConfig.h" -#include - -const Config BuildConfig; - -Config::Config() -{ - CLIENT_ID = "@MOJANGDEMO_CLIENT_ID@"; -} diff --git a/launcher/minecraft/auth-msa/BuildConfig.h b/launcher/minecraft/auth-msa/BuildConfig.h deleted file mode 100644 index 7a01d704..00000000 --- a/launcher/minecraft/auth-msa/BuildConfig.h +++ /dev/null @@ -1,11 +0,0 @@ -#pragma once -#include - -class Config -{ -public: - Config(); - QString CLIENT_ID; -}; - -extern const Config BuildConfig; diff --git a/launcher/minecraft/auth-msa/CMakeLists.txt b/launcher/minecraft/auth-msa/CMakeLists.txt deleted file mode 100644 index 22777d1b..00000000 --- a/launcher/minecraft/auth-msa/CMakeLists.txt +++ /dev/null @@ -1,28 +0,0 @@ -find_package(Qt5 COMPONENTS Core Gui Network Widgets REQUIRED) - -set(CMAKE_AUTOMOC ON) -set(CMAKE_AUTOUIC ON) -set(CMAKE_INCLUDE_CURRENT_DIR ON) -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall") - - -set(MOJANGDEMO_CLIENT_ID "" CACHE STRING "Client ID used for OAuth2 in mojangdemo") - -configure_file("${CMAKE_CURRENT_SOURCE_DIR}/BuildConfig.cpp.in" "${CMAKE_CURRENT_BINARY_DIR}/BuildConfig.cpp") - -set(mojang_SRCS - main.cpp - context.cpp - context.h - - mainwindow.cpp - mainwindow.h - mainwindow.ui - - ${CMAKE_CURRENT_BINARY_DIR}/BuildConfig.cpp - BuildConfig.h -) - -add_executable( mojangdemo ${mojang_SRCS} ) -target_link_libraries( mojangdemo Katabasis Qt5::Gui Qt5::Widgets ) -target_include_directories(mojangdemo PRIVATE logic) diff --git a/launcher/minecraft/auth-msa/context.cpp b/launcher/minecraft/auth-msa/context.cpp deleted file mode 100644 index d7ecda30..00000000 --- a/launcher/minecraft/auth-msa/context.cpp +++ /dev/null @@ -1,938 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include - -#include -#include - -#include "context.h" -#include "katabasis/Globals.h" -#include "katabasis/StoreQSettings.h" -#include "katabasis/Requestor.h" -#include "BuildConfig.h" - -using OAuth2 = Katabasis::OAuth2; -using Requestor = Katabasis::Requestor; -using Activity = Katabasis::Activity; - -Context::Context(QObject *parent) : - QObject(parent) -{ - mgr = new QNetworkAccessManager(this); - - Katabasis::OAuth2::Options opts; - opts.scope = "XboxLive.signin offline_access"; - opts.clientIdentifier = BuildConfig.CLIENT_ID; - opts.authorizationUrl = "https://login.live.com/oauth20_authorize.srf"; - opts.accessTokenUrl = "https://login.live.com/oauth20_token.srf"; - opts.listenerPorts = {28562, 28563, 28564, 28565, 28566}; - - oauth2 = new OAuth2(opts, m_account.msaToken, this, mgr); - - connect(oauth2, &OAuth2::linkingFailed, this, &Context::onLinkingFailed); - connect(oauth2, &OAuth2::linkingSucceeded, this, &Context::onLinkingSucceeded); - connect(oauth2, &OAuth2::openBrowser, this, &Context::onOpenBrowser); - connect(oauth2, &OAuth2::closeBrowser, this, &Context::onCloseBrowser); - connect(oauth2, &OAuth2::activityChanged, this, &Context::onOAuthActivityChanged); -} - -void Context::beginActivity(Activity activity) { - if(isBusy()) { - throw 0; - } - activity_ = activity; - emit activityChanged(activity_); -} - -void Context::finishActivity() { - if(!isBusy()) { - throw 0; - } - activity_ = Katabasis::Activity::Idle; - m_account.validity_ = m_account.minecraftProfile.validity; - emit activityChanged(activity_); -} - -QString Context::gameToken() { - return m_account.minecraftToken.token; -} - -QString Context::userId() { - return m_account.minecraftProfile.id; -} - -QString Context::userName() { - return m_account.minecraftProfile.name; -} - -bool Context::silentSignIn() { - if(isBusy()) { - return false; - } - beginActivity(Activity::Refreshing); - if(!oauth2->refresh()) { - finishActivity(); - return false; - } - - requestsDone = 0; - xboxProfileSucceeded = false; - mcAuthSucceeded = false; - - return true; -} - -bool Context::signIn() { - if(isBusy()) { - return false; - } - - requestsDone = 0; - xboxProfileSucceeded = false; - mcAuthSucceeded = false; - - beginActivity(Activity::LoggingIn); - oauth2->unlink(); - m_account = AccountData(); - oauth2->link(); - return true; -} - -bool Context::signOut() { - if(isBusy()) { - return false; - } - beginActivity(Activity::LoggingOut); - oauth2->unlink(); - m_account = AccountData(); - finishActivity(); - return true; -} - - -void Context::onOpenBrowser(const QUrl &url) { - QDesktopServices::openUrl(url); -} - -void Context::onCloseBrowser() { - -} - -void Context::onLinkingFailed() { - finishActivity(); -} - -void Context::onLinkingSucceeded() { - auto *o2t = qobject_cast(sender()); - if (!o2t->linked()) { - finishActivity(); - return; - } - QVariantMap extraTokens = o2t->extraTokens(); - if (!extraTokens.isEmpty()) { - qDebug() << "Extra tokens in response:"; - foreach (QString key, extraTokens.keys()) { - qDebug() << "\t" << key << ":" << extraTokens.value(key); - } - } - doUserAuth(); -} - -void Context::onOAuthActivityChanged(Katabasis::Activity activity) { - // respond to activity change here -} - -void Context::doUserAuth() { - 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_account.msaToken.token); - - QNetworkRequest request = QNetworkRequest(QUrl("https://user.auth.xboxlive.com/user/authenticate")); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - request.setRawHeader("Accept", "application/json"); - auto *requestor = new Katabasis::Requestor(mgr, oauth2, this); - requestor->setAddAccessTokenInQuery(false); - - connect(requestor, &Requestor::finished, this, &Context::onUserAuthDone); - requestor->post(request, xbox_auth_data.toUtf8()); - 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::ISODateWithMs); - 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; -} - -/* -{ - "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) { - QJsonParseError jsonError; - QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); - if(jsonError.error) { - qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString(); - qDebug() << data; - return false; - } - - auto obj = doc.object(); - if(!getDateTime(obj.value("IssueInstant"), output.issueInstant)) { - qWarning() << "User IssueInstant is not a timestamp"; - qDebug() << data; - return false; - } - if(!getDateTime(obj.value("NotAfter"), output.notAfter)) { - qWarning() << "User NotAfter is not a timestamp"; - qDebug() << data; - return false; - } - if(!getString(obj.value("Token"), output.token)) { - qWarning() << "User Token is not a timestamp"; - qDebug() << data; - return false; - } - auto arrayVal = obj.value("DisplayClaims").toObject().value("xui"); - if(!arrayVal.isArray()) { - qWarning() << "Missing xui claims array"; - qDebug() << data; - return false; - } - bool foundUHS = false; - 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..."; - qDebug() << data; - return false; - } - output.extra[iter.key()] = claim; - } - - break; - } - if(!foundUHS) { - qWarning() << "Missing uhs"; - qDebug() << data; - return false; - } - output.validity = Katabasis::Validity::Certain; - qDebug() << data; - return true; -} - -} - -void Context::onUserAuthDone( - int requestId, - QNetworkReply::NetworkError error, - QByteArray replyData, - QList headers -) { - if (error != QNetworkReply::NoError) { - qWarning() << "Reply error:" << error; - finishActivity(); - return; - } - - Katabasis::Token temp; - if(!parseXTokenResponse(replyData, temp)) { - qWarning() << "Could not parse user authentication response..."; - finishActivity(); - return; - } - m_account.userToken = temp; - - 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 Context::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_account.userToken.token); - - QNetworkRequest request = QNetworkRequest(QUrl("https://xsts.auth.xboxlive.com/xsts/authorize")); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - request.setRawHeader("Accept", "application/json"); - Requestor *requestor = new Requestor(mgr, oauth2, this); - requestor->setAddAccessTokenInQuery(false); - - connect(requestor, &Requestor::finished, this, &Context::onSTSAuthMinecraftDone); - requestor->post(request, xbox_auth_data.toUtf8()); - qDebug() << "Second layer of XBox auth ... commencing."; -} - -void Context::onSTSAuthMinecraftDone( - int requestId, - QNetworkReply::NetworkError error, - QByteArray replyData, - QList headers -) { - if (error != QNetworkReply::NoError) { - qWarning() << "Reply error:" << error; - finishActivity(); - return; - } - - Katabasis::Token temp; - if(!parseXTokenResponse(replyData, temp)) { - qWarning() << "Could not parse authorization response for access to mojang services..."; - finishActivity(); - return; - } - - if(temp.extra["uhs"] != m_account.userToken.extra["uhs"]) { - qWarning() << "Server has changed user hash in the reply... something is wrong. ABORTING"; - qDebug() << replyData; - finishActivity(); - return; - } - m_account.mojangservicesToken = temp; - - doMinecraftAuth(); -} - -void Context::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_account.userToken.token); - - QNetworkRequest request = QNetworkRequest(QUrl("https://xsts.auth.xboxlive.com/xsts/authorize")); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - request.setRawHeader("Accept", "application/json"); - Requestor *requestor = new Requestor(mgr, oauth2, this); - requestor->setAddAccessTokenInQuery(false); - - connect(requestor, &Requestor::finished, this, &Context::onSTSAuthGenericDone); - requestor->post(request, xbox_auth_data.toUtf8()); - qDebug() << "Second layer of XBox auth ... commencing."; -} - -void Context::onSTSAuthGenericDone( - int requestId, - QNetworkReply::NetworkError error, - QByteArray replyData, - QList headers -) { - if (error != QNetworkReply::NoError) { - qWarning() << "Reply error:" << error; - finishActivity(); - return; - } - - Katabasis::Token temp; - if(!parseXTokenResponse(replyData, temp)) { - qWarning() << "Could not parse authorization response for access to xbox API..."; - finishActivity(); - return; - } - - if(temp.extra["uhs"] != m_account.userToken.extra["uhs"]) { - qWarning() << "Server has changed user hash in the reply... something is wrong. ABORTING"; - qDebug() << replyData; - finishActivity(); - return; - } - m_account.xboxApiToken = temp; - - doXBoxProfile(); -} - - -void Context::doMinecraftAuth() { - QString mc_auth_template = R"XXX( -{ - "identityToken": "XBL3.0 x=%1;%2" -} -)XXX"; - auto data = mc_auth_template.arg(m_account.mojangservicesToken.extra["uhs"].toString(), m_account.mojangservicesToken.token); - - QNetworkRequest request = QNetworkRequest(QUrl("https://api.minecraftservices.com/authentication/login_with_xbox")); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - request.setRawHeader("Accept", "application/json"); - Requestor *requestor = new Requestor(mgr, oauth2, this); - requestor->setAddAccessTokenInQuery(false); - - connect(requestor, &Requestor::finished, this, &Context::onMinecraftAuthDone); - requestor->post(request, data.toUtf8()); - qDebug() << "Getting Minecraft access token..."; -} - -namespace { -bool parseMojangResponse(QByteArray & data, Katabasis::Token &output) { - QJsonParseError jsonError; - QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); - if(jsonError.error) { - qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString(); - qDebug() << data; - return false; - } - - auto obj = doc.object(); - double expires_in = 0; - if(!getNumber(obj.value("expires_in"), expires_in)) { - qWarning() << "expires_in is not a valid number"; - qDebug() << data; - return false; - } - auto currentTime = QDateTime::currentDateTimeUtc(); - output.issueInstant = currentTime; - output.notAfter = currentTime.addSecs(expires_in); - - QString username; - if(!getString(obj.value("username"), username)) { - qWarning() << "username is not valid"; - qDebug() << data; - return false; - } - - // TODO: it's a JWT... validate it? - if(!getString(obj.value("access_token"), output.token)) { - qWarning() << "access_token is not valid"; - qDebug() << data; - return false; - } - output.validity = Katabasis::Validity::Certain; - qDebug() << data; - return true; -} -} - -void Context::onMinecraftAuthDone( - int requestId, - QNetworkReply::NetworkError error, - QByteArray replyData, - QList headers -) { - requestsDone++; - - if (error != QNetworkReply::NoError) { - qWarning() << "Reply error:" << error; - qDebug() << replyData; - finishActivity(); - return; - } - - if(!parseMojangResponse(replyData, m_account.minecraftToken)) { - qWarning() << "Could not parse login_with_xbox response..."; - qDebug() << replyData; - finishActivity(); - return; - } - mcAuthSucceeded = true; - - checkResult(); -} - -void Context::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_account.userToken.extra["uhs"].toString(), m_account.xboxApiToken.token).toUtf8()); - Requestor *requestor = new Requestor(mgr, oauth2, this); - requestor->setAddAccessTokenInQuery(false); - - connect(requestor, &Requestor::finished, this, &Context::onXBoxProfileDone); - requestor->get(request); - qDebug() << "Getting Xbox profile..."; -} - -void Context::onXBoxProfileDone( - int requestId, - QNetworkReply::NetworkError error, - QByteArray replyData, - QList headers -) { - requestsDone ++; - - if (error != QNetworkReply::NoError) { - qWarning() << "Reply error:" << error; - qDebug() << replyData; - finishActivity(); - return; - } - - qDebug() << "XBox profile: " << replyData; - - xboxProfileSucceeded = true; - checkResult(); -} - -void Context::checkResult() { - if(requestsDone != 2) { - return; - } - if(mcAuthSucceeded && xboxProfileSucceeded) { - doMinecraftProfile(); - } - else { - finishActivity(); - } -} - -namespace { -bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) { - QJsonParseError jsonError; - QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); - if(jsonError.error) { - qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString(); - qDebug() << data; - return false; - } - - auto obj = doc.object(); - if(!getString(obj.value("id"), output.id)) { - qWarning() << "minecraft profile id is not a string"; - qDebug() << data; - return false; - } - - if(!getString(obj.value("name"), output.name)) { - qWarning() << "minecraft profile name is not a string"; - qDebug() << data; - 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(); - int i = -1; - int currentCape = -1; - for(auto cape: capesArray) { - i++; - 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 = i; - } - if(!getString(capeObj.value("url"), capeOut.url)) { - continue; - } - if(!getString(capeObj.value("alias"), capeOut.alias)) { - continue; - } - - // we deal with only the active skin - output.capes.push_back(capeOut); - } - output.currentCape = currentCape; - output.validity = Katabasis::Validity::Certain; - return true; -} -} - -void Context::doMinecraftProfile() { - auto url = QUrl("https://api.minecraftservices.com/minecraft/profile"); - QNetworkRequest request = QNetworkRequest(url); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - // request.setRawHeader("Accept", "application/json"); - request.setRawHeader("Authorization", QString("Bearer %1").arg(m_account.minecraftToken.token).toUtf8()); - - Requestor *requestor = new Requestor(mgr, oauth2, this); - requestor->setAddAccessTokenInQuery(false); - - connect(requestor, &Requestor::finished, this, &Context::onMinecraftProfileDone); - requestor->get(request); -} - -void Context::onMinecraftProfileDone(int, QNetworkReply::NetworkError error, QByteArray data, QList headers) { - qDebug() << data; - if (error == QNetworkReply::ContentNotFoundError) { - m_account.minecraftProfile = MinecraftProfile(); - finishActivity(); - return; - } - if (error != QNetworkReply::NoError) { - finishActivity(); - return; - } - if(!parseMinecraftProfile(data, m_account.minecraftProfile)) { - m_account.minecraftProfile = MinecraftProfile(); - finishActivity(); - return; - } - doGetSkin(); -} - -void Context::doGetSkin() { - auto url = QUrl(m_account.minecraftProfile.skin.url); - QNetworkRequest request = QNetworkRequest(url); - Requestor *requestor = new Requestor(mgr, oauth2, this); - requestor->setAddAccessTokenInQuery(false); - connect(requestor, &Requestor::finished, this, &Context::onSkinDone); - requestor->get(request); -} - -void Context::onSkinDone(int, QNetworkReply::NetworkError error, QByteArray data, QList) { - if (error == QNetworkReply::NoError) { - m_account.minecraftProfile.skin.data = data; - } - finishActivity(); -} - -namespace { -void tokenToJSON(QJsonObject &parent, Katabasis::Token t, const char * tokenName) { - if(t.validity == Katabasis::Validity::None || !t.persistent) { - return; - } - QJsonObject out; - if(t.issueInstant.isValid()) { - out["iat"] = QJsonValue(t.issueInstant.toSecsSinceEpoch()); - } - - if(t.notAfter.isValid()) { - out["exp"] = QJsonValue(t.notAfter.toSecsSinceEpoch()); - } - - if(!t.token.isEmpty()) { - out["token"] = QJsonValue(t.token); - } - if(!t.refresh_token.isEmpty()) { - out["refresh_token"] = QJsonValue(t.refresh_token); - } - if(t.extra.size()) { - out["extra"] = QJsonObject::fromVariantMap(t.extra); - } - if(out.size()) { - parent[tokenName] = out; - } -} - -Katabasis::Token tokenFromJSON(const QJsonObject &parent, const char * tokenName) { - Katabasis::Token out; - auto tokenObject = parent.value(tokenName).toObject(); - if(tokenObject.isEmpty()) { - return out; - } - auto issueInstant = tokenObject.value("iat"); - if(issueInstant.isDouble()) { - out.issueInstant = QDateTime::fromSecsSinceEpoch((int64_t) issueInstant.toDouble()); - } - - auto notAfter = tokenObject.value("exp"); - if(notAfter.isDouble()) { - out.notAfter = QDateTime::fromSecsSinceEpoch((int64_t) notAfter.toDouble()); - } - - auto token = tokenObject.value("token"); - if(token.isString()) { - out.token = token.toString(); - out.validity = Katabasis::Validity::Assumed; - } - - auto refresh_token = tokenObject.value("refresh_token"); - if(refresh_token.isString()) { - out.refresh_token = refresh_token.toString(); - } - - auto extra = tokenObject.value("extra"); - if(extra.isObject()) { - out.extra = extra.toObject().toVariantMap(); - } - return out; -} - -void profileToJSON(QJsonObject &parent, MinecraftProfile p, const char * tokenName) { - if(p.id.isEmpty()) { - return; - } - QJsonObject out; - out["id"] = QJsonValue(p.id); - out["name"] = QJsonValue(p.name); - if(p.currentCape != -1) { - out["cape"] = p.capes[p.currentCape].id; - } - - { - QJsonObject skinObj; - skinObj["id"] = p.skin.id; - skinObj["url"] = p.skin.url; - skinObj["variant"] = p.skin.variant; - if(p.skin.data.size()) { - skinObj["data"] = QString::fromLatin1(p.skin.data.toBase64()); - } - out["skin"] = skinObj; - } - - QJsonArray capesArray; - for(auto & cape: p.capes) { - QJsonObject capeObj; - capeObj["id"] = cape.id; - capeObj["url"] = cape.url; - capeObj["alias"] = cape.alias; - if(cape.data.size()) { - capeObj["data"] = QString::fromLatin1(cape.data.toBase64()); - } - capesArray.push_back(capeObj); - } - out["capes"] = capesArray; - parent[tokenName] = out; -} - -MinecraftProfile profileFromJSON(const QJsonObject &parent, const char * tokenName) { - MinecraftProfile out; - auto tokenObject = parent.value(tokenName).toObject(); - if(tokenObject.isEmpty()) { - return out; - } - { - auto idV = tokenObject.value("id"); - auto nameV = tokenObject.value("name"); - if(!idV.isString() || !nameV.isString()) { - qWarning() << "mandatory profile attributes are missing or of unexpected type"; - return MinecraftProfile(); - } - out.name = nameV.toString(); - out.id = idV.toString(); - } - - { - auto skinV = tokenObject.value("skin"); - if(!skinV.isObject()) { - qWarning() << "skin is missing"; - return MinecraftProfile(); - } - auto skinObj = skinV.toObject(); - auto idV = skinObj.value("id"); - auto urlV = skinObj.value("url"); - auto variantV = skinObj.value("variant"); - if(!idV.isString() || !urlV.isString() || !variantV.isString()) { - qWarning() << "mandatory skin attributes are missing or of unexpected type"; - return MinecraftProfile(); - } - out.skin.id = idV.toString(); - out.skin.url = urlV.toString(); - out.skin.variant = variantV.toString(); - - // data for skin is optional - auto dataV = skinObj.value("data"); - if(dataV.isString()) { - // TODO: validate base64 - out.skin.data = QByteArray::fromBase64(dataV.toString().toLatin1()); - } - else if (!dataV.isUndefined()) { - qWarning() << "skin data is something unexpected"; - return MinecraftProfile(); - } - } - - auto capesV = tokenObject.value("capes"); - if(!capesV.isArray()) { - qWarning() << "capes is not an array!"; - return MinecraftProfile(); - } - auto capesArray = capesV.toArray(); - for(auto capeV: capesArray) { - if(!capeV.isObject()) { - qWarning() << "cape is not an object!"; - return MinecraftProfile(); - } - auto capeObj = capeV.toObject(); - auto idV = capeObj.value("id"); - auto urlV = capeObj.value("url"); - auto aliasV = capeObj.value("alias"); - if(!idV.isString() || !urlV.isString() || !aliasV.isString()) { - qWarning() << "mandatory skin attributes are missing or of unexpected type"; - return MinecraftProfile(); - } - Cape cape; - cape.id = idV.toString(); - cape.url = urlV.toString(); - cape.alias = aliasV.toString(); - - // data for cape is optional. - auto dataV = capeObj.value("data"); - if(dataV.isString()) { - // TODO: validate base64 - cape.data = QByteArray::fromBase64(dataV.toString().toLatin1()); - } - else if (!dataV.isUndefined()) { - qWarning() << "cape data is something unexpected"; - return MinecraftProfile(); - } - out.capes.push_back(cape); - } - out.validity = Katabasis::Validity::Assumed; - return out; -} - -} - -bool Context::resumeFromState(QByteArray data) { - QJsonParseError error; - auto doc = QJsonDocument::fromJson(data, &error); - if(error.error != QJsonParseError::NoError) { - qWarning() << "Failed to parse account data as JSON."; - return false; - } - auto docObject = doc.object(); - m_account.msaToken = tokenFromJSON(docObject, "msa"); - m_account.userToken = tokenFromJSON(docObject, "utoken"); - m_account.xboxApiToken = tokenFromJSON(docObject, "xrp-main"); - m_account.mojangservicesToken = tokenFromJSON(docObject, "xrp-mc"); - m_account.minecraftToken = tokenFromJSON(docObject, "ygg"); - - m_account.minecraftProfile = profileFromJSON(docObject, "profile"); - - m_account.validity_ = m_account.minecraftProfile.validity; - - return true; -} - -QByteArray Context::saveState() { - QJsonDocument doc; - QJsonObject output; - tokenToJSON(output, m_account.msaToken, "msa"); - tokenToJSON(output, m_account.userToken, "utoken"); - tokenToJSON(output, m_account.xboxApiToken, "xrp-main"); - tokenToJSON(output, m_account.mojangservicesToken, "xrp-mc"); - tokenToJSON(output, m_account.minecraftToken, "ygg"); - profileToJSON(output, m_account.minecraftProfile, "profile"); - doc.setObject(output); - return doc.toJson(QJsonDocument::Indented); -} diff --git a/launcher/minecraft/auth-msa/context.h b/launcher/minecraft/auth-msa/context.h deleted file mode 100644 index f1ac99b8..00000000 --- a/launcher/minecraft/auth-msa/context.h +++ /dev/null @@ -1,128 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include - -struct Skin { - QString id; - QString url; - QString variant; - - QByteArray data; -}; - -struct Cape { - QString id; - QString url; - QString alias; - - QByteArray data; -}; - -struct MinecraftProfile { - QString id; - QString name; - Skin skin; - int currentCape = -1; - QVector capes; - Katabasis::Validity validity = Katabasis::Validity::None; -}; - -enum class AccountType { - MSA, - Mojang -}; - -struct AccountData { - AccountType type = AccountType::MSA; - - Katabasis::Token msaToken; - Katabasis::Token userToken; - Katabasis::Token xboxApiToken; - Katabasis::Token mojangservicesToken; - Katabasis::Token minecraftToken; - - MinecraftProfile minecraftProfile; - Katabasis::Validity validity_ = Katabasis::Validity::None; -}; - -class Context : public QObject -{ - Q_OBJECT - -public: - explicit Context(QObject *parent = 0); - - QByteArray saveState(); - bool resumeFromState(QByteArray data); - - bool isBusy() { - return activity_ != Katabasis::Activity::Idle; - }; - Katabasis::Validity validity() { - return m_account.validity_; - }; - - bool signIn(); - bool silentSignIn(); - bool signOut(); - - QString userName(); - QString userId(); - QString gameToken(); -signals: - void succeeded(); - void failed(); - void activityChanged(Katabasis::Activity activity); - -private slots: - void onLinkingSucceeded(); - void onLinkingFailed(); - void onOpenBrowser(const QUrl &url); - void onCloseBrowser(); - void onOAuthActivityChanged(Katabasis::Activity activity); - -private: - void doUserAuth(); - Q_SLOT void onUserAuthDone(int, QNetworkReply::NetworkError, QByteArray, QList); - - void doSTSAuthMinecraft(); - Q_SLOT void onSTSAuthMinecraftDone(int, QNetworkReply::NetworkError, QByteArray, QList); - void doMinecraftAuth(); - Q_SLOT void onMinecraftAuthDone(int, QNetworkReply::NetworkError, QByteArray, QList); - - void doSTSAuthGeneric(); - Q_SLOT void onSTSAuthGenericDone(int, QNetworkReply::NetworkError, QByteArray, QList); - void doXBoxProfile(); - Q_SLOT void onXBoxProfileDone(int, QNetworkReply::NetworkError, QByteArray, QList); - - void doMinecraftProfile(); - Q_SLOT void onMinecraftProfileDone(int, QNetworkReply::NetworkError, QByteArray, QList); - - void doGetSkin(); - Q_SLOT void onSkinDone(int, QNetworkReply::NetworkError, QByteArray, QList); - - void checkResult(); - -private: - void beginActivity(Katabasis::Activity activity); - void finishActivity(); - void clearTokens(); - -private: - Katabasis::OAuth2 *oauth2 = nullptr; - - int requestsDone = 0; - bool xboxProfileSucceeded = false; - bool mcAuthSucceeded = false; - Katabasis::Activity activity_ = Katabasis::Activity::Idle; - - AccountData m_account; - - QNetworkAccessManager *mgr = nullptr; -}; diff --git a/launcher/minecraft/auth-msa/main.cpp b/launcher/minecraft/auth-msa/main.cpp deleted file mode 100644 index 481e0126..00000000 --- a/launcher/minecraft/auth-msa/main.cpp +++ /dev/null @@ -1,100 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include "context.h" -#include "mainwindow.h" - -void myMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg) -{ - QByteArray localMsg = msg.toLocal8Bit(); - const char *file = context.file ? context.file : ""; - const char *function = context.function ? context.function : ""; - switch (type) { - case QtDebugMsg: - fprintf(stderr, "Debug: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function); - break; - case QtInfoMsg: - fprintf(stderr, "Info: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function); - break; - case QtWarningMsg: - fprintf(stderr, "Warning: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function); - break; - case QtCriticalMsg: - fprintf(stderr, "Critical: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function); - break; - case QtFatalMsg: - fprintf(stderr, "Fatal: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function); - break; - } -} - -class Helper : public QObject { - Q_OBJECT - -public: - Helper(Context * context) : QObject(), context_(context), msg_(QString()) { - QFile tokenCache("usercache.dat"); - if(tokenCache.open(QIODevice::ReadOnly)) { - context_->resumeFromState(tokenCache.readAll()); - } - } - -public slots: - void run() { - connect(context_, &Context::activityChanged, this, &Helper::onActivityChanged); - context_->silentSignIn(); - } - - void onFailed() { - qDebug() << "Login failed"; - } - - void onActivityChanged(Katabasis::Activity activity) { - if(activity == Katabasis::Activity::Idle) { - switch(context_->validity()) { - case Katabasis::Validity::None: { - // account is gone, remove it. - QFile::remove("usercache.dat"); - } - break; - case Katabasis::Validity::Assumed: { - // this is basically a soft-failed refresh. do nothing. - } - break; - case Katabasis::Validity::Certain: { - // stuff got refreshed / signed in. Save. - auto data = context_->saveState(); - QSaveFile tokenCache("usercache.dat"); - if(tokenCache.open(QIODevice::WriteOnly)) { - tokenCache.write(context_->saveState()); - tokenCache.commit(); - } - } - break; - } - } - } - -private: - Context *context_; - QString msg_; -}; - -int main(int argc, char *argv[]) { - qInstallMessageHandler(myMessageOutput); - QApplication a(argc, argv); - QCoreApplication::setOrganizationName("MultiMC"); - QCoreApplication::setApplicationName("MultiMC"); - Context c; - Helper helper(&c); - MainWindow window(&c); - window.show(); - QTimer::singleShot(0, &helper, &Helper::run); - return a.exec(); -} - -#include "main.moc" diff --git a/launcher/minecraft/auth-msa/mainwindow.cpp b/launcher/minecraft/auth-msa/mainwindow.cpp deleted file mode 100644 index d4e18dc0..00000000 --- a/launcher/minecraft/auth-msa/mainwindow.cpp +++ /dev/null @@ -1,97 +0,0 @@ -#include "mainwindow.h" -#include "ui_mainwindow.h" -#include - -#include - -#include "BuildConfig.h" - -MainWindow::MainWindow(Context * context, QWidget *parent) : - QMainWindow(parent), - m_context(context), - m_ui(new Ui::MainWindow) -{ - m_ui->setupUi(this); - connect(m_ui->signInButton_MSA, &QPushButton::clicked, this, &MainWindow::SignInMSAClicked); - connect(m_ui->signInButton_Mojang, &QPushButton::clicked, this, &MainWindow::SignInMojangClicked); - connect(m_ui->signOutButton, &QPushButton::clicked, this, &MainWindow::SignOutClicked); - connect(m_ui->refreshButton, &QPushButton::clicked, this, &MainWindow::RefreshClicked); - - // connect(m_context, &Context::linkingSucceeded, this, &MainWindow::SignInSucceeded); - // connect(m_context, &Context::linkingFailed, this, &MainWindow::SignInFailed); - connect(m_context, &Context::activityChanged, this, &MainWindow::ActivityChanged); - ActivityChanged(Katabasis::Activity::Idle); -} - -MainWindow::~MainWindow() = default; - -void MainWindow::ActivityChanged(Katabasis::Activity activity) { - switch(activity) { - case Katabasis::Activity::Idle: { - if(m_context->validity() != Katabasis::Validity::None) { - m_ui->signInButton_Mojang->setEnabled(false); - m_ui->signInButton_MSA->setEnabled(false); - m_ui->signOutButton->setEnabled(true); - m_ui->refreshButton->setEnabled(true); - m_ui->statusBar->showMessage(QString("Hello %1!").arg(m_context->userName())); - } - else { - m_ui->signInButton_Mojang->setEnabled(true); - m_ui->signInButton_MSA->setEnabled(true); - m_ui->signOutButton->setEnabled(false); - m_ui->refreshButton->setEnabled(false); - m_ui->statusBar->showMessage("Press the login button to start."); - } - } - break; - case Katabasis::Activity::LoggingIn: { - m_ui->signInButton_Mojang->setEnabled(false); - m_ui->signInButton_MSA->setEnabled(false); - m_ui->signOutButton->setEnabled(false); - m_ui->refreshButton->setEnabled(false); - m_ui->statusBar->showMessage("Logging in..."); - } - break; - case Katabasis::Activity::LoggingOut: { - m_ui->signInButton_Mojang->setEnabled(false); - m_ui->signInButton_MSA->setEnabled(false); - m_ui->signOutButton->setEnabled(false); - m_ui->refreshButton->setEnabled(false); - m_ui->statusBar->showMessage("Logging out..."); - } - break; - case Katabasis::Activity::Refreshing: { - m_ui->signInButton_Mojang->setEnabled(false); - m_ui->signInButton_MSA->setEnabled(false); - m_ui->signOutButton->setEnabled(false); - m_ui->refreshButton->setEnabled(false); - m_ui->statusBar->showMessage("Refreshing login..."); - } - break; - } -} - -void MainWindow::SignInMSAClicked() { - qDebug() << "Sign In MSA"; - // signIn({{"prompt", "select_account"}}) - // FIXME: wrong. very wrong. this should not be operating on the current context - m_context->signIn(); -} - -void MainWindow::SignInMojangClicked() { - qDebug() << "Sign In Mojang"; - // signIn({{"prompt", "select_account"}}) - // FIXME: wrong. very wrong. this should not be operating on the current context - m_context->signIn(); -} - - -void MainWindow::SignOutClicked() { - qDebug() << "Sign Out"; - m_context->signOut(); -} - -void MainWindow::RefreshClicked() { - qDebug() << "Refresh"; - m_context->silentSignIn(); -} diff --git a/launcher/minecraft/auth-msa/mainwindow.h b/launcher/minecraft/auth-msa/mainwindow.h deleted file mode 100644 index abde52d8..00000000 --- a/launcher/minecraft/auth-msa/mainwindow.h +++ /dev/null @@ -1,34 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "context.h" - -namespace Ui { -class MainWindow; -} - -class MainWindow : public QMainWindow { - Q_OBJECT - -public: - explicit MainWindow(Context * context, QWidget *parent = nullptr); - ~MainWindow() override; - -private slots: - void SignInMojangClicked(); - void SignInMSAClicked(); - - void SignOutClicked(); - void RefreshClicked(); - - void ActivityChanged(Katabasis::Activity activity); - -private: - Context* m_context; - QScopedPointer m_ui; -}; - diff --git a/launcher/minecraft/auth-msa/mainwindow.ui b/launcher/minecraft/auth-msa/mainwindow.ui deleted file mode 100644 index 32b34128..00000000 --- a/launcher/minecraft/auth-msa/mainwindow.ui +++ /dev/null @@ -1,72 +0,0 @@ - - - MainWindow - - - - 0 - 0 - 1037 - 511 - - - - SmartMapsClient - - - true - - - - - - - SignIn Mojang - - - - - - - Qt::Horizontal - - - - - - - - - - Refresh - - - - - - - SignIn MSA - - - - - - - SignOut - - - - - - - Make Active - - - - - - - - - - diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp new file mode 100644 index 00000000..77c73c1b --- /dev/null +++ b/launcher/minecraft/auth/AccountData.cpp @@ -0,0 +1,387 @@ +#include "AccountData.h" +#include +#include +#include +#include +#include + +namespace { +void tokenToJSONV3(QJsonObject &parent, Katabasis::Token t, const char * tokenName) { + if(!t.persistent) { + return; + } + QJsonObject out; + if(t.issueInstant.isValid()) { + out["iat"] = QJsonValue(t.issueInstant.toMSecsSinceEpoch() / 1000); + } + + if(t.notAfter.isValid()) { + out["exp"] = QJsonValue(t.notAfter.toMSecsSinceEpoch() / 1000); + } + + bool save = false; + if(!t.token.isEmpty()) { + out["token"] = QJsonValue(t.token); + save = true; + } + if(!t.refresh_token.isEmpty()) { + out["refresh_token"] = QJsonValue(t.refresh_token); + save = true; + } + if(t.extra.size()) { + out["extra"] = QJsonObject::fromVariantMap(t.extra); + save = true; + } + if(save) { + parent[tokenName] = out; + } +} + +Katabasis::Token tokenFromJSONV3(const QJsonObject &parent, const char * tokenName) { + Katabasis::Token out; + auto tokenObject = parent.value(tokenName).toObject(); + if(tokenObject.isEmpty()) { + return out; + } + auto issueInstant = tokenObject.value("iat"); + if(issueInstant.isDouble()) { + out.issueInstant = QDateTime::fromMSecsSinceEpoch(((int64_t) issueInstant.toDouble()) * 1000); + } + + auto notAfter = tokenObject.value("exp"); + if(notAfter.isDouble()) { + out.notAfter = QDateTime::fromMSecsSinceEpoch(((int64_t) notAfter.toDouble()) * 1000); + } + + auto token = tokenObject.value("token"); + if(token.isString()) { + out.token = token.toString(); + out.validity = Katabasis::Validity::Assumed; + } + + auto refresh_token = tokenObject.value("refresh_token"); + if(refresh_token.isString()) { + out.refresh_token = refresh_token.toString(); + } + + auto extra = tokenObject.value("extra"); + if(extra.isObject()) { + out.extra = extra.toObject().toVariantMap(); + } + return out; +} + +void profileToJSONV3(QJsonObject &parent, MinecraftProfile p, const char * tokenName) { + if(p.id.isEmpty()) { + return; + } + QJsonObject out; + out["id"] = QJsonValue(p.id); + out["name"] = QJsonValue(p.name); + if(p.currentCape != -1) { + out["cape"] = p.capes[p.currentCape].id; + } + + { + QJsonObject skinObj; + skinObj["id"] = p.skin.id; + skinObj["url"] = p.skin.url; + skinObj["variant"] = p.skin.variant; + if(p.skin.data.size()) { + skinObj["data"] = QString::fromLatin1(p.skin.data.toBase64()); + } + out["skin"] = skinObj; + } + + QJsonArray capesArray; + for(auto & cape: p.capes) { + QJsonObject capeObj; + capeObj["id"] = cape.id; + capeObj["url"] = cape.url; + capeObj["alias"] = cape.alias; + if(cape.data.size()) { + capeObj["data"] = QString::fromLatin1(cape.data.toBase64()); + } + capesArray.push_back(capeObj); + } + out["capes"] = capesArray; + parent[tokenName] = out; +} + +MinecraftProfile profileFromJSONV3(const QJsonObject &parent, const char * tokenName) { + MinecraftProfile out; + auto tokenObject = parent.value(tokenName).toObject(); + if(tokenObject.isEmpty()) { + return out; + } + { + auto idV = tokenObject.value("id"); + auto nameV = tokenObject.value("name"); + if(!idV.isString() || !nameV.isString()) { + qWarning() << "mandatory profile attributes are missing or of unexpected type"; + return MinecraftProfile(); + } + out.name = nameV.toString(); + out.id = idV.toString(); + } + + { + auto skinV = tokenObject.value("skin"); + if(!skinV.isObject()) { + qWarning() << "skin is missing"; + return MinecraftProfile(); + } + auto skinObj = skinV.toObject(); + auto idV = skinObj.value("id"); + auto urlV = skinObj.value("url"); + auto variantV = skinObj.value("variant"); + if(!idV.isString() || !urlV.isString() || !variantV.isString()) { + qWarning() << "mandatory skin attributes are missing or of unexpected type"; + return MinecraftProfile(); + } + out.skin.id = idV.toString(); + out.skin.url = urlV.toString(); + out.skin.variant = variantV.toString(); + + // data for skin is optional + auto dataV = skinObj.value("data"); + if(dataV.isString()) { + // TODO: validate base64 + out.skin.data = QByteArray::fromBase64(dataV.toString().toLatin1()); + } + else if (!dataV.isUndefined()) { + qWarning() << "skin data is something unexpected"; + return MinecraftProfile(); + } + } + + auto capesV = tokenObject.value("capes"); + if(!capesV.isArray()) { + qWarning() << "capes is not an array!"; + return MinecraftProfile(); + } + auto capesArray = capesV.toArray(); + for(auto capeV: capesArray) { + if(!capeV.isObject()) { + qWarning() << "cape is not an object!"; + return MinecraftProfile(); + } + auto capeObj = capeV.toObject(); + auto idV = capeObj.value("id"); + auto urlV = capeObj.value("url"); + auto aliasV = capeObj.value("alias"); + if(!idV.isString() || !urlV.isString() || !aliasV.isString()) { + qWarning() << "mandatory skin attributes are missing or of unexpected type"; + return MinecraftProfile(); + } + Cape cape; + cape.id = idV.toString(); + cape.url = urlV.toString(); + cape.alias = aliasV.toString(); + + // data for cape is optional. + auto dataV = capeObj.value("data"); + if(dataV.isString()) { + // TODO: validate base64 + cape.data = QByteArray::fromBase64(dataV.toString().toLatin1()); + } + else if (!dataV.isUndefined()) { + qWarning() << "cape data is something unexpected"; + return MinecraftProfile(); + } + out.capes.push_back(cape); + } + out.validity = Katabasis::Validity::Assumed; + return out; +} + +} + +bool AccountData::resumeStateFromV2(QJsonObject data) { + // The JSON object must at least have a username for it to be valid. + if (!data.value("username").isString()) + { + qCritical() << "Can't load Mojang account info from JSON object. Username field is missing or of the wrong type."; + return false; + } + + QString userName = data.value("username").toString(""); + QString clientToken = data.value("clientToken").toString(""); + QString accessToken = data.value("accessToken").toString(""); + + QJsonArray profileArray = data.value("profiles").toArray(); + if (profileArray.size() < 1) + { + qCritical() << "Can't load Mojang account with username \"" << userName << "\". No profiles found."; + return false; + } + + struct AccountProfile + { + QString id; + QString name; + bool legacy; + }; + + QList profiles; + int currentProfileIndex = 0; + int index = -1; + QString currentProfile = data.value("activeProfile").toString(""); + for (QJsonValue profileVal : profileArray) + { + index++; + QJsonObject profileObject = profileVal.toObject(); + QString id = profileObject.value("id").toString(""); + QString name = profileObject.value("name").toString(""); + bool legacy = profileObject.value("legacy").toBool(false); + if (id.isEmpty() || name.isEmpty()) + { + qWarning() << "Unable to load a profile" << name << "because it was missing an ID or a name."; + continue; + } + if(id == currentProfile) { + currentProfileIndex = index; + } + profiles.append({id, name, legacy}); + } + auto & profile = profiles[currentProfileIndex]; + + type = AccountType::Mojang; + legacy = profile.legacy; + + minecraftProfile.id = profile.id; + minecraftProfile.name = profile.name; + minecraftProfile.validity = Katabasis::Validity::Assumed; + + yggdrasilToken.token = accessToken; + yggdrasilToken.extra["clientToken"] = clientToken; + yggdrasilToken.extra["userName"] = userName; + yggdrasilToken.validity = Katabasis::Validity::Assumed; + + validity_ = minecraftProfile.validity; + return true; +} + +bool AccountData::resumeStateFromV3(QJsonObject data) { + auto typeV = data.value("type"); + if(!typeV.isString()) { + qWarning() << "Failed to parse account data: type is missing."; + return false; + } + auto typeS = typeV.toString(); + if(typeS == "MSA") { + type = AccountType::MSA; + } else if (typeS == "Mojang") { + type = AccountType::Mojang; + } else { + qWarning() << "Failed to parse account data: type is not recognized."; + return false; + } + + if(type == AccountType::Mojang) { + legacy = data.value("legacy").toBool(false); + canMigrateToMSA = data.value("canMigrateToMSA").toBool(false); + } + + if(type == AccountType::MSA) { + msaToken = tokenFromJSONV3(data, "msa"); + userToken = tokenFromJSONV3(data, "utoken"); + xboxApiToken = tokenFromJSONV3(data, "xrp-main"); + mojangservicesToken = tokenFromJSONV3(data, "xrp-mc"); + } + + yggdrasilToken = tokenFromJSONV3(data, "ygg"); + minecraftProfile = profileFromJSONV3(data, "profile"); + + validity_ = minecraftProfile.validity; + + return true; +} + +QJsonObject AccountData::saveState() const { + QJsonObject output; + if(type == AccountType::Mojang) { + output["type"] = "Mojang"; + if(legacy) { + output["legacy"] = true; + } + if(canMigrateToMSA) { + output["canMigrateToMSA"] = true; + } + } + else if (type == AccountType::MSA) { + output["type"] = "MSA"; + tokenToJSONV3(output, msaToken, "msa"); + tokenToJSONV3(output, userToken, "utoken"); + tokenToJSONV3(output, xboxApiToken, "xrp-main"); + tokenToJSONV3(output, mojangservicesToken, "xrp-mc"); + } + + tokenToJSONV3(output, yggdrasilToken, "ygg"); + profileToJSONV3(output, minecraftProfile, "profile"); + return output; +} + +QString AccountData::userName() const { + if(type != AccountType::Mojang) { + return QString(); + } + return yggdrasilToken.extra["userName"].toString(); +} + +QString AccountData::accessToken() const { + return yggdrasilToken.token; +} + +QString AccountData::clientToken() const { + if(type != AccountType::Mojang) { + return QString(); + } + return yggdrasilToken.extra["clientToken"].toString(); +} + +void AccountData::setClientToken(QString clientToken) { + if(type != AccountType::Mojang) { + return; + } + yggdrasilToken.extra["clientToken"] = clientToken; +} + +void AccountData::generateClientTokenIfMissing() { + if(yggdrasilToken.extra.contains("clientToken")) { + return; + } + invalidateClientToken(); +} + +void AccountData::invalidateClientToken() { + if(type != AccountType::Mojang) { + return; + } + yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegExp("[{-}]")); +} + +QString AccountData::profileId() const { + return minecraftProfile.id; +} + +QString AccountData::profileName() const { + return minecraftProfile.name; +} + +QString AccountData::accountDisplayString() const { + switch(type) { + case AccountType::Mojang: { + return userName(); + } + case AccountType::MSA: { + if(xboxApiToken.extra.contains("gtg")) { + return xboxApiToken.extra["gtg"].toString(); + } + return "Xbox profile missing"; + } + default: { + return "Invalid Account"; + } + } +} diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h new file mode 100644 index 00000000..b2d09cb0 --- /dev/null +++ b/launcher/minecraft/auth/AccountData.h @@ -0,0 +1,73 @@ +#pragma once +#include +#include +#include +#include +#include + +struct Skin { + QString id; + QString url; + QString variant; + + QByteArray data; +}; + +struct Cape { + QString id; + QString url; + QString alias; + + QByteArray data; +}; +