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:
parent
ffcef673de
commit
3c46d8a412
@ -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.";
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
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();
|
||||
tryagain = false;
|
||||
}
|
||||
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));
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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()) {
|
||||
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");
|
||||
}
|
||||
if(account->isExpired()) {
|
||||
case AccountState::Errored: {
|
||||
return tr("Errored", "Account status");
|
||||
}
|
||||
case AccountState::Expired: {
|
||||
return tr("Expired", "Account status");
|
||||
}
|
||||
return tr("Ready", "Account status");
|
||||
case AccountState::Gone: {
|
||||
return tr("Gone", "Account status");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case ProfileNameColumn: {
|
||||
@ -585,8 +610,103 @@ bool AccountList::anyAccountIsValid()
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -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).
|
||||
|
@ -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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
else if (newState == STATE_FAILED_HARD || newState == STATE_FAILED_SOFT || newState == STATE_FAILED_GONE)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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() {
|
@ -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_;
|
||||
};
|
7
launcher/minecraft/auth/AuthStep.cpp
Normal file
7
launcher/minecraft/auth/AuthStep.cpp
Normal file
@ -0,0 +1,7 @@
|
||||
#include "AuthStep.h"
|
||||
|
||||
AuthStep::AuthStep(AccountData *data) : QObject(nullptr), m_data(data) {
|
||||
}
|
||||
|
||||
AuthStep::~AuthStep() noexcept = default;
|
||||
|
33
launcher/minecraft/auth/AuthStep.h
Normal file
33
launcher/minecraft/auth/AuthStep.h
Normal 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;
|
||||
};
|
@ -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,41 +74,9 @@ QJsonObject MinecraftAccount::saveToJson() const
|
||||
return data.saveState();
|
||||
}
|
||||
|
||||
AccountStatus MinecraftAccount::accountStatus() const {
|
||||
if(data.type == AccountType::Mojang) {
|
||||
if (data.accessToken().isEmpty()) {
|
||||
return NotVerified;
|
||||
AccountState MinecraftAccount::accountState() const {
|
||||
return data.accountState;
|
||||
}
|
||||
else {
|
||||
return Verified;
|
||||
}
|
||||
}
|
||||
// MSA
|
||||
// FIXME: this is extremely crude and probably wrong
|
||||
if(data.msaToken.token.isEmpty()) {
|
||||
return NotVerified;
|
||||
}
|
||||
else {
|
||||
return Verified;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@ -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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
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;
|
||||
}
|
||||
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);
|
||||
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
|
||||
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();
|
||||
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: {
|
||||
case AccountTaskState::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();
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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;
|
@ -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);
|
@ -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."));
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
};
|
71
launcher/minecraft/auth/flows/AuthFlow.cpp
Normal file
71
launcher/minecraft/auth/flows/AuthFlow.cpp
Normal 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();
|
||||
}
|
||||
}
|
45
launcher/minecraft/auth/flows/AuthFlow.h
Normal file
45
launcher/minecraft/auth/flows/AuthFlow.h
Normal 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;
|
||||
};
|
37
launcher/minecraft/auth/flows/MSA.cpp
Normal file
37
launcher/minecraft/auth/flows/MSA.cpp
Normal 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));
|
||||
}
|
22
launcher/minecraft/auth/flows/MSA.h
Normal file
22
launcher/minecraft/auth/flows/MSA.h
Normal 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
|
||||
);
|
||||
};
|
@ -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();
|
||||
}
|
@ -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;
|
||||
};
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
};
|
27
launcher/minecraft/auth/flows/Mojang.cpp
Normal file
27
launcher/minecraft/auth/flows/Mojang.cpp
Normal 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));
|
||||
}
|
26
launcher/minecraft/auth/flows/Mojang.h
Normal file
26
launcher/minecraft/auth/flows/Mojang.h
Normal 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;
|
||||
};
|
@ -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);
|
||||
}
|
@ -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;
|
||||
};
|
@ -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();
|
||||
}
|
@ -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;
|
||||
};
|
53
launcher/minecraft/auth/steps/EntitlementsStep.cpp
Normal file
53
launcher/minecraft/auth/steps/EntitlementsStep.cpp
Normal 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"));
|
||||
}
|
25
launcher/minecraft/auth/steps/EntitlementsStep.h
Normal file
25
launcher/minecraft/auth/steps/EntitlementsStep.h
Normal 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;
|
||||
};
|
43
launcher/minecraft/auth/steps/GetSkinStep.cpp
Normal file
43
launcher/minecraft/auth/steps/GetSkinStep.cpp
Normal 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"));
|
||||
}
|
22
launcher/minecraft/auth/steps/GetSkinStep.h
Normal file
22
launcher/minecraft/auth/steps/GetSkinStep.h
Normal 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>);
|
||||
};
|
78
launcher/minecraft/auth/steps/LauncherLoginStep.cpp
Normal file
78
launcher/minecraft/auth/steps/LauncherLoginStep.cpp
Normal 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(""));
|
||||
}
|
22
launcher/minecraft/auth/steps/LauncherLoginStep.h
Normal file
22
launcher/minecraft/auth/steps/LauncherLoginStep.h
Normal 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>);
|
||||
};
|
111
launcher/minecraft/auth/steps/MSAStep.cpp
Normal file
111
launcher/minecraft/auth/steps/MSAStep.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
32
launcher/minecraft/auth/steps/MSAStep.h
Normal file
32
launcher/minecraft/auth/steps/MSAStep.h
Normal 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;
|
||||
};
|
45
launcher/minecraft/auth/steps/MigrationEligibilityStep.cpp
Normal file
45
launcher/minecraft/auth/steps/MigrationEligibilityStep.cpp
Normal 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"));
|
||||
}
|
22
launcher/minecraft/auth/steps/MigrationEligibilityStep.h
Normal file
22
launcher/minecraft/auth/steps/MigrationEligibilityStep.h
Normal 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>);
|
||||
};
|
83
launcher/minecraft/auth/steps/MinecraftProfileStep.cpp
Normal file
83
launcher/minecraft/auth/steps/MinecraftProfileStep.cpp
Normal 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.")
|
||||
);
|
||||
}
|
22
launcher/minecraft/auth/steps/MinecraftProfileStep.h
Normal file
22
launcher/minecraft/auth/steps/MinecraftProfileStep.h
Normal 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>);
|
||||
};
|
158
launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp
Normal file
158
launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp
Normal 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;
|
||||
}
|
34
launcher/minecraft/auth/steps/XboxAuthorizationStep.h
Normal file
34
launcher/minecraft/auth/steps/XboxAuthorizationStep.h
Normal 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;
|
||||
};
|
73
launcher/minecraft/auth/steps/XboxProfileStep.cpp
Normal file
73
launcher/minecraft/auth/steps/XboxProfileStep.cpp
Normal 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"));
|
||||
}
|
22
launcher/minecraft/auth/steps/XboxProfileStep.h
Normal file
22
launcher/minecraft/auth/steps/XboxProfileStep.h
Normal 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>);
|
||||
};
|
68
launcher/minecraft/auth/steps/XboxUserStep.cpp
Normal file
68
launcher/minecraft/auth/steps/XboxUserStep.cpp
Normal 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"));
|
||||
}
|
22
launcher/minecraft/auth/steps/XboxUserStep.h
Normal file
22
launcher/minecraft/auth/steps/XboxUserStep.h
Normal 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>);
|
||||
};
|
51
launcher/minecraft/auth/steps/YggdrasilStep.cpp
Normal file
51
launcher/minecraft/auth/steps/YggdrasilStep.cpp
Normal 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);
|
||||
}
|
28
launcher/minecraft/auth/steps/YggdrasilStep.h
Normal file
28
launcher/minecraft/auth/steps/YggdrasilStep.h
Normal 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;
|
||||
};
|
@ -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"));
|
||||
|
@ -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:
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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();
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
/*
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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;
|
||||
|
134
launcher/ui/widgets/ErrorFrame.cpp
Normal file
134
launcher/ui/widgets/ErrorFrame.cpp
Normal 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;
|
||||
}
|
49
launcher/ui/widgets/ErrorFrame.h
Normal file
49
launcher/ui/widgets/ErrorFrame.h
Normal 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;
|
||||
};
|
92
launcher/ui/widgets/ErrorFrame.ui
Normal file
92
launcher/ui/widgets/ErrorFrame.ui
Normal 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>
|
Loading…
Reference in New Issue
Block a user