GH-4071 Heavily refactor and rearchitect account system

This makes the account system much more modular
and makes it treat errors as something recoverable,
unless they come directly from the MSA refresh token
becoming invalid.
This commit is contained in:
Petr Mrázek 2021-12-04 01:18:05 +01:00
parent ffcef673de
commit 3c46d8a412
68 changed files with 2105 additions and 1446 deletions

View File

@ -827,6 +827,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
qDebug() << "Loading accounts...";
m_accounts->setListFilePath("accounts.json", true);
m_accounts->loadList();
m_accounts->fillQueue();
qDebug() << "<> Accounts loaded.";
}

View File

@ -196,36 +196,52 @@ set(ICONS_SOURCES
# Support for Minecraft instances and launch
set(MINECRAFT_SOURCES
# Minecraft support
minecraft/auth/AccountData.h
minecraft/auth/AccountData.cpp
minecraft/auth/AccountTask.h
minecraft/auth/AccountTask.cpp
minecraft/auth/AuthSession.h
minecraft/auth/AuthSession.cpp
minecraft/auth/AccountList.h
minecraft/auth/AccountData.h
minecraft/auth/AccountList.cpp
minecraft/auth/MinecraftAccount.h
minecraft/auth/AccountList.h
minecraft/auth/AccountTask.cpp
minecraft/auth/AccountTask.h
minecraft/auth/AuthRequest.cpp
minecraft/auth/AuthRequest.h
minecraft/auth/AuthSession.cpp
minecraft/auth/AuthSession.h
minecraft/auth/AuthStep.cpp
minecraft/auth/AuthStep.h
minecraft/auth/MinecraftAccount.cpp
minecraft/auth/flows/AuthContext.h
minecraft/auth/flows/AuthContext.cpp
minecraft/auth/flows/AuthRequest.h
minecraft/auth/flows/AuthRequest.cpp
minecraft/auth/MinecraftAccount.h
minecraft/auth/Parsers.cpp
minecraft/auth/Parsers.h
minecraft/auth/Yggdrasil.cpp
minecraft/auth/Yggdrasil.h
minecraft/auth/flows/MSAInteractive.h
minecraft/auth/flows/MSAInteractive.cpp
minecraft/auth/flows/MSASilent.h
minecraft/auth/flows/MSASilent.cpp
minecraft/auth/flows/AuthFlow.cpp
minecraft/auth/flows/AuthFlow.h
minecraft/auth/flows/Mojang.cpp
minecraft/auth/flows/Mojang.h
minecraft/auth/flows/MSA.cpp
minecraft/auth/flows/MSA.h
minecraft/auth/flows/MojangLogin.h
minecraft/auth/flows/MojangLogin.cpp
minecraft/auth/flows/MojangRefresh.h
minecraft/auth/flows/MojangRefresh.cpp
minecraft/auth/flows/Yggdrasil.h
minecraft/auth/flows/Yggdrasil.cpp
minecraft/auth/flows/Parsers.h
minecraft/auth/flows/Parsers.cpp
minecraft/auth/steps/EntitlementsStep.cpp
minecraft/auth/steps/EntitlementsStep.h
minecraft/auth/steps/GetSkinStep.cpp
minecraft/auth/steps/GetSkinStep.h
minecraft/auth/steps/LauncherLoginStep.cpp
minecraft/auth/steps/LauncherLoginStep.h
minecraft/auth/steps/MigrationEligibilityStep.cpp
minecraft/auth/steps/MigrationEligibilityStep.h
minecraft/auth/steps/MinecraftProfileStep.cpp
minecraft/auth/steps/MinecraftProfileStep.h
minecraft/auth/steps/MSAStep.cpp
minecraft/auth/steps/MSAStep.h
minecraft/auth/steps/XboxAuthorizationStep.cpp
minecraft/auth/steps/XboxAuthorizationStep.h
minecraft/auth/steps/XboxProfileStep.cpp
minecraft/auth/steps/XboxProfileStep.h
minecraft/auth/steps/XboxUserStep.cpp
minecraft/auth/steps/XboxUserStep.h
minecraft/auth/steps/YggdrasilStep.cpp
minecraft/auth/steps/YggdrasilStep.h
minecraft/gameoptions/GameOptions.h
minecraft/gameoptions/GameOptions.cpp

View File

@ -35,6 +35,8 @@ void LaunchController::executeTask()
return;
}
JavaCommon::checkJVMArgs(m_instance->settings()->get("JvmArgs").toString(), m_parentWidget);
login();
}
@ -90,8 +92,6 @@ void LaunchController::decideAccount()
void LaunchController::login() {
JavaCommon::checkJVMArgs(m_instance->settings()->get("JvmArgs").toString(), m_parentWidget);
decideAccount();
// if no account is selected, we bail
@ -113,133 +113,10 @@ void LaunchController::login() {
{
m_session = std::make_shared<AuthSession>();
m_session->wants_online = m_online;
shared_qobject_ptr<AccountTask> task;
if(!password.isNull()) {
task = m_accountToUse->login(m_session, password);
}
else {
task = m_accountToUse->refresh(m_session);
}
if (task)
{
// We'll need to validate the access token to make sure the account
// is still logged in.
ProgressDialog progDialog(m_parentWidget);
if (m_online)
{
progDialog.setSkipButton(true, tr("Play Offline"));
}
progDialog.execWithTask(task.get());
if (!task->wasSuccessful())
{
auto failReasonNew = task->failReason();
if(failReasonNew == "Invalid token." || failReasonNew == "Invalid Signature")
{
// account->invalidateClientToken();
failReason = needLoginAgain;
}
else failReason = failReasonNew;
}
}
switch (m_session->status)
{
case AuthSession::Undetermined: {
qCritical() << "Received undetermined session status during login. Bye.";
tryagain = false;
emitFailed(tr("Received undetermined session status during login."));
return;
}
case AuthSession::RequiresPassword: {
// FIXME: this needs to understand MSA
EditAccountDialog passDialog(failReason, m_parentWidget, EditAccountDialog::PasswordField);
auto username = m_session->username;
auto chopN = [](QString toChop, int N) -> QString
{
if(toChop.size() > N)
{
auto left = toChop.left(N);
left += QString("\u25CF").repeated(toChop.size() - N);
return left;
}
return toChop;
};
m_accountToUse->fillSession(m_session);
if(username.contains('@'))
{
auto parts = username.split('@');
auto mailbox = chopN(parts[0],3);
QString domain = chopN(parts[1], 3);
username = mailbox + '@' + domain;
}
passDialog.setUsername(username);
if (passDialog.exec() == QDialog::Accepted)
{
password = passDialog.password();
}
else
{
tryagain = false;
emitFailed(tr("Received undetermined session status during login."));
}
break;
}
case AuthSession::RequiresProfileSetup: {
auto entitlement = m_accountToUse->accountData()->minecraftEntitlement;
QString errorString;
if(!entitlement.canPlayMinecraft) {
errorString = tr("The account does not own Minecraft. You need to purchase the game first to play it.");
QMessageBox::warning(
nullptr,
tr("Missing Minecraft profile"),
errorString,
QMessageBox::StandardButton::Ok,
QMessageBox::StandardButton::Ok
);
tryagain = false;
emitFailed(errorString);
return;
}
// Now handle setting up a profile name here...
ProfileSetupDialog dialog(m_accountToUse, m_parentWidget);
if (dialog.exec() == QDialog::Accepted)
{
tryagain = true;
continue;
}
else
{
tryagain = false;
emitFailed(tr("Received undetermined session status during login."));
return;
}
}
case AuthSession::RequiresOAuth: {
auto errorString = tr("Microsoft account has expired and needs to be logged into manually again.");
QMessageBox::warning(
m_parentWidget,
tr("Microsoft Account refresh failed"),
errorString,
QMessageBox::StandardButton::Ok,
QMessageBox::StandardButton::Ok
);
tryagain = false;
emitFailed(errorString);
return;
}
case AuthSession::GoneOrMigrated: {
auto errorString = tr("The account no longer exists on the servers. It may have been migrated, in which case please add the new account you migrated this one to.");
QMessageBox::warning(
m_parentWidget,
tr("Account gone"),
errorString,
QMessageBox::StandardButton::Ok,
QMessageBox::StandardButton::Ok
);
tryagain = false;
emitFailed(errorString);
return;
}
case AuthSession::PlayableOffline: {
switch(m_accountToUse->accountState()) {
case AccountState::Offline: {
// we ask the user for a player name
bool ok = false;
QString usedname = m_session->player_name;
@ -262,11 +139,90 @@ void LaunchController::login() {
}
m_session->MakeOffline(usedname);
// offline flavored game from here :3
// NOTE: fallthrough is intentional
}
case AuthSession::PlayableOnline:
{
launchInstance();
tryagain = false;
case AccountState::Online: {
if(m_accountToUse->ownsMinecraft() && !m_accountToUse->hasProfile()) {
auto entitlement = m_accountToUse->accountData()->minecraftEntitlement;
QString errorString;
if(!entitlement.canPlayMinecraft) {
errorString = tr("The account does not own Minecraft. You need to purchase the game first to play it.");
QMessageBox::warning(
nullptr,
tr("Missing Minecraft profile"),
errorString,
QMessageBox::StandardButton::Ok,
QMessageBox::StandardButton::Ok
);
emitFailed(errorString);
return;
}
// Now handle setting up a profile name here...
ProfileSetupDialog dialog(m_accountToUse, m_parentWidget);
if (dialog.exec() == QDialog::Accepted)
{
tryagain = true;
continue;
}
else
{
emitFailed(tr("Received undetermined session status during login."));
return;
}
}
else {
launchInstance();
}
return;
}
case AccountState::Unchecked: {
m_accountToUse->refresh();
// NOTE: fallthrough intentional
}
case AccountState::Working: {
// refresh is in progress, we need to wait for it to finish to proceed.
ProgressDialog progDialog(m_parentWidget);
if (m_online)
{
progDialog.setSkipButton(true, tr("Play Offline"));
}
auto task = m_accountToUse->currentTask();
progDialog.execWithTask(task.get());
continue;
}
// FIXME: this is missing - the meaning is that the account is queued for refresh and we should wait for that
/*
case AccountState::Queued: {
return;
}
*/
case AccountState::Errored: {
// This means some sort of soft error that we can fix with a refresh ... so let's refresh.
// TODO: implement
return;
}
case AccountState::Expired: {
auto errorString = tr("The account has expired and needs to be logged into manually again.");
QMessageBox::warning(
m_parentWidget,
tr("Account refresh failed"),
errorString,
QMessageBox::StandardButton::Ok,
QMessageBox::StandardButton::Ok
);
emitFailed(errorString);
return;
}
case AccountState::Gone: {
auto errorString = tr("The account no longer exists on the servers. It may have been migrated, in which case please add the new account you migrated this one to.");
QMessageBox::warning(
m_parentWidget,
tr("Account gone"),
errorString,
QMessageBox::StandardButton::Ok,
QMessageBox::StandardButton::Ok
);
emitFailed(errorString);
return;
}
}
@ -334,14 +290,7 @@ void LaunchController::launchInstance()
online_mode = "offline";
}
QString auth_server_status;
if(m_session->auth_server_online) {
auth_server_status = "online";
} else {
auth_server_status = "offline";
}
m_launcher->prependStep(new TextPrint(m_launcher.get(), "Launched instance in " + online_mode + " mode\nAuthentication server is " + auth_server_status + "\n", MessageLevel::Launcher));
m_launcher->prependStep(new TextPrint(m_launcher.get(), "Launched instance in " + online_mode + " mode\n", MessageLevel::Launcher));
// Prepend Version
m_launcher->prependStep(new TextPrint(m_launcher.get(), BuildConfig.LAUNCHER_NAME + " version: " + BuildConfig.printableVersionString() + "\n\n", MessageLevel::Launcher));

View File

@ -41,6 +41,16 @@ enum class AccountType {
Mojang
};
enum class AccountState {
Unchecked,
Offline,
Working,
Online,
Errored,
Expired,
Gone
};
struct AccountData {
QJsonObject saveState() const;
bool resumeStateFromV2(QJsonObject data);
@ -77,4 +87,9 @@ struct AccountData {
MinecraftProfile minecraftProfile;
MinecraftEntitlement minecraftEntitlement;
Katabasis::Validity validity_ = Katabasis::Validity::None;
// runtime only information (not saved with the account)
QString internalId;
QString errorString;
AccountState accountState = AccountState::Unchecked;
};

View File

@ -15,6 +15,7 @@
#include "AccountList.h"
#include "AccountData.h"
#include "AccountTask.h"
#include <QIODevice>
#include <QFile>
@ -24,6 +25,7 @@
#include <QJsonObject>
#include <QJsonParseError>
#include <QDir>
#include <QTimer>
#include <QDebug>
@ -35,7 +37,14 @@ enum AccountListVersion {
MojangMSA = 3
};
AccountList::AccountList(QObject *parent) : QAbstractListModel(parent) { }
AccountList::AccountList(QObject *parent) : QAbstractListModel(parent) {
m_refreshTimer = new QTimer(this);
m_refreshTimer->setSingleShot(true);
connect(m_refreshTimer, &QTimer::timeout, this, &AccountList::fillQueue);
m_nextTimer = new QTimer(this);
m_nextTimer->setSingleShot(true);
connect(m_nextTimer, &QTimer::timeout, this, &AccountList::tryNext);
}
AccountList::~AccountList() noexcept {}
@ -244,13 +253,29 @@ QVariant AccountList::data(const QModelIndex &index, int role) const
}
case StatusColumn: {
if(account->isActive()) {
return tr("Working", "Account status");
switch(account->accountState()) {
case AccountState::Unchecked: {
return tr("Unchecked", "Account status");
}
case AccountState::Offline: {
return tr("Offline", "Account status");
}
case AccountState::Online: {
return tr("Online", "Account status");
}
case AccountState::Working: {
return tr("Working", "Account status");
}
case AccountState::Errored: {
return tr("Errored", "Account status");
}
case AccountState::Expired: {
return tr("Expired", "Account status");
}
case AccountState::Gone: {
return tr("Gone", "Account status");
}
}
if(account->isExpired()) {
return tr("Expired", "Account status");
}
return tr("Ready", "Account status");
}
case ProfileNameColumn: {
@ -583,10 +608,105 @@ void AccountList::setListFilePath(QString path, bool autosave)
bool AccountList::anyAccountIsValid()
{
for(auto account:m_accounts)
for(auto account: m_accounts)
{
if(account->accountStatus() != NotVerified)
if(account->ownsMinecraft()) {
return true;
}
}
return false;
}
void AccountList::fillQueue() {
if(m_defaultAccount && m_defaultAccount->shouldRefresh()) {
auto idToRefresh = m_defaultAccount->internalId();
m_refreshQueue.push_back(idToRefresh);
qDebug() << "AccountList: Queued default account with internal ID " << idToRefresh << " to refresh first";
}
for(int i = 0; i < count(); i++) {
auto account = at(i);
if(account == m_defaultAccount) {
continue;
}
if(account->shouldRefresh()) {
auto idToRefresh = account->internalId();
m_refreshQueue.push_back(idToRefresh);
qDebug() << "AccountList: Queued account with internal ID " << idToRefresh << " to refresh";
}
}
m_refreshQueue.removeDuplicates();
tryNext();
}
void AccountList::requestRefresh(QString accountId) {
m_refreshQueue.push_back(accountId);
if(!isActive()) {
tryNext();
}
}
void AccountList::tryNext() {
beginActivity();
while (m_refreshQueue.length()) {
auto accountId = m_refreshQueue.front();
m_refreshQueue.pop_front();
for(int i = 0; i < count(); i++) {
auto account = at(i);
if(account->internalId() == accountId) {
m_currentTask = account->refresh();
if(m_currentTask) {
connect(m_currentTask.get(), &AccountTask::succeeded, this, &AccountList::authSucceeded);
connect(m_currentTask.get(), &AccountTask::failed, this, &AccountList::authFailed);
m_currentTask->start();
qDebug() << "RefreshSchedule: Processing account " << account->accountDisplayString() << " with internal ID " << accountId;
return;
}
}
}
qDebug() << "RefreshSchedule: Account with with internal ID " << accountId << " not found.";
}
endActivity();
// if we get here, no account needed refreshing. Schedule refresh in an hour.
m_refreshTimer->start(std::chrono::hours(1));
}
void AccountList::authSucceeded() {
qDebug() << "RefreshSchedule: Background account refresh succeeded";
m_currentTask.reset();
endActivity();
m_nextTimer->start(std::chrono::seconds(20));
}
void AccountList::authFailed(QString reason) {
qDebug() << "RefreshSchedule: Background account refresh failed: " << reason;
m_currentTask.reset();
endActivity();
m_nextTimer->start(std::chrono::seconds(20));
}
bool AccountList::isActive() const {
return m_activityCount != 0;
}
void AccountList::beginActivity() {
bool activating = m_activityCount == 0;
m_activityCount++;
if(activating) {
emit activityChanged(true);
}
}
void AccountList::endActivity() {
if(m_activityCount == 0) {
qWarning() << m_name << " - Activity count would become below zero";
return;
}
bool deactivating = m_activityCount == 1;
m_activityCount--;
if(deactivating) {
emit activityChanged(false);
}
}

View File

@ -67,6 +67,8 @@ public:
MinecraftAccountPtr getAccountByProfileName(const QString &profileName) const;
QStringList profileNames() const;
void requestRefresh(QString accountId);
/*!
* Sets the path to load/save the list file from/to.
* If autosave is true, this list will automatically save to the given path whenever it changes.
@ -85,10 +87,20 @@ public:
void setDefaultAccount(MinecraftAccountPtr profileId);
bool anyAccountIsValid();
bool isActive() const;
protected:
void beginActivity();
void endActivity();
private:
const char* m_name;
uint32_t m_activityCount = 0;
signals:
void listChanged();
void listActivityChanged();
void defaultAccountChanged();
void activityChanged(bool active);
public slots:
/**
@ -101,7 +113,23 @@ public slots:
*/
void accountActivityChanged(bool active);
/**
* This is initially to run background account refresh tasks, or on a hourly timer
*/
void fillQueue();
private slots:
void tryNext();
void authSucceeded();
void authFailed(QString reason);
protected:
QList<QString> m_refreshQueue;
QTimer *m_refreshTimer;
QTimer *m_nextTimer;
shared_qobject_ptr<AccountTask> m_currentTask;
/*!
* Called whenever the list changes.
* This emits the listChanged() signal and autosaves the list (if autosave is enabled).

View File

@ -28,40 +28,79 @@
AccountTask::AccountTask(AccountData *data, QObject *parent)
: Task(parent), m_data(data)
{
changeState(STATE_CREATED);
changeState(AccountTaskState::STATE_CREATED);
}
QString AccountTask::getStateMessage() const
{
switch (m_accountState)
switch (m_taskState)
{
case STATE_CREATED:
case AccountTaskState::STATE_CREATED:
return "Waiting...";
case STATE_WORKING:
case AccountTaskState::STATE_WORKING:
return tr("Sending request to auth servers...");
case STATE_SUCCEEDED:
case AccountTaskState::STATE_SUCCEEDED:
return tr("Authentication task succeeded.");
case STATE_FAILED_SOFT:
case AccountTaskState::STATE_OFFLINE:
return tr("Failed to contact the authentication server.");
case STATE_FAILED_HARD:
return tr("Failed to authenticate.");
case STATE_FAILED_GONE:
case AccountTaskState::STATE_FAILED_SOFT:
return tr("Encountered an error during authentication.");
case AccountTaskState::STATE_FAILED_HARD:
return tr("Failed to authenticate. The session has expired.");
case AccountTaskState::STATE_FAILED_GONE:
return tr("Failed to authenticate. The account no longer exists.");
default:
return tr("...");
}
}
void AccountTask::changeState(AccountTask::State newState, QString reason)
bool AccountTask::changeState(AccountTaskState newState, QString reason)
{
m_accountState = newState;
m_taskState = newState;
setStatus(getStateMessage());
if (newState == STATE_SUCCEEDED)
{
emitSucceeded();
}
else if (newState == STATE_FAILED_HARD || newState == STATE_FAILED_SOFT || newState == STATE_FAILED_GONE)
{
emitFailed(reason);
switch(newState) {
case AccountTaskState::STATE_CREATED: {
m_data->errorString.clear();
return true;
}
case AccountTaskState::STATE_WORKING: {
m_data->accountState = AccountState::Working;
return true;
}
case AccountTaskState::STATE_SUCCEEDED: {
m_data->accountState = AccountState::Online;
emitSucceeded();
return false;
}
case AccountTaskState::STATE_OFFLINE: {
m_data->errorString = reason;
m_data->accountState = AccountState::Offline;
emitFailed(reason);
return false;
}
case AccountTaskState::STATE_FAILED_SOFT: {
m_data->errorString = reason;
m_data->accountState = AccountState::Errored;
emitFailed(reason);
return false;
}
case AccountTaskState::STATE_FAILED_HARD: {
m_data->errorString = reason;
m_data->accountState = AccountState::Expired;
emitFailed(reason);
return false;
}
case AccountTaskState::STATE_FAILED_GONE: {
m_data->errorString = reason;
m_data->accountState = AccountState::Gone;
emitFailed(reason);
return false;
}
default: {
QString error = tr("Unknown account task state: %1").arg(int(newState));
m_data->accountState = AccountState::Errored;
emitFailed(error);
return false;
}
}
}

View File

@ -26,62 +26,32 @@
class QNetworkReply;
/**
* Enum for describing the state of the current task.
* Used by the getStateMessage function to determine what the status message should be.
*/
enum class AccountTaskState
{
STATE_CREATED,
STATE_WORKING,
STATE_SUCCEEDED,
STATE_FAILED_SOFT, //!< soft failure. authentication went through partially
STATE_FAILED_HARD, //!< hard failure. main tokens are invalid
STATE_FAILED_GONE, //!< hard failure. main tokens are invalid, and the account no longer exists
STATE_OFFLINE //!< soft failure. authentication failed in the first step in a 'soft' way
};
class AccountTask : public Task
{
friend class AuthContext;
Q_OBJECT
public:
explicit AccountTask(AccountData * data, QObject *parent = 0);
virtual ~AccountTask() {};
/**
* assign a session to this task. the session will be filled with required infomration
* upon completion
*/
void assignSession(AuthSessionPtr session)
{
m_session = session;
}
AccountTaskState m_taskState = AccountTaskState::STATE_CREATED;
/// get the assigned session for filling with information.
AuthSessionPtr getAssignedSession()
{
return m_session;
}
/**
* Class describing a Account error response.
*/
struct Error
{
QString m_errorMessageShort;
QString m_errorMessageVerbose;
QString m_cause;
};
enum AbortedBy
{
BY_NOTHING,
BY_USER,
BY_TIMEOUT
} m_aborted = BY_NOTHING;
/**
* Enum for describing the state of the current task.
* Used by the getStateMessage function to determine what the status message should be.
*/
enum State
{
STATE_CREATED,
STATE_WORKING,
STATE_FAILED_SOFT, //!< soft failure. this generally means the user auth details haven't been invalidated
STATE_FAILED_HARD, //!< hard failure. auth is invalid
STATE_FAILED_GONE, //!< hard failure. auth is invalid, and the account no longer exists
STATE_SUCCEEDED
} m_accountState = STATE_CREATED;
State accountState() {
return m_accountState;
AccountTaskState taskState() {
return m_taskState;
}
signals:
@ -98,11 +68,9 @@ protected:
virtual QString getStateMessage() const;
protected slots:
void changeState(State newState, QString reason=QString());
// NOTE: true -> non-terminal state, false -> terminal state
bool changeState(AccountTaskState newState, QString reason = QString());
protected:
// FIXME: segfault disaster waiting to happen
AccountData *m_data = nullptr;
std::shared_ptr<Error> m_error;
AuthSessionPtr m_session;
};

View File

@ -44,6 +44,7 @@ void AuthRequest::onRequestFinished() {
if (reply_ != qobject_cast<QNetworkReply *>(sender())) {
return;
}
httpStatus_ = 200;
finish();
}
@ -55,10 +56,11 @@ void AuthRequest::onRequestError(QNetworkReply::NetworkError error) {
if (reply_ != qobject_cast<QNetworkReply *>(sender())) {
return;
}
qWarning() << "AuthRequest::onRequestError: Error string: " << reply_->errorString();
int httpStatus = reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
qWarning() << "AuthRequest::onRequestError: HTTP status" << httpStatus << reply_->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
errorString_ = reply_->errorString();
httpStatus_ = reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
error_ = error;
qWarning() << "AuthRequest::onRequestError: Error string: " << errorString_;
qWarning() << "AuthRequest::onRequestError: HTTP status" << httpStatus_ << reply_->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
// QTimer::singleShot(10, this, SLOT(finish()));
}
@ -103,6 +105,8 @@ void AuthRequest::setup(const QNetworkRequest &req, QNetworkAccessManager::Opera
status_ = Requesting;
error_ = QNetworkReply::NoError;
errorString_.clear();
httpStatus_ = 0;
}
void AuthRequest::finish() {

View File

@ -46,6 +46,11 @@ protected slots:
/// Handle upload progress.
void onUploadProgress(qint64 uploaded, qint64 total);
public:
QNetworkReply::NetworkError error_;
int httpStatus_ = 0;
QString errorString_;
protected:
void setup(const QNetworkRequest &request, QNetworkAccessManager::Operation operation, const QByteArray &verb = QByteArray());
@ -60,5 +65,6 @@ protected:
QNetworkAccessManager::Operation operation_;
QUrl url_;
Katabasis::ReplyList timedReplies_;
QNetworkReply::NetworkError error_;
QTimer *timer_;
};

View File

@ -0,0 +1,7 @@
#include "AuthStep.h"
AuthStep::AuthStep(AccountData *data) : QObject(nullptr), m_data(data) {
}
AuthStep::~AuthStep() noexcept = default;

View File

@ -0,0 +1,33 @@
#pragma once
#include <QObject>
#include <QList>
#include <QNetworkReply>
#include "QObjectPtr.h"
#include "minecraft/auth/AccountData.h"
#include "AccountTask.h"
class AuthStep : public QObject {
Q_OBJECT
public:
using Ptr = shared_qobject_ptr<AuthStep>;
public:
explicit AuthStep(AccountData *data);
virtual ~AuthStep() noexcept;
virtual QString describe() = 0;
public slots:
virtual void perform() = 0;
virtual void rehydrate() = 0;
signals:
void finished(AccountTaskState resultingState, QString message);
void showVerificationUriAndCode(const QUrl &uri, const QString &code, int expiresIn);
void hideVerificationUriAndCode();
protected:
AccountData *m_data;
};

View File

@ -16,7 +16,6 @@
*/
#include "MinecraftAccount.h"
#include "flows/AuthContext.h"
#include <QUuid>
#include <QJsonObject>
@ -28,14 +27,12 @@
#include <QDebug>
#include <QPainter>
#include "flows/MSASilent.h"
#include "flows/MSAInteractive.h"
#include "flows/MojangRefresh.h"
#include "flows/MojangLogin.h"
#include "flows/MSA.h"
#include "flows/Mojang.h"
MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) {
m_internalId = QUuid::createUuid().toString().remove(QRegExp("[{}-]"));
data.internalId = QUuid::createUuid().toString().remove(QRegExp("[{}-]"));
}
@ -77,42 +74,10 @@ QJsonObject MinecraftAccount::saveToJson() const
return data.saveState();
}
AccountStatus MinecraftAccount::accountStatus() const {
if(data.type == AccountType::Mojang) {
if (data.accessToken().isEmpty()) {
return NotVerified;
}
else {
return Verified;
}
}
// MSA
// FIXME: this is extremely crude and probably wrong
if(data.msaToken.token.isEmpty()) {
return NotVerified;
}
else {
return Verified;
}
AccountState MinecraftAccount::accountState() const {
return data.accountState;
}
bool MinecraftAccount::isExpired() const {
switch(data.type) {
case AccountType::Mojang: {
return data.accessToken().isEmpty();
}
break;
case AccountType::MSA: {
return data.msaToken.validity == Katabasis::Validity::None;
}
break;
default: {
return true;
}
}
}
QPixmap MinecraftAccount::getFace() const {
QPixmap skinTexture;
if(!skinTexture.loadFromData(data.minecraftProfile.skin.data, "PNG")) {
@ -126,136 +91,51 @@ QPixmap MinecraftAccount::getFace() const {
}
shared_qobject_ptr<AccountTask> MinecraftAccount::login(AuthSessionPtr session, QString password)
{
shared_qobject_ptr<AccountTask> MinecraftAccount::login(QString password) {
Q_ASSERT(m_currentTask.get() == nullptr);
// take care of the true offline status
if (accountStatus() == NotVerified && password.isEmpty())
{
if (session)
{
session->status = AuthSession::RequiresPassword;
fillSession(session);
}
return nullptr;
}
if(accountStatus() == Verified && !session->wants_online)
{
session->status = AuthSession::PlayableOffline;
session->auth_server_online = false;
fillSession(session);
return nullptr;
}
else
{
if (password.isEmpty())
{
m_currentTask.reset(new MojangRefresh(&data));
}
else
{
m_currentTask.reset(new MojangLogin(&data, password));
}
m_currentTask->assignSession(session);
connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
emit activityChanged(true);
}
m_currentTask.reset(new MojangLogin(&data, password));
connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
emit activityChanged(true);
return m_currentTask;
}
shared_qobject_ptr<AccountTask> MinecraftAccount::loginMSA(AuthSessionPtr session) {
shared_qobject_ptr<AccountTask> MinecraftAccount::loginMSA() {
Q_ASSERT(m_currentTask.get() == nullptr);
if(accountStatus() == Verified && !session->wants_online)
{
session->status = AuthSession::PlayableOffline;
session->auth_server_online = false;
fillSession(session);
return nullptr;
}
else
{
m_currentTask.reset(new MSAInteractive(&data));
m_currentTask->assignSession(session);
connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
emit activityChanged(true);
}
m_currentTask.reset(new MSAInteractive(&data));
connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
emit activityChanged(true);
return m_currentTask;
}
shared_qobject_ptr<AccountTask> MinecraftAccount::refresh(AuthSessionPtr session) {
Q_ASSERT(m_currentTask.get() == nullptr);
// take care of the true offline status
if (accountStatus() == NotVerified)
{
if (session)
{
if(data.type == AccountType::MSA) {
session->status = AuthSession::RequiresOAuth;
}
else {
session->status = AuthSession::RequiresPassword;
}
fillSession(session);
}
return nullptr;
shared_qobject_ptr<AccountTask> MinecraftAccount::refresh() {
if(m_currentTask) {
return m_currentTask;
}
if(accountStatus() == Verified && !session->wants_online)
{
session->status = AuthSession::PlayableOffline;
session->auth_server_online = false;
fillSession(session);
return nullptr;
if(data.type == AccountType::MSA) {
m_currentTask.reset(new MSASilent(&data));
}
else {
m_currentTask.reset(new MojangRefresh(&data));
}
else
{
if(data.type == AccountType::MSA) {
m_currentTask.reset(new MSASilent(&data));
}
else {
m_currentTask.reset(new MojangRefresh(&data));
}
m_currentTask->assignSession(session);
connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
emit activityChanged(true);
}
connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
emit activityChanged(true);
return m_currentTask;
}
shared_qobject_ptr<AccountTask> MinecraftAccount::currentTask() {
return m_currentTask;
}
void MinecraftAccount::authSucceeded()
{
auto session = m_currentTask->getAssignedSession();
if (session)
{
/*
session->status = AuthSession::RequiresProfileSetup;
session->auth_server_online = true;
*/
if(data.profileId().size() == 0) {
session->status = AuthSession::RequiresProfileSetup;
}
else {
if(session->wants_online) {
session->status = AuthSession::PlayableOnline;
}
else {
session->status = AuthSession::PlayableOffline;
}
}
fillSession(session);
session->auth_server_online = true;
}
m_currentTask.reset();
emit changed();
emit activityChanged(false);
@ -263,62 +143,35 @@ void MinecraftAccount::authSucceeded()
void MinecraftAccount::authFailed(QString reason)
{
auto session = m_currentTask->getAssignedSession();
// This is emitted when the yggdrasil tasks time out or are cancelled.
// -> we treat the error as no-op
switch (m_currentTask->accountState()) {
case AccountTask::STATE_FAILED_SOFT: {
if (session)
{
if(accountStatus() == Verified) {
session->status = AuthSession::PlayableOffline;
}
else {
if(data.type == AccountType::MSA) {
session->status = AuthSession::RequiresOAuth;
}
else {
session->status = AuthSession::RequiresPassword;
}
}
session->auth_server_online = false;
fillSession(session);
}
switch (m_currentTask->taskState()) {
case AccountTaskState::STATE_OFFLINE:
case AccountTaskState::STATE_FAILED_SOFT: {
// NOTE: this doesn't do much. There was an error of some sort.
}
break;
case AccountTask::STATE_FAILED_HARD: {
// FIXME: MSA data clearing
data.yggdrasilToken.token = QString();
data.yggdrasilToken.validity = Katabasis::Validity::None;
case AccountTaskState::STATE_FAILED_HARD: {
if(isMSA()) {
data.msaToken.token = QString();
data.msaToken.refresh_token = QString();
data.msaToken.validity = Katabasis::Validity::None;
data.validity_ = Katabasis::Validity::None;
}
else {
data.yggdrasilToken.token = QString();
data.yggdrasilToken.validity = Katabasis::Validity::None;
data.validity_ = Katabasis::Validity::None;
}
emit changed();
}
break;
case AccountTaskState::STATE_FAILED_GONE: {
data.validity_ = Katabasis::Validity::None;
emit changed();
if (session)
{
if(data.type == AccountType::MSA) {
session->status = AuthSession::RequiresOAuth;
}
else {
session->status = AuthSession::RequiresPassword;
}
session->auth_server_online = true;
fillSession(session);
}
}
break;
case AccountTask::STATE_FAILED_GONE: {
data.validity_ = Katabasis::Validity::None;
emit changed();
if (session)
{
session->status = AuthSession::GoneOrMigrated;
session->auth_server_online = true;
fillSession(session);
}
}
break;
case AccountTask::STATE_CREATED:
case AccountTask::STATE_WORKING:
case AccountTask::STATE_SUCCEEDED: {
case AccountTaskState::STATE_CREATED:
case AccountTaskState::STATE_WORKING:
case AccountTaskState::STATE_SUCCEEDED: {
// Not reachable here, as they are not failures.
}
}
@ -366,6 +219,18 @@ bool MinecraftAccount::shouldRefresh() const {
void MinecraftAccount::fillSession(AuthSessionPtr session)
{
if(ownsMinecraft() && !hasProfile()) {
session->status = AuthSession::RequiresProfileSetup;
}
else {
if(session->wants_online) {
session->status = AuthSession::PlayableOnline;
}
else {
session->status = AuthSession::PlayableOffline;
}
}
// the user name. you have to have an user name
// FIXME: not with MSA
session->username = data.userName();

View File

@ -24,6 +24,7 @@
#include <QPixmap>
#include <memory>
#include "AuthSession.h"
#include "Usable.h"
#include "AccountData.h"
@ -50,12 +51,6 @@ struct AccountProfile
bool legacy;
};
enum AccountStatus
{
NotVerified,
Verified
};
/**
* Object that stores information about a certain Mojang account.
*
@ -90,15 +85,17 @@ public: /* manipulation */
* Attempt to login. Empty password means we use the token.
* If the attempt fails because we already are performing some task, it returns false.
*/
shared_qobject_ptr<AccountTask> login(AuthSessionPtr session, QString password);
shared_qobject_ptr<AccountTask> login(QString password);
shared_qobject_ptr<AccountTask> loginMSA(AuthSessionPtr session);
shared_qobject_ptr<AccountTask> loginMSA();
shared_qobject_ptr<AccountTask> refresh(AuthSessionPtr session);
shared_qobject_ptr<AccountTask> refresh();
shared_qobject_ptr<AccountTask> currentTask();
public: /* queries */
QString internalId() const {
return m_internalId;
return data.internalId;
}
QString accountDisplayString() const {
@ -123,8 +120,6 @@ public: /* queries */
bool isActive() const;
bool isExpired() const;
bool canMigrate() const {
return data.canMigrateToMSA;
}
@ -133,6 +128,14 @@ public: /* queries */
return data.type == AccountType::MSA;
}
bool ownsMinecraft() const {
return data.minecraftEntitlement.ownsMinecraft;
}
bool hasProfile() const {
return data.profileId().size() != 0;
}
QString typeString() const {
switch(data.type) {
case AccountType::Mojang: {
@ -154,8 +157,8 @@ public: /* queries */
QPixmap getFace() const;
//! Returns whether the account is NotVerified, Verified or Online
AccountStatus accountStatus() const;
//! Returns the current state of the account
AccountState accountState() const;
AccountData * accountData() {
return &data;
@ -163,6 +166,8 @@ public: /* queries */
bool shouldRefresh() const;
void fillSession(AuthSessionPtr session);
signals:
/**
* This signal is emitted when the account changes
@ -174,7 +179,6 @@ signals:
// TODO: better signalling for the various possible state changes - especially errors
protected: /* variables */
QString m_internalId;
AccountData data;
// current task we are executing here
@ -189,7 +193,4 @@ private
slots:
void authSucceeded();
void authFailed(QString reason);
private:
void fillSession(AuthSessionPtr session);
};

View File

@ -72,7 +72,7 @@ bool getBool(QJsonValue value, bool & out) {
// 2148916238 = child account not linked to a family
*/
bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output, const char * name) {
bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output, QString name) {
qDebug() << "Parsing" << name <<":";
#ifndef NDEBUG
qDebug() << data;

View File

@ -1,6 +1,6 @@
#pragma once
#include "../AccountData.h"
#include "AccountData.h"
namespace Parsers
{
@ -10,7 +10,7 @@ namespace Parsers
bool getNumber(QJsonValue value, int64_t & out);
bool getBool(QJsonValue value, bool & out);
bool parseXTokenResponse(QByteArray &data, Katabasis::Token &output, const char * name);
bool parseXTokenResponse(QByteArray &data, Katabasis::Token &output, QString name);
bool parseMojangResponse(QByteArray &data, Katabasis::Token &output);
bool parseMinecraftProfile(QByteArray &data, MinecraftProfile &output);

View File

@ -14,7 +14,7 @@
*/
#include "Yggdrasil.h"
#include "../AccountData.h"
#include "AccountData.h"
#include <QObject>
#include <QString>
@ -30,11 +30,11 @@
Yggdrasil::Yggdrasil(AccountData *data, QObject *parent)
: AccountTask(data, parent)
{
changeState(STATE_CREATED);
changeState(AccountTaskState::STATE_CREATED);
}
void Yggdrasil::sendRequest(QUrl endpoint, QByteArray content) {
changeState(STATE_WORKING);
changeState(AccountTaskState::STATE_WORKING);
QNetworkRequest netRequest(endpoint);
netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
@ -185,14 +185,14 @@ void Yggdrasil::processResponse(QJsonObject responseData) {
QString clientToken = responseData.value("clientToken").toString("");
if (clientToken.isEmpty()) {
// Fail if the server gave us an empty client token
changeState(STATE_FAILED_HARD, tr("Authentication server didn't send a client token."));
changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send a client token."));
return;
}
if(m_data->clientToken().isEmpty()) {
m_data->setClientToken(clientToken);
}
else if(clientToken != m_data->clientToken()) {
changeState(STATE_FAILED_HARD, tr("Authentication server attempted to change the client token. This isn't supported."));
changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server attempted to change the client token. This isn't supported."));
return;
}
@ -201,7 +201,7 @@ void Yggdrasil::processResponse(QJsonObject responseData) {
QString accessToken = responseData.value("accessToken").toString("");
if (accessToken.isEmpty()) {
// Fail if the server didn't give us an access token.
changeState(STATE_FAILED_HARD, tr("Authentication server didn't send an access token."));
changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send an access token."));
return;
}
// Set the access token.
@ -212,25 +212,25 @@ void Yggdrasil::processResponse(QJsonObject responseData) {
// We've made it through the minefield of possible errors. Return true to indicate that
// we've succeeded.
qDebug() << "Finished reading authentication response.";
changeState(STATE_SUCCEEDED);
changeState(AccountTaskState::STATE_SUCCEEDED);
}
void Yggdrasil::processReply() {
changeState(STATE_WORKING);
changeState(AccountTaskState::STATE_WORKING);
switch (m_netReply->error())
{
case QNetworkReply::NoError:
break;
case QNetworkReply::TimeoutError:
changeState(STATE_FAILED_SOFT, tr("Authentication operation timed out."));
changeState(AccountTaskState::STATE_FAILED_SOFT, tr("Authentication operation timed out."));
return;
case QNetworkReply::OperationCanceledError:
changeState(STATE_FAILED_SOFT, tr("Authentication operation cancelled."));
changeState(AccountTaskState::STATE_FAILED_SOFT, tr("Authentication operation cancelled."));
return;
case QNetworkReply::SslHandshakeFailedError:
changeState(
STATE_FAILED_SOFT,
AccountTaskState::STATE_FAILED_SOFT,
tr(
"<b>SSL Handshake failed.</b><br/>There might be a few causes for it:<br/>"
"<ul>"
@ -248,13 +248,13 @@ void Yggdrasil::processReply() {
break;
case QNetworkReply::ContentGoneError: {
changeState(
STATE_FAILED_GONE,
AccountTaskState::STATE_FAILED_GONE,
tr("The Mojang account no longer exists. It may have been migrated to a Microsoft account.")
);
}
default:
changeState(
STATE_FAILED_SOFT,
AccountTaskState::STATE_FAILED_SOFT,
tr("Authentication operation failed due to a network error: %1 (%2)").arg(m_netReply->errorString()).arg(m_netReply->error())
);
return;
@ -279,7 +279,7 @@ void Yggdrasil::processReply() {
}
else {
changeState(
STATE_FAILED_SOFT,
AccountTaskState::STATE_FAILED_SOFT,
tr("Failed to parse authentication server response JSON response: %1 at offset %2.").arg(jsonError.errorString()).arg(jsonError.offset)
);
qCritical() << replyData;
@ -303,7 +303,7 @@ void Yggdrasil::processReply() {
// error.
qDebug() << "The request failed and the server gave no error message. Unknown error.";
changeState(
STATE_FAILED_SOFT,
AccountTaskState::STATE_FAILED_SOFT,
tr("An unknown error occurred when trying to communicate with the authentication server: %1").arg(m_netReply->errorString())
);
}
@ -322,10 +322,10 @@ void Yggdrasil::processError(QJsonObject responseData) {
causeVal.toString("")
}
);
changeState(STATE_FAILED_HARD, m_error->m_errorMessageVerbose);
changeState(AccountTaskState::STATE_FAILED_HARD, m_error->m_errorMessageVerbose);
}
else {
// Error is not in standard format. Don't set m_error and return unknown error.
changeState(STATE_FAILED_HARD, tr("An unknown Yggdrasil error occurred."));
changeState(AccountTaskState::STATE_FAILED_HARD, tr("An unknown Yggdrasil error occurred."));
}
}

View File

@ -15,14 +15,14 @@
#pragma once
#include "../AccountTask.h"
#include "AccountTask.h"
#include <QString>
#include <QJsonObject>
#include <QTimer>
#include <qsslerror.h>
#include "../MinecraftAccount.h"
#include "MinecraftAccount.h"
class QNetworkAccessManager;
class QNetworkReply;
@ -38,10 +38,26 @@ public:
AccountData *data,
QObject *parent = 0
);
virtual ~Yggdrasil() {};
virtual ~Yggdrasil() = default;
void refresh();
void login(QString password);
struct Error
{
QString m_errorMessageShort;
QString m_errorMessageVerbose;
QString m_cause;
};
std::shared_ptr<Error> m_error;
enum AbortedBy
{
BY_NOTHING,
BY_USER,
BY_TIMEOUT
} m_aborted = BY_NOTHING;
protected:
void executeTask() override;

View File

@ -1,671 +0,0 @@
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QDesktopServices>
#include <QMetaEnum>
#include <QDebug>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QUuid>
#include <QUrlQuery>
#include "AuthContext.h"
#include "katabasis/Globals.h"
#include "AuthRequest.h"
#include "Parsers.h"
#include <Application.h>
using OAuth2 = Katabasis::DeviceFlow;
using Activity = Katabasis::Activity;
AuthContext::AuthContext(AccountData * data, QObject *parent) :
AccountTask(data, parent)
{
}
void AuthContext::beginActivity(Activity activity) {
if(isBusy()) {
throw 0;
}
m_activity = activity;
changeState(STATE_WORKING, "Initializing");
emit activityChanged(m_activity);
}
void AuthContext::finishActivity() {
if(!isBusy()) {
throw 0;
}
m_activity = Katabasis::Activity::Idle;
setStage(AuthStage::Complete);
m_data->validity_ = m_data->minecraftProfile.validity;
emit activityChanged(m_activity);
}
void AuthContext::initMSA() {
if(m_oauth2) {
return;
}
OAuth2::Options opts;
opts.scope = "XboxLive.signin offline_access";
opts.clientIdentifier = APPLICATION->msaClientId();
opts.authorizationUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode";
opts.accessTokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
// FIXME: OAuth2 is not aware of our fancy shared pointers
m_oauth2 = new OAuth2(opts, m_data->msaToken, this, APPLICATION->network().get());
connect(m_oauth2, &OAuth2::activityChanged, this, &AuthContext::onOAuthActivityChanged);
connect(m_oauth2, &OAuth2::showVerificationUriAndCode, this, &AuthContext::showVerificationUriAndCode);
}
void AuthContext::initMojang() {
if(m_yggdrasil) {
return;
}
m_yggdrasil = new Yggdrasil(m_data, this);
connect(m_yggdrasil, &Task::failed, this, &AuthContext::onMojangFailed);
connect(m_yggdrasil, &Task::succeeded, this, &AuthContext::onMojangSucceeded);
}
void AuthContext::onMojangSucceeded() {
doMinecraftProfile();
}
void AuthContext::onMojangFailed() {
finishActivity();
m_error = m_yggdrasil->m_error;
m_aborted = m_yggdrasil->m_aborted;
changeState(m_yggdrasil->accountState(), tr("Mojang user authentication failed."));
}
void AuthContext::onOAuthActivityChanged(Katabasis::Activity activity) {
switch(activity) {
case Katabasis::Activity::Idle:
case Katabasis::Activity::LoggingIn:
case Katabasis::Activity::Refreshing:
case Katabasis::Activity::LoggingOut: {
// We asked it to do something, it's doing it. Nothing to act upon.
return;
}
case Katabasis::Activity::Succeeded: {
// Succeeded or did not invalidate tokens
emit hideVerificationUriAndCode();
if (!m_oauth2->linked()) {
finishActivity();
changeState(STATE_FAILED_HARD, tr("Microsoft user authentication ended with an impossible state (succeeded, but not succeeded at the same time)."));
return;
}
QVariantMap extraTokens = m_oauth2->extraTokens();
#ifndef NDEBUG
if (!extraTokens.isEmpty()) {
qDebug() << "Extra tokens in response:";
foreach (QString key, extraTokens.keys()) {
qDebug() << "\t" << key << ":" << extraTokens.value(key);
}
}
#endif
doUserAuth();
return;
}
case Katabasis::Activity::FailedSoft: {
emit hideVerificationUriAndCode();
finishActivity();
changeState(STATE_FAILED_SOFT, tr("Microsoft user authentication failed with a soft error."));
return;
}
case Katabasis::Activity::FailedGone:
case Katabasis::Activity::FailedHard: {
emit hideVerificationUriAndCode();
finishActivity();
changeState(STATE_FAILED_HARD, tr("Microsoft user authentication failed."));
return;
}
default: {
emit hideVerificationUriAndCode();
finishActivity();
changeState(STATE_FAILED_HARD, tr("Microsoft user authentication completed with an unrecognized result."));
return;
}
}
}
void AuthContext::doUserAuth() {
setStage(AuthStage::UserAuth);
changeState(STATE_WORKING, tr("Starting user authentication"));
QString xbox_auth_template = R"XXX(
{
"Properties": {
"AuthMethod": "RPS",
"SiteName": "user.auth.xboxlive.com",
"RpsTicket": "d=%1"
},
"RelyingParty": "http://auth.xboxlive.com",
"TokenType": "JWT"
}
)XXX";
auto xbox_auth_data = xbox_auth_template.arg(m_data->msaToken.token);
QNetworkRequest request = QNetworkRequest(QUrl("https://user.auth.xboxlive.com/user/authenticate"));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Accept", "application/json");
auto *requestor = new AuthRequest(this);
connect(requestor, &AuthRequest::finished, this, &AuthContext::onUserAuthDone);
requestor->post(request, xbox_auth_data.toUtf8());
qDebug() << "First layer of XBox auth ... commencing.";
}
void AuthContext::onUserAuthDone(
QNetworkReply::NetworkError error,
QByteArray replyData,
QList<QNetworkReply::RawHeaderPair> headers
) {
if (error != QNetworkReply::NoError) {
qWarning() << "Reply error:" << error;
finishActivity();
changeState(STATE_FAILED_HARD, tr("XBox user authentication failed."));
return;
}
Katabasis::Token temp;
if(!Parsers::parseXTokenResponse(replyData, temp, "UToken")) {
qWarning() << "Could not parse user authentication response...";
finishActivity();
changeState(STATE_FAILED_HARD, tr("XBox user authentication response could not be understood."));
return;
}
m_data->userToken = temp;
setStage(AuthStage::XboxAuth);
changeState(STATE_WORKING, tr("Starting XBox authentication"));
doSTSAuthMinecraft();
doSTSAuthGeneric();
}
/*
url = "https://xsts.auth.xboxlive.com/xsts/authorize"
headers = {"x-xbl-contract-version": "1"}
data = {
"RelyingParty": relying_party,
"TokenType": "JWT",
"Properties": {
"UserTokens": [self.user_token.token],
"SandboxId": "RETAIL",
},
}
*/
void AuthContext::doSTSAuthMinecraft() {
QString xbox_auth_template = R"XXX(
{
"Properties": {
"SandboxId": "RETAIL",
"UserTokens": [
"%1"
]
},
"RelyingParty": "rp://api.minecraftservices.com/",
"TokenType": "JWT"
}
)XXX";
auto xbox_auth_data = xbox_auth_template.arg(m_data->userToken.token);
QNetworkRequest request = QNetworkRequest(QUrl("https://xsts.auth.xboxlive.com/xsts/authorize"));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Accept", "application/json");
AuthRequest *requestor = new AuthRequest(this);
connect(requestor, &AuthRequest::finished, this, &AuthContext::onSTSAuthMinecraftDone);
requestor->post(request, xbox_auth_data.toUtf8());
qDebug() << "Getting Minecraft services STS token...";
}
void AuthContext::processSTSError(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers) {
if(error == QNetworkReply::AuthenticationRequiredError) {
QJsonParseError jsonError;
QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
if(jsonError.error) {
qWarning() << "Cannot parse error XSTS response as JSON: " << jsonError.errorString();
return;
}
int64_t errorCode = -1;
auto obj = doc.object();
if(!Parsers::getNumber(obj.value("XErr"), errorCode)) {
qWarning() << "XErr is not a number";
return;
}
stsErrors.insert(errorCode);
stsFailed = true;
}
}
void AuthContext::onSTSAuthMinecraftDone(
QNetworkReply::NetworkError error,
QByteArray replyData,
QList<QNetworkReply::RawHeaderPair> headers
) {
#ifndef NDEBUG
qDebug() << replyData;
#endif
if (error != QNetworkReply::NoError) {
qWarning() << "Reply error:" << error;
processSTSError(error, replyData, headers);
failResult(m_mcAuthSucceeded);
return;
}
Katabasis::Token temp;
if(!Parsers::parseXTokenResponse(replyData, temp, "STSAuthMinecraft")) {
qWarning() << "Could not parse authorization response for access to mojang services...";
failResult(m_mcAuthSucceeded);
return;
}
if(temp.extra["uhs"] != m_data->userToken.extra["uhs"]) {
qWarning() << "Server has changed user hash in the reply... something is wrong. ABORTING";
failResult(m_mcAuthSucceeded);
return;
}
m_data->mojangservicesToken = temp;
doMinecraftAuth();
}
void AuthContext::doMinecraftAuth() {
auto requestURL = "https://api.minecraftservices.com/launcher/login";
auto uhs = m_data->mojangservicesToken.extra["uhs"].toString();
auto xToken = m_data->mojangservicesToken.token;
QString mc_auth_template = R"XXX(
{
"xtoken": "XBL3.0 x=%1;%2",
"platform": "PC_LAUNCHER"
}
)XXX";
auto requestBody = mc_auth_template.arg(uhs, xToken);
QNetworkRequest request = QNetworkRequest(QUrl(requestURL));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Accept", "application/json");
AuthRequest *requestor = new AuthRequest(this);
connect(requestor, &AuthRequest::finished, this, &AuthContext::onMinecraftAuthDone);
requestor->post(request, requestBody.toUtf8());
qDebug() << "Getting Minecraft access token...";
}
void AuthContext::onMinecraftAuthDone(
QNetworkReply::NetworkError error,
QByteArray replyData,
QList<QNetworkReply::RawHeaderPair> headers
) {
qDebug() << replyData;
if (error != QNetworkReply::NoError) {
qWarning() << "Reply error:" << error;
#ifndef NDEBUG
qDebug() << replyData;
#endif
failResult(m_mcAuthSucceeded);
return;
}
if(!Parsers::parseMojangResponse(replyData, m_data->yggdrasilToken)) {
qWarning() << "Could not parse login_with_xbox response...";
#ifndef NDEBUG
qDebug() << replyData;
#endif
failResult(m_mcAuthSucceeded);
return;
}
succeedResult(m_mcAuthSucceeded);
}
void AuthContext::doSTSAuthGeneric() {
QString xbox_auth_template = R"XXX(
{
"Properties": {
"SandboxId": "RETAIL",
"UserTokens": [
"%1"
]
},
"RelyingParty": "http://xboxlive.com",
"TokenType": "JWT"
}
)XXX";
auto xbox_auth_data = xbox_auth_template.arg(m_data->userToken.token);
QNetworkRequest request = QNetworkRequest(QUrl("https://xsts.auth.xboxlive.com/xsts/authorize"));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Accept", "application/json");
AuthRequest *requestor = new AuthRequest(this);
connect(requestor, &AuthRequest::finished, this, &AuthContext::onSTSAuthGenericDone);
requestor->post(request, xbox_auth_data.toUtf8());
qDebug() << "Getting generic STS token...";
}
void AuthContext::onSTSAuthGenericDone(
QNetworkReply::NetworkError error,
QByteArray replyData,
QList<QNetworkReply::RawHeaderPair> headers
) {
#ifndef NDEBUG
qDebug() << replyData;
#endif
if (error != QNetworkReply::NoError) {
qWarning() << "Reply error:" << error;
processSTSError(error, replyData, headers);
failResult(m_xboxProfileSucceeded);
return;
}
Katabasis::Token temp;
if(!Parsers::parseXTokenResponse(replyData, temp, "STSAuthGeneric")) {
qWarning() << "Could not parse authorization response for access to xbox API...";
failResult(m_xboxProfileSucceeded);
return;
}
if(temp.extra["uhs"] != m_data->userToken.extra["uhs"]) {
qWarning() << "Server has changed user hash in the reply... something is wrong. ABORTING";
failResult(m_xboxProfileSucceeded);
return;
}
m_data->xboxApiToken = temp;
doXBoxProfile();
}
void AuthContext::doXBoxProfile() {
auto url = QUrl("https://profile.xboxlive.com/users/me/profile/settings");
QUrlQuery q;
q.addQueryItem(
"settings",
"GameDisplayName,AppDisplayName,AppDisplayPicRaw,GameDisplayPicRaw,"
"PublicGamerpic,ShowUserAsAvatar,Gamerscore,Gamertag,ModernGamertag,ModernGamertagSuffix,"
"UniqueModernGamertag,AccountTier,TenureLevel,XboxOneRep,"
"PreferredColor,Location,Bio,Watermarks,"
"RealName,RealNameOverride,IsQuarantined"
);
url.setQuery(q);
QNetworkRequest request = QNetworkRequest(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Accept", "application/json");
request.setRawHeader("x-xbl-contract-version", "3");
request.setRawHeader("Authorization", QString("XBL3.0 x=%1;%2").arg(m_data->userToken.extra["uhs"].toString(), m_data->xboxApiToken.token).toUtf8());
AuthRequest *requestor = new AuthRequest(this);
connect(requestor, &AuthRequest::finished, this, &AuthContext::onXBoxProfileDone);
requestor->get(request);
qDebug() << "Getting Xbox profile...";
}
void AuthContext::onXBoxProfileDone(
QNetworkReply::NetworkError error,
QByteArray replyData,
QList<QNetworkReply::RawHeaderPair> headers
) {
if (error != QNetworkReply::NoError) {
qWarning() << "Reply error:" << error;
#ifndef NDEBUG
qDebug() << replyData;
#endif
failResult(m_xboxProfileSucceeded);
return;
}
#ifndef NDEBUG
qDebug() << "XBox profile: " << replyData;
#endif
succeedResult(m_xboxProfileSucceeded);
}
void AuthContext::succeedResult(bool& flag) {
m_requestsDone ++;
flag = true;
checkResult();
}
void AuthContext::failResult(bool& flag) {
m_requestsDone ++;
flag = false;
checkResult();
}
void AuthContext::checkResult() {
qDebug() << "AuthContext::checkResult called";
if(m_requestsDone != 2) {
qDebug() << "Number of ready results:" << m_requestsDone;
return;
}
if(m_mcAuthSucceeded && m_xboxProfileSucceeded) {
doEntitlements();
}
else {
finishActivity();
if(stsFailed) {
if(stsErrors.contains(2148916233)) {
changeState(
STATE_FAILED_HARD,
tr("This Microsoft account does not have an XBox Live profile. Buy the game on %1 first.")
.arg("<a href=\"https://www.minecraft.net/en-us/store/minecraft-java-edition\">minecraft.net</a>")
);
}
else if (stsErrors.contains(2148916235)){
// NOTE: this is the Grulovia error
changeState(
STATE_FAILED_HARD,
tr("XBox Live is not available in your country. You've been blocked.")
);
}
else if (stsErrors.contains(2148916238)){
changeState(
STATE_FAILED_HARD,
tr("This Microsoft account is underaged and is not linked to a family.\n\nPlease set up your account according to %1.")
.arg("<a href=\"https://help.minecraft.net/hc/en-us/articles/4403181904525\">help.minecraft.net</a>")
);
}
else {
QStringList errorList;
for(auto & error: stsErrors) {
errorList.append(QString::number(error));
}
changeState(
STATE_FAILED_HARD,
tr("XSTS authentication ended with unrecognized error(s):\n\n%1").arg(errorList.join("\n"))
);
}
}
else {
changeState(STATE_FAILED_HARD, tr("XBox and/or Mojang authentication steps did not succeed"));
}
}
}
void AuthContext::doEntitlements() {
auto uuid = QUuid::createUuid();
entitlementsRequestId = uuid.toString().remove('{').remove('}');
auto url = "https://api.minecraftservices.com/entitlements/license?requestId=" + entitlementsRequestId;
QNetworkRequest request = QNetworkRequest(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Accept", "application/json");
request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8());
AuthRequest *requestor = new AuthRequest(this);
connect(requestor, &AuthRequest::finished, this, &AuthContext::onEntitlementsDone);
requestor->get(request);
qDebug() << "Getting Xbox profile...";
}
void AuthContext::onEntitlementsDone(
QNetworkReply::NetworkError error,
QByteArray data,
QList<QNetworkReply::RawHeaderPair> headers
) {
#ifndef NDEBUG
qDebug() << data;
#endif
// TODO: check presence of same entitlementsRequestId?
// TODO: validate JWTs?
Parsers::parseMinecraftEntitlements(data, m_data->minecraftEntitlement);
doMinecraftProfile();
}
void AuthContext::doMinecraftProfile() {
setStage(AuthStage::MinecraftProfile);
changeState(STATE_WORKING, tr("Starting minecraft profile acquisition"));
auto url = QUrl("https://api.minecraftservices.com/minecraft/profile");
QNetworkRequest request = QNetworkRequest(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
// request.setRawHeader("Accept", "application/json");
request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8());
AuthRequest *requestor = new AuthRequest(this);
connect(requestor, &AuthRequest::finished, this, &AuthContext::onMinecraftProfileDone);
requestor->get(request);
}
void AuthContext::onMinecraftProfileDone(
QNetworkReply::NetworkError error,
QByteArray data,
QList<QNetworkReply::RawHeaderPair> headers
) {
#ifndef NDEBUG
qDebug() << data;
#endif
if (error == QNetworkReply::ContentNotFoundError) {
// NOTE: Succeed even if we do not have a profile. This is a valid account state.
if(m_data->type == AccountType::Mojang) {
m_data->minecraftEntitlement.canPlayMinecraft = false;
m_data->minecraftEntitlement.ownsMinecraft = false;
}
m_data->minecraftProfile = MinecraftProfile();
succeed();
return;
}
if (error != QNetworkReply::NoError) {
finishActivity();
changeState(STATE_FAILED_HARD, tr("Minecraft Java profile acquisition failed."));
return;
}
if(!Parsers::parseMinecraftProfile(data, m_data->minecraftProfile)) {
m_data->minecraftProfile = MinecraftProfile();
finishActivity();
changeState(STATE_FAILED_HARD, tr("Minecraft Java profile response could not be parsed"));
return;
}
if(m_data->type == AccountType::Mojang) {
auto validProfile = m_data->minecraftProfile.validity == Katabasis::Validity::Certain;
m_data->minecraftEntitlement.canPlayMinecraft = validProfile;
m_data->minecraftEntitlement.ownsMinecraft = validProfile;
doMigrationEligibilityCheck();
}
else {
doGetSkin();
}
}
void AuthContext::doMigrationEligibilityCheck() {
setStage(AuthStage::MigrationEligibility);
changeState(STATE_WORKING, tr("Starting check for migration eligibility"));
auto url = QUrl("https://api.minecraftservices.com/rollout/v1/msamigration");
QNetworkRequest request = QNetworkRequest(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8());
AuthRequest *requestor = new AuthRequest(this);
connect(requestor, &AuthRequest::finished, this, &AuthContext::onMigrationEligibilityCheckDone);
requestor->get(request);
}
void AuthContext::onMigrationEligibilityCheckDone(
QNetworkReply::NetworkError error,
QByteArray data,
QList<QNetworkReply::RawHeaderPair> headers
) {
if (error == QNetworkReply::NoError) {
Parsers::parseRolloutResponse(data, m_data->canMigrateToMSA);
}
doGetSkin();
}
void AuthContext::doGetSkin() {
setStage(AuthStage::Skin);
changeState(STATE_WORKING, tr("Fetching player skin"));
auto url = QUrl(m_data->minecraftProfile.skin.url);
QNetworkRequest request = QNetworkRequest(url);
AuthRequest *requestor = new AuthRequest(this);
connect(requestor, &AuthRequest::finished, this, &AuthContext::onSkinDone);
requestor->get(request);
}
void AuthContext::onSkinDone(
QNetworkReply::NetworkError error,
QByteArray data,
QList<QNetworkReply::RawHeaderPair>
) {
if (error == QNetworkReply::NoError) {
m_data->minecraftProfile.skin.data = data;
}
succeed();
}
void AuthContext::succeed() {
m_data->validity_ = Katabasis::Validity::Certain;
finishActivity();
changeState(STATE_SUCCEEDED, tr("Finished all authentication steps"));
}
void AuthContext::setStage(AuthContext::AuthStage stage) {
m_stage = stage;
emit progress((int)m_stage, (int)AuthStage::Complete);
}
QString AuthContext::getStateMessage() const {
switch (m_accountState)
{
case STATE_WORKING:
switch(m_stage) {
case AuthStage::Initial: {
QString loginMessage = tr("Logging in as %1 user");
if(m_data->type == AccountType::MSA) {
return loginMessage.arg("Microsoft");
}
else {
return loginMessage.arg("Mojang");
}
}
case AuthStage::UserAuth:
return tr("Logging in as XBox user");
case AuthStage::XboxAuth:
return tr("Logging in with XBox and Mojang services");
case AuthStage::MinecraftProfile:
return tr("Getting Minecraft profile");
case AuthStage::MigrationEligibility:
return tr("Checking for migration eligibility");
case AuthStage::Skin:
return tr("Getting Minecraft skin");
case AuthStage::Complete:
return tr("Finished");
default:
break;
}
default:
return AccountTask::getStateMessage();
}
}

View File

@ -1,110 +0,0 @@
#pragma once
#include <QObject>
#include <QList>
#include <QVector>
#include <QSet>
#include <QNetworkReply>
#include <QImage>
#include <katabasis/DeviceFlow.h>
#include "Yggdrasil.h"
#include "../AccountData.h"
#include "../AccountTask.h"
class AuthContext : public AccountTask
{
Q_OBJECT
public:
explicit AuthContext(AccountData * data, QObject *parent = 0);
bool isBusy() {
return m_activity != Katabasis::Activity::Idle;
};
Katabasis::Validity validity() {
return m_data->validity_;
};
//bool signOut();
QString getStateMessage() const override;
signals:
void activityChanged(Katabasis::Activity activity);
private slots:
// OAuth-specific callbacks
void onOAuthActivityChanged(Katabasis::Activity activity);
// Yggdrasil specific callbacks
void onMojangSucceeded();
void onMojangFailed();
protected:
void initMSA();
void initMojang();
void doUserAuth();
Q_SLOT void onUserAuthDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
void processSTSError(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
void doSTSAuthMinecraft();
Q_SLOT void onSTSAuthMinecraftDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
void doMinecraftAuth();
Q_SLOT void onMinecraftAuthDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
void doSTSAuthGeneric();
Q_SLOT void onSTSAuthGenericDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
void doXBoxProfile();
Q_SLOT void onXBoxProfileDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
void doEntitlements();
Q_SLOT void onEntitlementsDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
void doMinecraftProfile();
Q_SLOT void onMinecraftProfileDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
void doMigrationEligibilityCheck();
Q_SLOT void onMigrationEligibilityCheckDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
void doGetSkin();
Q_SLOT void onSkinDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
void succeed();
void failResult(bool & flag);
void succeedResult(bool & flag);
void checkResult();
protected:
void beginActivity(Katabasis::Activity activity);
void finishActivity();
void clearTokens();
protected:
Katabasis::DeviceFlow *m_oauth2 = nullptr;
Yggdrasil *m_yggdrasil = nullptr;
int m_requestsDone = 0;
bool m_xboxProfileSucceeded = false;
bool m_mcAuthSucceeded = false;
QString entitlementsRequestId;
QSet<int64_t> stsErrors;
bool stsFailed = false;
Katabasis::Activity m_activity = Katabasis::Activity::Idle;
enum class AuthStage {
Initial,
UserAuth,
XboxAuth,
MinecraftProfile,
MigrationEligibility,
Skin,
Complete
} m_stage = AuthStage::Initial;
void setStage(AuthStage stage);
};

View File

@ -0,0 +1,71 @@
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QDebug>
#include "AuthFlow.h"
#include "katabasis/Globals.h"
#include <Application.h>
AuthFlow::AuthFlow(AccountData * data, QObject *parent) :
AccountTask(data, parent)
{
}
void AuthFlow::succeed() {
m_data->validity_ = Katabasis::Validity::Certain;
changeState(
AccountTaskState::STATE_SUCCEEDED,
tr("Finished all authentication steps")
);
}
void AuthFlow::executeTask() {
if(m_currentStep) {
return;
}
changeState(AccountTaskState::STATE_WORKING, tr("Initializing"));
nextStep();
}
void AuthFlow::nextStep() {
if(m_steps.size() == 0) {
// we got to the end without an incident... assume this is all.
m_currentStep.reset();
succeed();
return;
}
m_currentStep = m_steps.front();
qDebug() << "AuthFlow:" << m_currentStep->describe();
m_steps.pop_front();
connect(m_currentStep.get(), &AuthStep::finished, this, &AuthFlow::stepFinished);
connect(m_currentStep.get(), &AuthStep::showVerificationUriAndCode, this, &AuthFlow::showVerificationUriAndCode);
connect(m_currentStep.get(), &AuthStep::hideVerificationUriAndCode, this, &AuthFlow::hideVerificationUriAndCode);
m_currentStep->perform();
}
QString AuthFlow::getStateMessage() const {
switch (m_taskState)
{
case AccountTaskState::STATE_WORKING: {
if(m_currentStep) {
return m_currentStep->describe();
}
else {
return tr("Working...");
}
}
default: {
return AccountTask::getStateMessage();
}
}
}
void AuthFlow::stepFinished(AccountTaskState resultingState, QString message) {
if(changeState(resultingState, message)) {
nextStep();
}
}

View File

@ -0,0 +1,45 @@
#pragma once
#include <QObject>
#include <QList>
#include <QVector>
#include <QSet>
#include <QNetworkReply>
#include <QImage>
#include <katabasis/DeviceFlow.h>
#include "minecraft/auth/Yggdrasil.h"
#include "minecraft/auth/AccountData.h"
#include "minecraft/auth/AccountTask.h"
#include "minecraft/auth/AuthStep.h"
class AuthFlow : public AccountTask
{
Q_OBJECT
public:
explicit AuthFlow(AccountData * data, QObject *parent = 0);
Katabasis::Validity validity() {
return m_data->validity_;
};
QString getStateMessage() const override;
void executeTask() override;
signals:
void activityChanged(Katabasis::Activity activity);
private slots:
void stepFinished(AccountTaskState resultingState, QString message);
protected:
void succeed();
void nextStep();
protected:
QList<AuthStep::Ptr> m_steps;
AuthStep::Ptr m_currentStep;
};

View File

@ -0,0 +1,37 @@
#include "MSA.h"
#include "minecraft/auth/steps/MSAStep.h"
#include "minecraft/auth/steps/XboxUserStep.h"
#include "minecraft/auth/steps/XboxAuthorizationStep.h"
#include "minecraft/auth/steps/LauncherLoginStep.h"
#include "minecraft/auth/steps/XboxProfileStep.h"
#include "minecraft/auth/steps/EntitlementsStep.h"
#include "minecraft/auth/steps/MinecraftProfileStep.h"
#include "minecraft/auth/steps/GetSkinStep.h"
MSASilent::MSASilent(AccountData* data, QObject* parent) : AuthFlow(data, parent) {
m_steps.append(new MSAStep(m_data, MSAStep::Action::Refresh));
m_steps.append(new XboxUserStep(m_data));
m_steps.append(new XboxAuthorizationStep(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox"));
m_steps.append(new XboxAuthorizationStep(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang"));
m_steps.append(new LauncherLoginStep(m_data));
m_steps.append(new XboxProfileStep(m_data));
m_steps.append(new EntitlementsStep(m_data));
m_steps.append(new MinecraftProfileStep(m_data));
m_steps.append(new GetSkinStep(m_data));
}
MSAInteractive::MSAInteractive(
AccountData* data,
QObject* parent
) : AuthFlow(data, parent) {
m_steps.append(new MSAStep(m_data, MSAStep::Action::Login));
m_steps.append(new XboxUserStep(m_data));
m_steps.append(new XboxAuthorizationStep(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox"));
m_steps.append(new XboxAuthorizationStep(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang"));
m_steps.append(new LauncherLoginStep(m_data));
m_steps.append(new XboxProfileStep(m_data));
m_steps.append(new EntitlementsStep(m_data));
m_steps.append(new MinecraftProfileStep(m_data));
m_steps.append(new GetSkinStep(m_data));
}

View File

@ -0,0 +1,22 @@
#pragma once
#include "AuthFlow.h"
class MSAInteractive : public AuthFlow
{
Q_OBJECT
public:
explicit MSAInteractive(
AccountData *data,
QObject *parent = 0
);
};
class MSASilent : public AuthFlow
{
Q_OBJECT
public:
explicit MSASilent(
AccountData * data,
QObject *parent = 0
);
};

View File

@ -1,22 +0,0 @@
#include "MSAInteractive.h"
MSAInteractive::MSAInteractive(
AccountData* data,
QObject* parent
) : AuthContext(data, parent) {}
void MSAInteractive::executeTask() {
m_requestsDone = 0;
m_xboxProfileSucceeded = false;
m_mcAuthSucceeded = false;
initMSA();
QVariantMap extraOpts;
extraOpts["prompt"] = "select_account";
m_oauth2->setExtraRequestParams(extraOpts);
beginActivity(Katabasis::Activity::LoggingIn);
*m_data = AccountData();
m_oauth2->login();
}

View File

@ -1,13 +0,0 @@
#pragma once
#include "AuthContext.h"
class MSAInteractive : public AuthContext
{
Q_OBJECT
public:
explicit MSAInteractive(
AccountData *data,
QObject *parent = 0
);
void executeTask() override;
};

View File

@ -1,16 +0,0 @@
#include "MSASilent.h"
MSASilent::MSASilent(AccountData* data, QObject* parent) : AuthContext(data, parent) {}
void MSASilent::executeTask() {
m_requestsDone = 0;
m_xboxProfileSucceeded = false;
m_mcAuthSucceeded = false;
initMSA();
beginActivity(Katabasis::Activity::Refreshing);
if(!m_oauth2->refresh()) {
finishActivity();
}
}

View File

@ -1,13 +0,0 @@
#pragma once
#include "AuthContext.h"
class MSASilent : public AuthContext
{
Q_OBJECT
public:
explicit MSASilent(
AccountData * data,
QObject *parent = 0
);
void executeTask() override;
};

View File

@ -0,0 +1,27 @@
#include "Mojang.h"
#include "minecraft/auth/steps/YggdrasilStep.h"
#include "minecraft/auth/steps/MinecraftProfileStep.h"
#include "minecraft/auth/steps/MigrationEligibilityStep.h"
#include "minecraft/auth/steps/GetSkinStep.h"
MojangRefresh::MojangRefresh(
AccountData *data,
QObject *parent
) : AuthFlow(data, parent) {
m_steps.append(new YggdrasilStep(m_data, QString()));
m_steps.append(new MinecraftProfileStep(m_data));
m_steps.append(new MigrationEligibilityStep(m_data));
m_steps.append(new GetSkinStep(m_data));
}
MojangLogin::MojangLogin(
AccountData *data,
QString password,
QObject *parent
): AuthFlow(data, parent), m_password(password) {
m_steps.append(new YggdrasilStep(m_data, m_password));
m_steps.append(new MinecraftProfileStep(m_data));
m_steps.append(new MigrationEligibilityStep(m_data));
m_steps.append(new GetSkinStep(m_data));
}

View File

@ -0,0 +1,26 @@
#pragma once
#include "AuthFlow.h"
class MojangRefresh : public AuthFlow
{
Q_OBJECT
public:
explicit MojangRefresh(
AccountData *data,
QObject *parent = 0
);
};
class MojangLogin : public AuthFlow
{
Q_OBJECT
public:
explicit MojangLogin(
AccountData *data,
QString password,
QObject *parent = 0
);
private:
QString m_password;
};

View File

@ -1,18 +0,0 @@
#include "MojangLogin.h"
MojangLogin::MojangLogin(
AccountData *data,
QString password,
QObject *parent
): AuthContext(data, parent), m_password(password) {}
void MojangLogin::executeTask() {
m_requestsDone = 0;
m_xboxProfileSucceeded = false;
m_mcAuthSucceeded = false;
initMojang();
beginActivity(Katabasis::Activity::LoggingIn);
m_yggdrasil->login(m_password);
}

View File

@ -1,17 +0,0 @@
#pragma once
#include "AuthContext.h"
class MojangLogin : public AuthContext
{
Q_OBJECT
public:
explicit MojangLogin(
AccountData *data,
QString password,
QObject *parent = 0
);
void executeTask() override;
private:
QString m_password;
};

View File

@ -1,17 +0,0 @@
#include "MojangRefresh.h"
MojangRefresh::MojangRefresh(
AccountData *data,
QObject *parent
) : AuthContext(data, parent) {}
void MojangRefresh::executeTask() {
m_requestsDone = 0;
m_xboxProfileSucceeded = false;
m_mcAuthSucceeded = false;
initMojang();
beginActivity(Katabasis::Activity::Refreshing);
m_yggdrasil->refresh();
}

View File

@ -1,10 +0,0 @@
#pragma once
#include "AuthContext.h"
class MojangRefresh : public AuthContext
{
Q_OBJECT
public:
explicit MojangRefresh(AccountData *data, QObject *parent = 0);
void executeTask() override;
};

View File

@ -0,0 +1,53 @@
#include "EntitlementsStep.h"
#include <QNetworkRequest>
#include <QUuid>
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
EntitlementsStep::EntitlementsStep(AccountData* data) : AuthStep(data) {}
EntitlementsStep::~EntitlementsStep() noexcept = default;
QString EntitlementsStep::describe() {
return tr("Determining game ownership.");
}
void EntitlementsStep::perform() {
auto uuid = QUuid::createUuid();
m_entitlementsRequestId = uuid.toString().remove('{').remove('}');
auto url = "https://api.minecraftservices.com/entitlements/license?requestId=" + m_entitlementsRequestId;
QNetworkRequest request = QNetworkRequest(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Accept", "application/json");
request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8());
AuthRequest *requestor = new AuthRequest(this);
connect(requestor, &AuthRequest::finished, this, &EntitlementsStep::onRequestDone);
requestor->get(request);
qDebug() << "Getting entitlements...";
}
void EntitlementsStep::rehydrate() {
// NOOP, for now. We only save bools and there's nothing to check.
}
void EntitlementsStep::onRequestDone(
QNetworkReply::NetworkError error,
QByteArray data,
QList<QNetworkReply::RawHeaderPair> headers
) {
auto requestor = qobject_cast<AuthRequest *>(QObject::sender());
requestor->deleteLater();
#ifndef NDEBUG
qDebug() << data;
#endif
// TODO: check presence of same entitlementsRequestId?
// TODO: validate JWTs?
Parsers::parseMinecraftEntitlements(data, m_data->minecraftEntitlement);
emit finished(AccountTaskState::STATE_WORKING, tr("Got entitlements"));
}

View File

@ -0,0 +1,25 @@
#pragma once
#include <QObject>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h"
class EntitlementsStep : public AuthStep {
Q_OBJECT
public:
explicit EntitlementsStep(AccountData *data);
virtual ~EntitlementsStep() noexcept;
void perform() override;
void rehydrate() override;
QString describe() override;
private slots:
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
private:
QString m_entitlementsRequestId;
};

View File

@ -0,0 +1,43 @@
#include "GetSkinStep.h"
#include <QNetworkRequest>
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
GetSkinStep::GetSkinStep(AccountData* data) : AuthStep(data) {
}
GetSkinStep::~GetSkinStep() noexcept = default;
QString GetSkinStep::describe() {
return tr("Getting skin.");
}
void GetSkinStep::perform() {
auto url = QUrl(m_data->minecraftProfile.skin.url);
QNetworkRequest request = QNetworkRequest(url);
AuthRequest *requestor = new AuthRequest(this);
connect(requestor, &AuthRequest::finished, this, &GetSkinStep::onRequestDone);
requestor->get(request);
}
void GetSkinStep::rehydrate() {
// NOOP, for now.
}
void GetSkinStep::onRequestDone(
QNetworkReply::NetworkError error,
QByteArray data,
QList<QNetworkReply::RawHeaderPair> headers
) {
auto requestor = qobject_cast<AuthRequest *>(QObject::sender());
requestor->deleteLater();
if (error == QNetworkReply::NoError) {
m_data->minecraftProfile.skin.data = data;
}
emit finished(AccountTaskState::STATE_SUCCEEDED, tr("Got skin"));
}

View File

@ -0,0 +1,22 @@
#pragma once
#include <QObject>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h"
class GetSkinStep : public AuthStep {
Q_OBJECT
public:
explicit GetSkinStep(AccountData *data);
virtual ~GetSkinStep() noexcept;
void perform() override;
void rehydrate() override;
QString describe() override;
private slots:
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
};

View File

@ -0,0 +1,78 @@
#include "LauncherLoginStep.h"
#include <QNetworkRequest>
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
#include "minecraft/auth/AccountTask.h"
LauncherLoginStep::LauncherLoginStep(AccountData* data) : AuthStep(data) {
}
LauncherLoginStep::~LauncherLoginStep() noexcept = default;
QString LauncherLoginStep::describe() {
return tr("Accessing Mojang services.");
}
void LauncherLoginStep::perform() {
auto requestURL = "https://api.minecraftservices.com/launcher/login";
auto uhs = m_data->mojangservicesToken.extra["uhs"].toString();
auto xToken = m_data->mojangservicesToken.token;
QString mc_auth_template = R"XXX(
{
"xtoken": "XBL3.0 x=%1;%2",
"platform": "PC_LAUNCHER"
}
)XXX";
auto requestBody = mc_auth_template.arg(uhs, xToken);
QNetworkRequest request = QNetworkRequest(QUrl(requestURL));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Accept", "application/json");
AuthRequest *requestor = new AuthRequest(this);
connect(requestor, &AuthRequest::finished, this, &LauncherLoginStep::onRequestDone);
requestor->post(request, requestBody.toUtf8());
qDebug() << "Getting Minecraft access token...";
}
void LauncherLoginStep::rehydrate() {
// TODO: check the token validity
}
void LauncherLoginStep::onRequestDone(
QNetworkReply::NetworkError error,
QByteArray data,
QList<QNetworkReply::RawHeaderPair> headers
) {
auto requestor = qobject_cast<AuthRequest *>(QObject::sender());
requestor->deleteLater();
qDebug() << data;
if (error != QNetworkReply::NoError) {
qWarning() << "Reply error:" << error;
#ifndef NDEBUG
qDebug() << data;
#endif
emit finished(
AccountTaskState::STATE_FAILED_SOFT,
tr("Failed to get Minecraft access token: %1").arg(requestor->errorString_)
);
return;
}
if(!Parsers::parseMojangResponse(data, m_data->yggdrasilToken)) {
qWarning() << "Could not parse login_with_xbox response...";
#ifndef NDEBUG
qDebug() << data;
#endif
emit finished(
AccountTaskState::STATE_FAILED_SOFT,
tr("Failed to parse the Minecraft access token response.")
);
return;
}
emit finished(AccountTaskState::STATE_WORKING, tr(""));
}

View File

@ -0,0 +1,22 @@
#pragma once
#include <QObject>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h"
class LauncherLoginStep : public AuthStep {
Q_OBJECT
public:
explicit LauncherLoginStep(AccountData *data);
virtual ~LauncherLoginStep() noexcept;
void perform() override;
void rehydrate() override;
QString describe() override;
private slots:
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
};

View File

@ -0,0 +1,111 @@
#include "MSAStep.h"
#include <QNetworkRequest>
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
#include "Application.h"
using OAuth2 = Katabasis::DeviceFlow;
using Activity = Katabasis::Activity;
MSAStep::MSAStep(AccountData* data, Action action) : AuthStep(data), m_action(action) {
OAuth2::Options opts;
opts.scope = "XboxLive.signin offline_access";
opts.clientIdentifier = APPLICATION->msaClientId();
opts.authorizationUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode";
opts.accessTokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
// FIXME: OAuth2 is not aware of our fancy shared pointers
m_oauth2 = new OAuth2(opts, m_data->msaToken, this, APPLICATION->network().get());
connect(m_oauth2, &OAuth2::activityChanged, this, &MSAStep::onOAuthActivityChanged);
connect(m_oauth2, &OAuth2::showVerificationUriAndCode, this, &MSAStep::showVerificationUriAndCode);
}
MSAStep::~MSAStep() noexcept = default;
QString MSAStep::describe() {
return tr("Logging in with Microsoft account.");
}
void MSAStep::rehydrate() {
switch(m_action) {
case Refresh: {
// TODO: check the tokens and see if they are old (older than a day)
return;
}
case Login: {
// NOOP
return;
}
}
}
void MSAStep::perform() {
switch(m_action) {
case Refresh: {
m_oauth2->refresh();
return;
}
case Login: {
QVariantMap extraOpts;
extraOpts["prompt"] = "select_account";
m_oauth2->setExtraRequestParams(extraOpts);
*m_data = AccountData();
m_oauth2->login();
return;
}
}
}
void MSAStep::onOAuthActivityChanged(Katabasis::Activity activity) {
switch(activity) {
case Katabasis::Activity::Idle:
case Katabasis::Activity::LoggingIn:
case Katabasis::Activity::Refreshing:
case Katabasis::Activity::LoggingOut: {
// We asked it to do something, it's doing it. Nothing to act upon.
return;
}
case Katabasis::Activity::Succeeded: {
// Succeeded or did not invalidate tokens
emit hideVerificationUriAndCode();
QVariantMap extraTokens = m_oauth2->extraTokens();
#ifndef NDEBUG
if (!extraTokens.isEmpty()) {
qDebug() << "Extra tokens in response:";
foreach (QString key, extraTokens.keys()) {
qDebug() << "\t" << key << ":" << extraTokens.value(key);
}
}
#endif
emit finished(AccountTaskState::STATE_WORKING, tr("Got "));
return;
}
case Katabasis::Activity::FailedSoft: {
// NOTE: soft error in the first step means 'offline'
emit hideVerificationUriAndCode();
emit finished(AccountTaskState::STATE_OFFLINE, tr("Microsoft user authentication ended with a network error."));
return;
}
case Katabasis::Activity::FailedGone: {
emit hideVerificationUriAndCode();
emit finished(AccountTaskState::STATE_FAILED_GONE, tr("Microsoft user authentication failed - user no longer exists."));
return;
}
case Katabasis::Activity::FailedHard: {
emit hideVerificationUriAndCode();
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Microsoft user authentication failed."));
return;
}
default: {
emit hideVerificationUriAndCode();
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Microsoft user authentication completed with an unrecognized result."));
return;
}
}
}

View File

@ -0,0 +1,32 @@
#pragma once
#include <QObject>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h"
#include <katabasis/DeviceFlow.h>
class MSAStep : public AuthStep {
Q_OBJECT
public:
enum Action {
Refresh,
Login
};
public:
explicit MSAStep(AccountData *data, Action action);
virtual ~MSAStep() noexcept;
void perform() override;
void rehydrate() override;
QString describe() override;
private slots:
void onOAuthActivityChanged(Katabasis::Activity activity);
private:
Katabasis::DeviceFlow *m_oauth2 = nullptr;
Action m_action;
};

View File

@ -0,0 +1,45 @@
#include "MigrationEligibilityStep.h"
#include <QNetworkRequest>
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
MigrationEligibilityStep::MigrationEligibilityStep(AccountData* data) : AuthStep(data) {
}
MigrationEligibilityStep::~MigrationEligibilityStep() noexcept = default;
QString MigrationEligibilityStep::describe() {
return tr("Checking for migration eligibility.");
}
void MigrationEligibilityStep::perform() {
auto url = QUrl("https://api.minecraftservices.com/rollout/v1/msamigration");
QNetworkRequest request = QNetworkRequest(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8());
AuthRequest *requestor = new AuthRequest(this);
connect(requestor, &AuthRequest::finished, this, &MigrationEligibilityStep::onRequestDone);
requestor->get(request);
}
void MigrationEligibilityStep::rehydrate() {
// NOOP, for now. We only save bools and there's nothing to check.
}
void MigrationEligibilityStep::onRequestDone(
QNetworkReply::NetworkError error,
QByteArray data,
QList<QNetworkReply::RawHeaderPair> headers
) {
auto requestor = qobject_cast<AuthRequest *>(QObject::sender());
requestor->deleteLater();
if (error == QNetworkReply::NoError) {
Parsers::parseRolloutResponse(data, m_data->canMigrateToMSA);
}
emit finished(AccountTaskState::STATE_WORKING, tr("Got migration flags"));
}

View File

@ -0,0 +1,22 @@
#pragma once
#include <QObject>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h"
class MigrationEligibilityStep : public AuthStep {
Q_OBJECT
public:
explicit MigrationEligibilityStep(AccountData *data);
virtual ~MigrationEligibilityStep() noexcept;
void perform() override;
void rehydrate() override;
QString describe() override;
private slots:
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
};

View File

@ -0,0 +1,83 @@
#include "MinecraftProfileStep.h"
#include <QNetworkRequest>
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
MinecraftProfileStep::MinecraftProfileStep(AccountData* data) : AuthStep(data) {
}
MinecraftProfileStep::~MinecraftProfileStep() noexcept = default;
QString MinecraftProfileStep::describe() {
return tr("Fetching the Minecraft profile.");
}
void MinecraftProfileStep::perform() {
auto url = QUrl("https://api.minecraftservices.com/minecraft/profile");
QNetworkRequest request = QNetworkRequest(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8());
AuthRequest *requestor = new AuthRequest(this);
connect(requestor, &AuthRequest::finished, this, &MinecraftProfileStep::onRequestDone);
requestor->get(request);
}
void MinecraftProfileStep::rehydrate() {
// NOOP, for now. We only save bools and there's nothing to check.
}
void MinecraftProfileStep::onRequestDone(
QNetworkReply::NetworkError error,
QByteArray data,
QList<QNetworkReply::RawHeaderPair> headers
) {
auto requestor = qobject_cast<AuthRequest *>(QObject::sender());
requestor->deleteLater();
#ifndef NDEBUG
qDebug() << data;
#endif
if (error == QNetworkReply::ContentNotFoundError) {
// NOTE: Succeed even if we do not have a profile. This is a valid account state.
if(m_data->type == AccountType::Mojang) {
m_data->minecraftEntitlement.canPlayMinecraft = false;
m_data->minecraftEntitlement.ownsMinecraft = false;
}
m_data->minecraftProfile = MinecraftProfile();
emit finished(
AccountTaskState::STATE_SUCCEEDED,
tr("Account has no Minecraft profile.")
);
return;
}
if (error != QNetworkReply::NoError) {
emit finished(
AccountTaskState::STATE_FAILED_SOFT,
tr("Minecraft Java profile acquisition failed.")
);
return;
}
if(!Parsers::parseMinecraftProfile(data, m_data->minecraftProfile)) {
m_data->minecraftProfile = MinecraftProfile();
emit finished(
AccountTaskState::STATE_FAILED_SOFT,
tr("Minecraft Java profile response could not be parsed")
);
return;
}
if(m_data->type == AccountType::Mojang) {
auto validProfile = m_data->minecraftProfile.validity == Katabasis::Validity::Certain;
m_data->minecraftEntitlement.canPlayMinecraft = validProfile;
m_data->minecraftEntitlement.ownsMinecraft = validProfile;
}
emit finished(
AccountTaskState::STATE_WORKING,
tr("Minecraft Java profile acquisition succeeded.")
);
}

View File

@ -0,0 +1,22 @@
#pragma once
#include <QObject>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h"
class MinecraftProfileStep : public AuthStep {
Q_OBJECT
public:
explicit MinecraftProfileStep(AccountData *data);
virtual ~MinecraftProfileStep() noexcept;
void perform() override;
void rehydrate() override;
QString describe() override;
private slots:
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
};

View File

@ -0,0 +1,158 @@
#include "XboxAuthorizationStep.h"
#include <QNetworkRequest>
#include <QJsonParseError>
#include <QJsonDocument>
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
XboxAuthorizationStep::XboxAuthorizationStep(AccountData* data, Katabasis::Token *token, QString relyingParty, QString authorizationKind):
AuthStep(data),
m_token(token),
m_relyingParty(relyingParty),
m_authorizationKind(authorizationKind)
{
}
XboxAuthorizationStep::~XboxAuthorizationStep() noexcept = default;
QString XboxAuthorizationStep::describe() {
return tr("Getting authorization to access %1 services.").arg(m_authorizationKind);
}
void XboxAuthorizationStep::rehydrate() {
// FIXME: check if the tokens are good?
}
void XboxAuthorizationStep::perform() {
QString xbox_auth_template = R"XXX(
{
"Properties": {
"SandboxId": "RETAIL",
"UserTokens": [
"%1"
]
},
"RelyingParty": "%2",
"TokenType": "JWT"
}
)XXX";
auto xbox_auth_data = xbox_auth_template.arg(m_data->userToken.token, m_relyingParty);
// http://xboxlive.com
QNetworkRequest request = QNetworkRequest(QUrl("https://xsts.auth.xboxlive.com/xsts/authorize"));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Accept", "application/json");
AuthRequest *requestor = new AuthRequest(this);
connect(requestor, &AuthRequest::finished, this, &XboxAuthorizationStep::onRequestDone);
requestor->post(request, xbox_auth_data.toUtf8());
qDebug() << "Getting authorization token for " << m_relyingParty;
}
void XboxAuthorizationStep::onRequestDone(
QNetworkReply::NetworkError error,
QByteArray data,
QList<QNetworkReply::RawHeaderPair> headers
) {
auto requestor = qobject_cast<AuthRequest *>(QObject::sender());
requestor->deleteLater();
#ifndef NDEBUG
qDebug() << data;
#endif
if (error != QNetworkReply::NoError) {
qWarning() << "Reply error:" << error;
if(!processSTSError(error, data, headers)) {
emit finished(
AccountTaskState::STATE_FAILED_SOFT,
tr("Failed to get authorization for %1 services. Error %1.").arg(m_authorizationKind, error)
);
}
return;
}
Katabasis::Token temp;
if(!Parsers::parseXTokenResponse(data, temp, m_authorizationKind)) {
emit finished(
AccountTaskState::STATE_FAILED_SOFT,
tr("Could not parse authorization response for access to %1 services.").arg(m_authorizationKind)
);
return;
}
if(temp.extra["uhs"] != m_data->userToken.extra["uhs"]) {
emit finished(
AccountTaskState::STATE_FAILED_SOFT,
tr("Server has changed %1 authorization user hash in the reply. Something is wrong.").arg(m_authorizationKind)
);
return;
}
auto & token = *m_token;
token = temp;
emit finished(AccountTaskState::STATE_WORKING, tr("Got authorization to access %1").arg(m_relyingParty));
}
bool XboxAuthorizationStep::processSTSError(
QNetworkReply::NetworkError error,
QByteArray data,
QList<QNetworkReply::RawHeaderPair> headers
) {
if(error == QNetworkReply::AuthenticationRequiredError) {
QJsonParseError jsonError;
QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
if(jsonError.error) {
qWarning() << "Cannot parse error XSTS response as JSON: " << jsonError.errorString();
emit finished(
AccountTaskState::STATE_FAILED_SOFT,
tr("Cannot parse %1 authorization error response as JSON: %2").arg(m_authorizationKind, jsonError.errorString())
);
return true;
}
int64_t errorCode = -1;
auto obj = doc.object();
if(!Parsers::getNumber(obj.value("XErr"), errorCode)) {
emit finished(
AccountTaskState::STATE_FAILED_SOFT,
tr("XErr element is missing from %1 authorization error response.").arg(m_authorizationKind)
);
return true;
}
switch(errorCode) {
case 2148916233:{
emit finished(
AccountTaskState::STATE_FAILED_SOFT,
tr("This Microsoft account does not have an XBox Live profile. Buy the game on %1 first.")
.arg("<a href=\"https://www.minecraft.net/en-us/store/minecraft-java-edition\">minecraft.net</a>")
);
return true;
}
case 2148916235: {
// NOTE: this is the Grulovia error
emit finished(
AccountTaskState::STATE_FAILED_SOFT,
tr("XBox Live is not available in your country. You've been blocked.")
);
return true;
}
case 2148916238: {
emit finished(
AccountTaskState::STATE_FAILED_SOFT,
tr("This Microsoft account is underaged and is not linked to a family.\n\nPlease set up your account according to %1.")
.arg("<a href=\"https://help.minecraft.net/hc/en-us/articles/4403181904525\">help.minecraft.net</a>")
);
return true;
}
default: {
emit finished(
AccountTaskState::STATE_FAILED_SOFT,
tr("XSTS authentication ended with unrecognized error(s):\n\n%1").arg(errorCode)
);
return true;
}
}
}
return false;
}

View File

@ -0,0 +1,34 @@
#pragma once
#include <QObject>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h"
class XboxAuthorizationStep : public AuthStep {
Q_OBJECT
public:
explicit XboxAuthorizationStep(AccountData *data, Katabasis::Token *token, QString relyingParty, QString authorizationKind);
virtual ~XboxAuthorizationStep() noexcept;
void perform() override;
void rehydrate() override;
QString describe() override;
private:
bool processSTSError(
QNetworkReply::NetworkError error,
QByteArray data,
QList<QNetworkReply::RawHeaderPair> headers
);
private slots:
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
private:
Katabasis::Token *m_token;
QString m_relyingParty;
QString m_authorizationKind;
};

View File

@ -0,0 +1,73 @@
#include "XboxProfileStep.h"
#include <QNetworkRequest>
#include <QUrlQuery>
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
XboxProfileStep::XboxProfileStep(AccountData* data) : AuthStep(data) {
}
XboxProfileStep::~XboxProfileStep() noexcept = default;
QString XboxProfileStep::describe() {
return tr("Fetching Xbox profile.");
}
void XboxProfileStep::rehydrate() {
// NOOP, for now. We only save bools and there's nothing to check.
}
void XboxProfileStep::perform() {
auto url = QUrl("https://profile.xboxlive.com/users/me/profile/settings");
QUrlQuery q;
q.addQueryItem(
"settings",
"GameDisplayName,AppDisplayName,AppDisplayPicRaw,GameDisplayPicRaw,"
"PublicGamerpic,ShowUserAsAvatar,Gamerscore,Gamertag,ModernGamertag,ModernGamertagSuffix,"
"UniqueModernGamertag,AccountTier,TenureLevel,XboxOneRep,"
"PreferredColor,Location,Bio,Watermarks,"
"RealName,RealNameOverride,IsQuarantined"
);
url.setQuery(q);
QNetworkRequest request = QNetworkRequest(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Accept", "application/json");
request.setRawHeader("x-xbl-contract-version", "3");
request.setRawHeader("Authorization", QString("XBL3.0 x=%1;%2").arg(m_data->userToken.extra["uhs"].toString(), m_data->xboxApiToken.token).toUtf8());
AuthRequest *requestor = new AuthRequest(this);
connect(requestor, &AuthRequest::finished, this, &XboxProfileStep::onRequestDone);
requestor->get(request);
qDebug() << "Getting Xbox profile...";
}
void XboxProfileStep::onRequestDone(
QNetworkReply::NetworkError error,
QByteArray data,
QList<QNetworkReply::RawHeaderPair> headers
) {
auto requestor = qobject_cast<AuthRequest *>(QObject::sender());
requestor->deleteLater();
if (error != QNetworkReply::NoError) {
qWarning() << "Reply error:" << error;
#ifndef NDEBUG
qDebug() << data;
#endif
finished(
AccountTaskState::STATE_FAILED_SOFT,
tr("Failed to retrieve the Xbox profile.")
);
return;
}
#ifndef NDEBUG
qDebug() << "XBox profile: " << data;
#endif
emit finished(AccountTaskState::STATE_WORKING, tr("Got Xbox profile"));
}

View File

@ -0,0 +1,22 @@
#pragma once
#include <QObject>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h"
class XboxProfileStep : public AuthStep {
Q_OBJECT
public:
explicit XboxProfileStep(AccountData *data);
virtual ~XboxProfileStep() noexcept;
void perform() override;
void rehydrate() override;
QString describe() override;
private slots:
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
};

View File

@ -0,0 +1,68 @@
#include "XboxUserStep.h"
#include <QNetworkRequest>
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
XboxUserStep::XboxUserStep(AccountData* data) : AuthStep(data) {
}
XboxUserStep::~XboxUserStep() noexcept = default;
QString XboxUserStep::describe() {
return tr("Logging in as an Xbox user.");
}
void XboxUserStep::rehydrate() {
// NOOP, for now. We only save bools and there's nothing to check.
}
void XboxUserStep::perform() {
QString xbox_auth_template = R"XXX(
{
"Properties": {
"AuthMethod": "RPS",
"SiteName": "user.auth.xboxlive.com",
"RpsTicket": "d=%1"
},
"RelyingParty": "http://auth.xboxlive.com",
"TokenType": "JWT"
}
)XXX";
auto xbox_auth_data = xbox_auth_template.arg(m_data->msaToken.token);
QNetworkRequest request = QNetworkRequest(QUrl("https://user.auth.xboxlive.com/user/authenticate"));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Accept", "application/json");
auto *requestor = new AuthRequest(this);
connect(requestor, &AuthRequest::finished, this, &XboxUserStep::onRequestDone);
requestor->post(request, xbox_auth_data.toUtf8());
qDebug() << "First layer of XBox auth ... commencing.";
}
void XboxUserStep::onRequestDone(
QNetworkReply::NetworkError error,
QByteArray data,
QList<QNetworkReply::RawHeaderPair> headers
) {
auto requestor = qobject_cast<AuthRequest *>(QObject::sender());
requestor->deleteLater();
if (error != QNetworkReply::NoError) {
qWarning() << "Reply error:" << error;
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("XBox user authentication failed."));
return;
}
Katabasis::Token temp;
if(!Parsers::parseXTokenResponse(data, temp, "UToken")) {
qWarning() << "Could not parse user authentication response...";
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("XBox user authentication response could not be understood."));
return;
}
m_data->userToken = temp;
emit finished(AccountTaskState::STATE_WORKING, tr("Got Xbox user token"));
}

View File

@ -0,0 +1,22 @@
#pragma once
#include <QObject>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h"
class XboxUserStep : public AuthStep {
Q_OBJECT
public:
explicit XboxUserStep(AccountData *data);
virtual ~XboxUserStep() noexcept;
void perform() override;
void rehydrate() override;
QString describe() override;
private slots:
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
};

View File

@ -0,0 +1,51 @@
#include "YggdrasilStep.h"
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
#include "minecraft/auth/Yggdrasil.h"
YggdrasilStep::YggdrasilStep(AccountData* data, QString password) : AuthStep(data), m_password(password) {
m_yggdrasil = new Yggdrasil(m_data, this);
connect(m_yggdrasil, &Task::failed, this, &YggdrasilStep::onAuthFailed);
connect(m_yggdrasil, &Task::succeeded, this, &YggdrasilStep::onAuthSucceeded);
}
YggdrasilStep::~YggdrasilStep() noexcept = default;
QString YggdrasilStep::describe() {
return tr("Logging in with Mojang account.");
}
void YggdrasilStep::rehydrate() {
// NOOP, for now.
}
void YggdrasilStep::perform() {
if(m_password.size()) {
m_yggdrasil->login(m_password);
}
else {
m_yggdrasil->refresh();
}
}
void YggdrasilStep::onAuthSucceeded() {
emit finished(AccountTaskState::STATE_WORKING, tr("Logged in with Mojang"));
}
void YggdrasilStep::onAuthFailed() {
// TODO: hook these in again, expand to MSA
// m_error = m_yggdrasil->m_error;
// m_aborted = m_yggdrasil->m_aborted;
auto state = m_yggdrasil->taskState();
QString errorMessage = tr("Mojang user authentication failed.");
// NOTE: soft error in the first step means 'offline'
if(state == AccountTaskState::STATE_FAILED_SOFT) {
state = AccountTaskState::STATE_OFFLINE;
errorMessage = tr("Mojang user authentication ended with a network error.");
}
emit finished(AccountTaskState::STATE_OFFLINE, errorMessage);
}

View File

@ -0,0 +1,28 @@
#pragma once
#include <QObject>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h"
class Yggdrasil;
class YggdrasilStep : public AuthStep {
Q_OBJECT
public:
explicit YggdrasilStep(AccountData *data, QString password);
virtual ~YggdrasilStep() noexcept;
void perform() override;
void rehydrate() override;
QString describe() override;
private slots:
void onAuthSucceeded();
void onAuthFailed();
private:
Yggdrasil *m_yggdrasil = nullptr;
QString m_password;
};

View File

@ -5,15 +5,15 @@
#include "Application.h"
CapeChange::CapeChange(QObject *parent, AuthSessionPtr session, QString cape)
: Task(parent), m_capeId(cape), m_session(session)
CapeChange::CapeChange(QObject *parent, QString token, QString cape)
: Task(parent), m_capeId(cape), m_token(token)
{
}
void CapeChange::setCape(QString& cape) {
QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active"));
auto requestString = QString("{\"capeId\":\"%1\"}").arg(m_capeId);
request.setRawHeader("Authorization", QString("Bearer %1").arg(m_session->access_token).toLocal8Bit());
request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit());
QNetworkReply *rep = APPLICATION->network()->put(request, requestString.toUtf8());
setStatus(tr("Equipping cape"));
@ -27,7 +27,7 @@ void CapeChange::setCape(QString& cape) {
void CapeChange::clearCape() {
QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active"));
auto requestString = QString("{\"capeId\":\"%1\"}").arg(m_capeId);
request.setRawHeader("Authorization", QString("Bearer %1").arg(m_session->access_token).toLocal8Bit());
request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit());
QNetworkReply *rep = APPLICATION->network()->deleteResource(request);
setStatus(tr("Removing cape"));

View File

@ -3,7 +3,6 @@
#include <QFile>
#include <QtNetwork/QtNetwork>
#include <memory>
#include <minecraft/auth/AuthSession.h>
#include "tasks/Task.h"
#include "QObjectPtr.h"
@ -11,7 +10,7 @@ class CapeChange : public Task
{
Q_OBJECT
public:
CapeChange(QObject *parent, AuthSessionPtr session, QString capeId);
CapeChange(QObject *parent, QString token, QString capeId);
virtual ~CapeChange() {}
private:
@ -20,7 +19,7 @@ private:
private:
QString m_capeId;
AuthSessionPtr m_session;
QString m_token;
shared_qobject_ptr<QNetworkReply> m_reply;
protected:

View File

@ -5,15 +5,15 @@
#include "Application.h"
SkinDelete::SkinDelete(QObject *parent, AuthSessionPtr session)
: Task(parent), m_session(session)
SkinDelete::SkinDelete(QObject *parent, QString token)
: Task(parent), m_token(token)
{
}
void SkinDelete::executeTask()
{
QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/skins/active"));
request.setRawHeader("Authorization", QString("Bearer %1").arg(m_session->access_token).toLocal8Bit());
request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit());
QNetworkReply *rep = APPLICATION->network()->deleteResource(request);
m_reply = shared_qobject_ptr<QNetworkReply>(rep);

View File

@ -2,7 +2,6 @@
#include <QFile>
#include <QtNetwork/QtNetwork>
#include <minecraft/auth/AuthSession.h>
#include "tasks/Task.h"
typedef shared_qobject_ptr<class SkinDelete> SkinDeletePtr;
@ -11,11 +10,11 @@ class SkinDelete : public Task
{
Q_OBJECT
public:
SkinDelete(QObject *parent, AuthSessionPtr session);
SkinDelete(QObject *parent, QString token);
virtual ~SkinDelete() = default;
private:
AuthSessionPtr m_session;
QString m_token;
shared_qobject_ptr<QNetworkReply> m_reply;
protected:
@ -25,4 +24,3 @@ public slots:
void downloadError(QNetworkReply::NetworkError);
void downloadFinished();
};

View File

@ -16,15 +16,15 @@ QByteArray getVariant(SkinUpload::Model model) {
}
}
SkinUpload::SkinUpload(QObject *parent, AuthSessionPtr session, QByteArray skin, SkinUpload::Model model)
: Task(parent), m_model(model), m_skin(skin), m_session(session)
SkinUpload::SkinUpload(QObject *parent, QString token, QByteArray skin, SkinUpload::Model model)
: Task(parent), m_model(model), m_skin(skin), m_token(token)
{
}
void SkinUpload::executeTask()
{
QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/skins"));
request.setRawHeader("Authorization", QString("Bearer %1").arg(m_session->access_token).toLocal8Bit());
request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit());
QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
QHttpPart skin;

View File

@ -3,7 +3,6 @@
#include <QFile>
#include <QtNetwork/QtNetwork>
#include <memory>
#include <minecraft/auth/AuthSession.h>
#include "tasks/Task.h"
typedef shared_qobject_ptr<class SkinUpload> SkinUploadPtr;
@ -19,13 +18,13 @@ public:
};
// Note this class takes ownership of the file.
SkinUpload(QObject *parent, AuthSessionPtr session, QByteArray skin, Model model = STEVE);
SkinUpload(QObject *parent, QString token, QByteArray skin, Model model = STEVE);
virtual ~SkinUpload() {}
private:
Model m_model;
QByteArray m_skin;
AuthSessionPtr m_session;
QString m_token;
shared_qobject_ptr<QNetworkReply> m_reply;
protected:
virtual void executeTask();

View File

@ -43,7 +43,7 @@ void LoginDialog::accept()
// Setup the login task and start it
m_account = MinecraftAccount::createFromUsername(ui->userTextBox->text());
m_loginTask = m_account->login(nullptr, ui->passTextBox->text());
m_loginTask = m_account->login(ui->passTextBox->text());
connect(m_loginTask.get(), &Task::failed, this, &LoginDialog::onTaskFailed);
connect(m_loginTask.get(), &Task::succeeded, this, &LoginDialog::onTaskSucceeded);
connect(m_loginTask.get(), &Task::status, this, &LoginDialog::onTaskStatus);

View File

@ -37,7 +37,7 @@ int MSALoginDialog::exec() {
// Setup the login task and start it
m_account = MinecraftAccount::createBlankMSA();
m_loginTask = m_account->loginMSA(nullptr);
m_loginTask = m_account->loginMSA();
connect(m_loginTask.get(), &Task::failed, this, &MSALoginDialog::onTaskFailed);
connect(m_loginTask.get(), &Task::succeeded, this, &MSALoginDialog::onTaskSucceeded);
connect(m_loginTask.get(), &Task::status, this, &MSALoginDialog::onTaskStatus);

View File

@ -25,8 +25,8 @@
#include "ui/dialogs/ProgressDialog.h"
#include <Application.h>
#include "minecraft/auth/flows/AuthRequest.h"
#include "minecraft/auth/flows/Parsers.h"
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
ProfileSetupDialog::ProfileSetupDialog(MinecraftAccountPtr accountToSetup, QWidget *parent)
@ -150,6 +150,9 @@ void ProfileSetupDialog::checkFinished(
QByteArray data,
QList<QNetworkReply::RawHeaderPair> headers
) {
auto requestor = qobject_cast<AuthRequest *>(QObject::sender());
requestor->deleteLater();
if(error == QNetworkReply::NoError) {
auto doc = QJsonDocument::fromJson(data);
auto root = doc.object();
@ -231,6 +234,9 @@ void ProfileSetupDialog::setupProfileFinished(
QByteArray data,
QList<QNetworkReply::RawHeaderPair> headers
) {
auto requestor = qobject_cast<AuthRequest *>(QObject::sender());
requestor->deleteLater();
isWorking = false;
if(error == QNetworkReply::NoError) {
/*

View File

@ -20,16 +20,6 @@ void SkinUploadDialog::on_buttonBox_rejected()
void SkinUploadDialog::on_buttonBox_accepted()
{
AuthSessionPtr session = std::make_shared<AuthSession>();
auto login = m_acct->refresh(session);
ProgressDialog prog(this);
if (prog.execWithTask((Task*)login.get()) != QDialog::Accepted)
{
//FIXME: recover with password prompt
CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Failed to login!"), QMessageBox::Warning)->exec();
close();
return;
}
QString fileName;
QString input = ui->skinPathTextBox->text();
QRegExp urlPrefixMatcher("^([a-z]+)://.+$");
@ -91,11 +81,12 @@ void SkinUploadDialog::on_buttonBox_accepted()
{
model = SkinUpload::ALEX;
}
ProgressDialog prog(this);
SequentialTask skinUpload;
skinUpload.addTask(shared_qobject_ptr<SkinUpload>(new SkinUpload(this, session, FS::read(fileName), model)));
skinUpload.addTask(shared_qobject_ptr<SkinUpload>(new SkinUpload(this, m_acct->accessToken(), FS::read(fileName), model)));
auto selectedCape = ui->capeCombo->currentData().toString();
if(selectedCape != m_acct->accountData()->minecraftProfile.currentCape) {
skinUpload.addTask(shared_qobject_ptr<CapeChange>(new CapeChange(this, session, selectedCape)));
skinUpload.addTask(shared_qobject_ptr<CapeChange>(new CapeChange(this, m_acct->accessToken(), selectedCape)));
}
if (prog.execWithTask(&skinUpload) != QDialog::Accepted)
{

View File

@ -170,13 +170,7 @@ void AccountListPage::on_actionRefresh_triggered() {
if (selection.size() > 0) {
QModelIndex selected = selection.first();
MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value<MinecraftAccountPtr>();
AuthSessionPtr session = std::make_shared<AuthSession>();
auto task = account->refresh(session);
if (task) {
ProgressDialog progDialog(this);
progDialog.execWithTask(task.get());
// TODO: respond to results of the task
}
m_accounts->requestRefresh(account->internalId());
}
}
@ -244,15 +238,9 @@ void AccountListPage::on_actionDeleteSkin_triggered()
return;
QModelIndex selected = selection.first();
AuthSessionPtr session = std::make_shared<AuthSession>();
MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value<MinecraftAccountPtr>();
auto login = account->refresh(session);
ProgressDialog prog(this);
if (prog.execWithTask((Task*)login.get()) != QDialog::Accepted) {
CustomMessageBox::selectable(this, tr("Skin Delete"), tr("Failed to login!"), QMessageBox::Warning)->exec();
return;
}
auto deleteSkinTask = std::make_shared<SkinDelete>(this, session);
auto deleteSkinTask = std::make_shared<SkinDelete>(this, account->accessToken());
if (prog.execWithTask((Task*)deleteSkinTask.get()) != QDialog::Accepted) {
CustomMessageBox::selectable(this, tr("Skin Delete"), tr("Failed to delete current skin!"), QMessageBox::Warning)->exec();
return;

View File

@ -0,0 +1,134 @@
/* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <QMessageBox>
#include <QtGui>
#include "ErrorFrame.h"
#include "ui_ErrorFrame.h"
#include "ui/dialogs/CustomMessageBox.h"
void ErrorFrame::clear()
{
setTitle(QString());
setDescription(QString());
}
ErrorFrame::ErrorFrame(QWidget *parent) :
QFrame(parent),
ui(new Ui::ErrorFrame)
{
ui->setupUi(this);
ui->label_Description->setHidden(true);
ui->label_Title->setHidden(true);
updateHiddenState();
}
ErrorFrame::~ErrorFrame()
{
delete ui;
}
void ErrorFrame::updateHiddenState()
{
if(ui->label_Description->isHidden() && ui->label_Title->isHidden())
{
setHidden(true);
}
else
{
setHidden(false);
}
}
void ErrorFrame::setTitle(QString text)
{
if(text.isEmpty())
{
ui->label_Title->setHidden(true);
}
else
{
ui->label_Title->setText(text);
ui->label_Title->setHidden(false);
}
updateHiddenState();
}
void ErrorFrame::setDescription(QString text)
{
if(text.isEmpty())
{
ui->label_Description->setHidden(true);
updateHiddenState();
return;
}
else
{
ui->label_Description->setHidden(false);
updateHiddenState();
}
ui->label_Description->setToolTip("");
QString intermediatetext = text.trimmed();
bool prev(false);
QChar rem('\n');
QString finaltext;
finaltext.reserve(intermediatetext.size());
foreach(const QChar& c, intermediatetext)
{
if(c == rem && prev){
continue;
}
prev = c == rem;
finaltext += c;
}
QString labeltext;
labeltext.reserve(300);
if(finaltext.length() > 290)
{
ui->label_Description->setOpenExternalLinks(false);
ui->label_Description->setTextFormat(Qt::TextFormat::RichText);
desc = text;
// This allows injecting HTML here.
labeltext.append("<html><body>" + finaltext.left(287) + "<a href=\"#mod_desc\">...</a></body></html>");
QObject::connect(ui->label_Description, &QLabel::linkActivated, this, &ErrorFrame::ellipsisHandler);
}
else
{
ui->label_Description->setTextFormat(Qt::TextFormat::PlainText);
labeltext.append(finaltext);
}
ui->label_Description->setText(labeltext);
}
void ErrorFrame::ellipsisHandler(const QString &link)
{
if(!currentBox)
{
currentBox = CustomMessageBox::selectable(this, QString(), desc);
connect(currentBox, &QMessageBox::finished, this, &ErrorFrame::boxClosed);
currentBox->show();
}
else
{
currentBox->setText(desc);
}
}
void ErrorFrame::boxClosed(int result)
{
currentBox = nullptr;
}

View File

@ -0,0 +1,49 @@
/* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <QFrame>
namespace Ui
{
class ErrorFrame;
}
class ErrorFrame : public QFrame
{
Q_OBJECT
public:
explicit ErrorFrame(QWidget *parent = 0);
~ErrorFrame();
void setTitle(QString text);
void setDescription(QString text);
void clear();
public slots:
void ellipsisHandler(const QString& link );
void boxClosed(int result);
private:
void updateHiddenState();
private:
Ui::ErrorFrame *ui;
QString desc;
class QMessageBox * currentBox = nullptr;
};

View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ErrorFrame</class>
<widget class="QFrame" name="ErrorFrame">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>527</width>
<height>113</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>120</height>
</size>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>6</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_Title">
<property name="text">
<string notr="true"/>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_Description">
<property name="toolTip">
<string notr="true"/>
</property>
<property name="text">
<string notr="true"/>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>