This commit is contained in:
Trial97
2024-05-18 16:24:22 +03:00
211 changed files with 2635 additions and 3610 deletions

View File

@@ -42,7 +42,7 @@
#include <QUuid>
namespace {
void tokenToJSONV3(QJsonObject& parent, Katabasis::Token t, const char* tokenName)
void tokenToJSONV3(QJsonObject& parent, Token t, const char* tokenName)
{
if (!t.persistent) {
return;
@@ -74,9 +74,9 @@ void tokenToJSONV3(QJsonObject& parent, Katabasis::Token t, const char* tokenNam
}
}
Katabasis::Token tokenFromJSONV3(const QJsonObject& parent, const char* tokenName)
Token tokenFromJSONV3(const QJsonObject& parent, const char* tokenName)
{
Katabasis::Token out;
Token out;
auto tokenObject = parent.value(tokenName).toObject();
if (tokenObject.isEmpty()) {
return out;
@@ -94,7 +94,7 @@ Katabasis::Token tokenFromJSONV3(const QJsonObject& parent, const char* tokenNam
auto token = tokenObject.value("token");
if (token.isString()) {
out.token = token.toString();
out.validity = Katabasis::Validity::Assumed;
out.validity = Validity::Assumed;
}
auto refresh_token = tokenObject.value("refresh_token");
@@ -241,13 +241,13 @@ MinecraftProfile profileFromJSONV3(const QJsonObject& parent, const char* tokenN
}
}
}
out.validity = Katabasis::Validity::Assumed;
out.validity = Validity::Assumed;
return out;
}
void entitlementToJSONV3(QJsonObject& parent, MinecraftEntitlement p)
{
if (p.validity == Katabasis::Validity::None) {
if (p.validity == Validity::None) {
return;
}
QJsonObject out;
@@ -271,7 +271,7 @@ bool entitlementFromJSONV3(const QJsonObject& parent, MinecraftEntitlement& out)
}
out.canPlayMinecraft = canPlayMinecraftV.toBool(false);
out.ownsMinecraft = ownsMinecraftV.toBool(false);
out.validity = Katabasis::Validity::Assumed;
out.validity = Validity::Assumed;
}
return true;
}
@@ -313,10 +313,10 @@ bool AccountData::resumeStateFromV3(QJsonObject data)
minecraftProfile = profileFromJSONV3(data, "profile");
if (!entitlementFromJSONV3(data, minecraftEntitlement)) {
if (minecraftProfile.validity != Katabasis::Validity::None) {
if (minecraftProfile.validity != Validity::None) {
minecraftEntitlement.canPlayMinecraft = true;
minecraftEntitlement.ownsMinecraft = true;
minecraftEntitlement.validity = Katabasis::Validity::Assumed;
minecraftEntitlement.validity = Validity::Assumed;
}
}

View File

@@ -34,12 +34,29 @@
*/
#pragma once
#include <katabasis/Bits.h>
#include <QByteArray>
#include <QJsonObject>
#include <QString>
#include <QVector>
#include <QDateTime>
#include <QMap>
#include <QString>
#include <QVariantMap>
enum class Validity { None, Assumed, Certain };
struct Token {
QDateTime issueInstant;
QDateTime notAfter;
QString token;
QString refresh_token;
QVariantMap extra;
Validity validity = Validity::None;
bool persistent = true;
};
struct Skin {
QString id;
QString url;
@@ -59,7 +76,7 @@ struct Cape {
struct MinecraftEntitlement {
bool ownsMinecraft = false;
bool canPlayMinecraft = false;
Katabasis::Validity validity = Katabasis::Validity::None;
Validity validity = Validity::None;
};
struct MinecraftProfile {
@@ -68,7 +85,7 @@ struct MinecraftProfile {
Skin skin;
QString currentCape;
QMap<QString, Cape> capes;
Katabasis::Validity validity = Katabasis::Validity::None;
Validity validity = Validity::None;
};
enum class AccountType { MSA, Offline };
@@ -93,15 +110,15 @@ struct AccountData {
AccountType type = AccountType::MSA;
QString msaClientID;
Katabasis::Token msaToken;
Katabasis::Token userToken;
Katabasis::Token xboxApiToken;
Katabasis::Token mojangservicesToken;
Token msaToken;
Token userToken;
Token xboxApiToken;
Token mojangservicesToken;
Katabasis::Token yggdrasilToken;
Token yggdrasilToken;
MinecraftProfile minecraftProfile;
MinecraftEntitlement minecraftEntitlement;
Katabasis::Validity validity_ = Katabasis::Validity::None;
Validity validity_ = Validity::None;
// runtime only information (not saved with the account)
QString internalId;

View File

@@ -35,7 +35,7 @@
#include "AccountList.h"
#include "AccountData.h"
#include "AccountTask.h"
#include "tasks/Task.h"
#include <QDir>
#include <QFile>
@@ -52,8 +52,6 @@
#include <FileSystem.h>
#include <QSaveFile>
#include <chrono>
enum AccountListVersion { MojangMSA = 3 };
AccountList::AccountList(QObject* parent) : QAbstractListModel(parent)
@@ -641,8 +639,8 @@ void AccountList::tryNext()
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);
connect(m_currentTask.get(), &Task::succeeded, this, &AccountList::authSucceeded);
connect(m_currentTask.get(), &Task::failed, this, &AccountList::authFailed);
m_currentTask->start();
qDebug() << "RefreshSchedule: Processing account " << account->accountDisplayString() << " with internal ID "
<< accountId;

View File

@@ -36,6 +36,7 @@
#pragma once
#include "MinecraftAccount.h"
#include "minecraft/auth/AuthFlow.h"
#include <QAbstractListModel>
#include <QObject>
@@ -144,7 +145,7 @@ class AccountList : public QAbstractListModel {
QList<QString> m_refreshQueue;
QTimer* m_refreshTimer;
QTimer* m_nextTimer;
shared_qobject_ptr<AccountTask> m_currentTask;
shared_qobject_ptr<AuthFlow> m_currentTask;
/*!
* Called whenever the list changes.

View File

@@ -1,134 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* 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 "AccountTask.h"
#include "MinecraftAccount.h"
#include <QByteArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#include <QObject>
#include <QString>
#include <QDebug>
AccountTask::AccountTask(AccountData* data, QObject* parent) : Task(parent), m_data(data)
{
changeState(AccountTaskState::STATE_CREATED);
}
QString AccountTask::getStateMessage() const
{
switch (m_taskState) {
case AccountTaskState::STATE_CREATED:
return "Waiting...";
case AccountTaskState::STATE_WORKING:
return tr("Sending request to auth servers...");
case AccountTaskState::STATE_SUCCEEDED:
return tr("Authentication task succeeded.");
case AccountTaskState::STATE_OFFLINE:
return tr("Failed to contact the authentication server.");
case AccountTaskState::STATE_DISABLED:
return tr("Client ID has changed. New session needs to be created.");
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("...");
}
}
bool AccountTask::changeState(AccountTaskState newState, QString reason)
{
m_taskState = newState;
// FIXME: virtual method invoked in constructor.
// We want that behavior, but maybe make it less weird?
setStatus(getStateMessage());
switch (newState) {
case AccountTaskState::STATE_CREATED: {
m_data->errorString.clear();
return true;
}
case AccountTaskState::STATE_WORKING: {
m_data->accountState = AccountState::Working;
return true;
}
case AccountTaskState::STATE_SUCCEEDED: {
m_data->accountState = AccountState::Online;
emitSucceeded();
return false;
}
case AccountTaskState::STATE_OFFLINE: {
m_data->errorString = reason;
m_data->accountState = AccountState::Offline;
emitFailed(reason);
return false;
}
case AccountTaskState::STATE_DISABLED: {
m_data->errorString = reason;
m_data->accountState = AccountState::Disabled;
emitFailed(reason);
return false;
}
case AccountTaskState::STATE_FAILED_SOFT: {
m_data->errorString = reason;
m_data->accountState = AccountState::Errored;
emitFailed(reason);
return false;
}
case AccountTaskState::STATE_FAILED_HARD: {
m_data->errorString = reason;
m_data->accountState = AccountState::Expired;
emitFailed(reason);
return false;
}
case AccountTaskState::STATE_FAILED_GONE: {
m_data->errorString = reason;
m_data->accountState = AccountState::Gone;
emitFailed(reason);
return false;
}
default: {
QString error = tr("Unknown account task state: %1").arg(int(newState));
m_data->accountState = AccountState::Errored;
emitFailed(error);
return false;
}
}
}

View File

@@ -1,92 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* 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 <tasks/Task.h>
#include <qsslerror.h>
#include <QJsonObject>
#include <QString>
#include <QTimer>
#include "MinecraftAccount.h"
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_DISABLED, //!< MSA Client ID has changed. Tell user to reloginn
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 {
Q_OBJECT
public:
explicit AccountTask(AccountData* data, QObject* parent = 0);
virtual ~AccountTask(){};
AccountTaskState m_taskState = AccountTaskState::STATE_CREATED;
AccountTaskState taskState() { return m_taskState; }
signals:
void showVerificationUriAndCode(const QUrl& uri, const QString& code, int expiresIn);
void hideVerificationUriAndCode();
protected:
/**
* Returns the state message for the given state.
* Used to set the status message for the task.
* Should be overridden by subclasses that want to change messages for a given state.
*/
virtual QString getStateMessage() const;
protected slots:
// NOTE: true -> non-terminal state, false -> terminal state
bool changeState(AccountTaskState newState, QString reason = QString());
protected:
AccountData* m_data = nullptr;
};

View File

@@ -0,0 +1,146 @@
#include <QDebug>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include "minecraft/auth/AccountData.h"
#include "minecraft/auth/steps/EntitlementsStep.h"
#include "minecraft/auth/steps/GetSkinStep.h"
#include "minecraft/auth/steps/LauncherLoginStep.h"
#include "minecraft/auth/steps/MSADeviceCodeStep.h"
#include "minecraft/auth/steps/MSAStep.h"
#include "minecraft/auth/steps/MinecraftProfileStep.h"
#include "minecraft/auth/steps/XboxAuthorizationStep.h"
#include "minecraft/auth/steps/XboxProfileStep.h"
#include "minecraft/auth/steps/XboxUserStep.h"
#include "tasks/Task.h"
#include "AuthFlow.h"
#include <Application.h>
AuthFlow::AuthFlow(AccountData* data, Action action, QObject* parent) : Task(parent), m_data(data)
{
if (data->type == AccountType::MSA) {
if (action == Action::DeviceCode) {
auto oauthStep = makeShared<MSADeviceCodeStep>(m_data);
connect(oauthStep.get(), &MSADeviceCodeStep::authorizeWithBrowser, this, &AuthFlow::authorizeWithBrowserWithExtra);
connect(this, &Task::aborted, oauthStep.get(), &MSADeviceCodeStep::abort);
m_steps.append(oauthStep);
} else {
auto oauthStep = makeShared<MSAStep>(m_data, action == Action::Refresh);
connect(oauthStep.get(), &MSAStep::authorizeWithBrowser, this, &AuthFlow::authorizeWithBrowser);
m_steps.append(oauthStep);
}
m_steps.append(makeShared<XboxUserStep>(m_data));
m_steps.append(makeShared<XboxAuthorizationStep>(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox"));
m_steps.append(
makeShared<XboxAuthorizationStep>(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang"));
m_steps.append(makeShared<LauncherLoginStep>(m_data));
m_steps.append(makeShared<XboxProfileStep>(m_data));
m_steps.append(makeShared<EntitlementsStep>(m_data));
m_steps.append(makeShared<MinecraftProfileStep>(m_data));
m_steps.append(makeShared<GetSkinStep>(m_data));
}
changeState(AccountTaskState::STATE_CREATED);
}
void AuthFlow::succeed()
{
m_data->validity_ = Validity::Certain;
changeState(AccountTaskState::STATE_SUCCEEDED, tr("Finished all authentication steps"));
}
void AuthFlow::executeTask()
{
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);
m_currentStep->perform();
}
void AuthFlow::stepFinished(AccountTaskState resultingState, QString message)
{
if (changeState(resultingState, message))
nextStep();
}
bool AuthFlow::changeState(AccountTaskState newState, QString reason)
{
m_taskState = newState;
setDetails(reason);
switch (newState) {
case AccountTaskState::STATE_CREATED: {
setStatus(tr("Waiting..."));
m_data->errorString.clear();
return true;
}
case AccountTaskState::STATE_WORKING: {
setStatus(m_currentStep ? m_currentStep->describe() : tr("Working..."));
m_data->accountState = AccountState::Working;
return true;
}
case AccountTaskState::STATE_SUCCEEDED: {
setStatus(tr("Authentication task succeeded."));
m_data->accountState = AccountState::Online;
emitSucceeded();
return false;
}
case AccountTaskState::STATE_OFFLINE: {
setStatus(tr("Failed to contact the authentication server."));
m_data->errorString = reason;
m_data->accountState = AccountState::Offline;
emitFailed(reason);
return false;
}
case AccountTaskState::STATE_DISABLED: {
setStatus(tr("Client ID has changed. New session needs to be created."));
m_data->errorString = reason;
m_data->accountState = AccountState::Disabled;
emitFailed(reason);
return false;
}
case AccountTaskState::STATE_FAILED_SOFT: {
setStatus(tr("Encountered an error during authentication."));
m_data->errorString = reason;
m_data->accountState = AccountState::Errored;
emitFailed(reason);
return false;
}
case AccountTaskState::STATE_FAILED_HARD: {
setStatus(tr("Failed to authenticate. The session has expired."));
m_data->errorString = reason;
m_data->accountState = AccountState::Expired;
emitFailed(reason);
return false;
}
case AccountTaskState::STATE_FAILED_GONE: {
setStatus(tr("Failed to authenticate. The account no longer exists."));
m_data->errorString = reason;
m_data->accountState = AccountState::Gone;
emitFailed(reason);
return false;
}
default: {
setStatus(tr("..."));
QString error = tr("Unknown account task state: %1").arg(int(newState));
m_data->accountState = AccountState::Errored;
emitFailed(error);
return false;
}
}
}

View File

@@ -0,0 +1,45 @@
#pragma once
#include <QImage>
#include <QList>
#include <QNetworkReply>
#include <QObject>
#include <QSet>
#include <QVector>
#include "minecraft/auth/AccountData.h"
#include "minecraft/auth/AuthStep.h"
#include "tasks/Task.h"
class AuthFlow : public Task {
Q_OBJECT
public:
enum class Action { Refresh, Login, DeviceCode };
explicit AuthFlow(AccountData* data, Action action = Action::Refresh, QObject* parent = 0);
virtual ~AuthFlow() = default;
void executeTask() override;
AccountTaskState taskState() { return m_taskState; }
signals:
void authorizeWithBrowser(const QUrl& url);
void authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn);
protected:
void succeed();
void nextStep();
private slots:
// NOTE: true -> non-terminal state, false -> terminal state
bool changeState(AccountTaskState newState, QString reason = QString());
void stepFinished(AccountTaskState resultingState, QString message);
private:
AccountTaskState m_taskState = AccountTaskState::STATE_CREATED;
QList<AuthStep::Ptr> m_steps;
AuthStep::Ptr m_currentStep;
AccountData* m_data = nullptr;
};

View File

@@ -1,175 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* 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 <cassert>
#include <QBuffer>
#include <QDebug>
#include <QTimer>
#include <QUrlQuery>
#include "Application.h"
#include "AuthRequest.h"
#include "katabasis/Globals.h"
AuthRequest::AuthRequest(QObject* parent) : QObject(parent) {}
AuthRequest::~AuthRequest() {}
void AuthRequest::get(const QNetworkRequest& req, int timeout /* = 60*1000*/)
{
setup(req, QNetworkAccessManager::GetOperation);
reply_ = APPLICATION->network()->get(request_);
status_ = Requesting;
timedReplies_.add(new Katabasis::Reply(reply_, timeout));
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15
connect(reply_, &QNetworkReply::errorOccurred, this, &AuthRequest::onRequestError);
#else // &QNetworkReply::error SIGNAL depricated
connect(reply_, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &AuthRequest::onRequestError);
#endif
connect(reply_, &QNetworkReply::finished, this, &AuthRequest::onRequestFinished);
connect(reply_, &QNetworkReply::sslErrors, this, &AuthRequest::onSslErrors);
}
void AuthRequest::post(const QNetworkRequest& req, const QByteArray& data, int timeout /* = 60*1000*/)
{
setup(req, QNetworkAccessManager::PostOperation);
data_ = data;
status_ = Requesting;
reply_ = APPLICATION->network()->post(request_, data_);
timedReplies_.add(new Katabasis::Reply(reply_, timeout));
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15
connect(reply_, &QNetworkReply::errorOccurred, this, &AuthRequest::onRequestError);
#else // &QNetworkReply::error SIGNAL depricated
connect(reply_, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &AuthRequest::onRequestError);
#endif
connect(reply_, &QNetworkReply::finished, this, &AuthRequest::onRequestFinished);
connect(reply_, &QNetworkReply::sslErrors, this, &AuthRequest::onSslErrors);
connect(reply_, &QNetworkReply::uploadProgress, this, &AuthRequest::onUploadProgress);
}
void AuthRequest::onRequestFinished()
{
if (status_ == Idle) {
return;
}
if (reply_ != qobject_cast<QNetworkReply*>(sender())) {
return;
}
httpStatus_ = reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
finish();
}
void AuthRequest::onRequestError(QNetworkReply::NetworkError error)
{
qWarning() << "AuthRequest::onRequestError: Error" << (int)error;
if (status_ == Idle) {
return;
}
if (reply_ != qobject_cast<QNetworkReply*>(sender())) {
return;
}
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()));
}
void AuthRequest::onSslErrors(QList<QSslError> errors)
{
int i = 1;
for (auto error : errors) {
qCritical() << "LOGIN SSL Error #" << i << " : " << error.errorString();
auto cert = error.certificate();
qCritical() << "Certificate in question:\n" << cert.toText();
i++;
}
}
void AuthRequest::onUploadProgress(qint64 uploaded, qint64 total)
{
if (status_ == Idle) {
qWarning() << "AuthRequest::onUploadProgress: No pending request";
return;
}
if (reply_ != qobject_cast<QNetworkReply*>(sender())) {
return;
}
// Restart timeout because request in progress
Katabasis::Reply* o2Reply = timedReplies_.find(reply_);
if (o2Reply) {
o2Reply->start();
}
emit uploadProgress(uploaded, total);
}
void AuthRequest::setup(const QNetworkRequest& req, QNetworkAccessManager::Operation operation, const QByteArray& verb)
{
request_ = req;
operation_ = operation;
url_ = req.url();
QUrl url = url_;
request_.setUrl(url);
if (!verb.isEmpty()) {
request_.setRawHeader(Katabasis::HTTP_HTTP_HEADER, verb);
}
status_ = Requesting;
error_ = QNetworkReply::NoError;
errorString_.clear();
httpStatus_ = 0;
}
void AuthRequest::finish()
{
QByteArray data;
if (status_ == Idle) {
qWarning() << "AuthRequest::finish: No pending request";
return;
}
data = reply_->readAll();
status_ = Idle;
timedReplies_.remove(reply_);
reply_->disconnect(this);
reply_->deleteLater();
QList<QNetworkReply::RawHeaderPair> headers = reply_->rawHeaderPairs();
emit finished(error_, data, headers);
}

View File

@@ -1,67 +0,0 @@
#pragma once
#include <QByteArray>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QObject>
#include <QUrl>
#include "katabasis/Reply.h"
/// Makes authentication requests.
class AuthRequest : public QObject {
Q_OBJECT
public:
explicit AuthRequest(QObject* parent = 0);
~AuthRequest();
public slots:
void get(const QNetworkRequest& req, int timeout = 60 * 1000);
void post(const QNetworkRequest& req, const QByteArray& data, int timeout = 60 * 1000);
signals:
/// Emitted when a request has been completed or failed.
void finished(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers);
/// Emitted when an upload has progressed.
void uploadProgress(qint64 bytesSent, qint64 bytesTotal);
protected slots:
/// Handle request finished.
void onRequestFinished();
/// Handle request error.
void onRequestError(QNetworkReply::NetworkError error);
/// Handle ssl errors.
void onSslErrors(QList<QSslError> errors);
/// Finish the request, emit finished() signal.
void finish();
/// Handle upload progress.
void onUploadProgress(qint64 uploaded, qint64 total);
public:
QNetworkReply::NetworkError error_;
int httpStatus_ = 0;
QString errorString_;
protected:
void setup(const QNetworkRequest& request, QNetworkAccessManager::Operation operation, const QByteArray& verb = QByteArray());
enum Status { Idle, Requesting, ReRequesting };
QNetworkRequest request_;
QByteArray data_;
QNetworkReply* reply_;
Status status_;
QNetworkAccessManager::Operation operation_;
QUrl url_;
Katabasis::ReplyList timedReplies_;
QTimer* timer_;
};

View File

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

View File

@@ -3,30 +3,40 @@
#include <QNetworkReply>
#include <QObject>
#include "AccountTask.h"
#include "QObjectPtr.h"
#include "minecraft/auth/AccountData.h"
/**
* 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_DISABLED, //!< MSA Client ID has changed. Tell user to reloginn
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 AuthStep : public QObject {
Q_OBJECT
public:
using Ptr = shared_qobject_ptr<AuthStep>;
public:
explicit AuthStep(AccountData* data);
virtual ~AuthStep() noexcept;
explicit AuthStep(AccountData* data) : QObject(nullptr), m_data(data){};
virtual ~AuthStep() noexcept = default;
virtual QString describe() = 0;
public slots:
virtual void perform() = 0;
virtual void rehydrate() = 0;
signals:
void finished(AccountTaskState resultingState, QString message);
void showVerificationUriAndCode(const QUrl& uri, const QString& code, int expiresIn);
void hideVerificationUriAndCode();
protected:
AccountData* m_data;

View File

@@ -50,9 +50,8 @@
#include <QPainter>
#include "flows/MSA.h"
#include "flows/Offline.h"
#include "minecraft/auth/AccountData.h"
#include "minecraft/auth/AuthFlow.h"
MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent)
{
@@ -80,7 +79,7 @@ MinecraftAccountPtr MinecraftAccount::createOffline(const QString& username)
auto account = makeShared<MinecraftAccount>();
account->data.type = AccountType::Offline;
account->data.yggdrasilToken.token = "0";
account->data.yggdrasilToken.validity = Katabasis::Validity::Certain;
account->data.yggdrasilToken.validity = Validity::Certain;
account->data.yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc();
account->data.yggdrasilToken.extra["userName"] = username;
account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]"));
@@ -88,7 +87,7 @@ MinecraftAccountPtr MinecraftAccount::createOffline(const QString& username)
account->data.minecraftEntitlement.canPlayMinecraft = true;
account->data.minecraftProfile.id = uuidFromUsername(username).toString().remove(QRegularExpression("[{}-]"));
account->data.minecraftProfile.name = username;
account->data.minecraftProfile.validity = Katabasis::Validity::Certain;
account->data.minecraftProfile.validity = Validity::Certain;
return account;
}
@@ -120,11 +119,11 @@ QPixmap MinecraftAccount::getFace() const
return skin.scaled(64, 64, Qt::KeepAspectRatio);
}
shared_qobject_ptr<AccountTask> MinecraftAccount::loginMSA()
shared_qobject_ptr<AuthFlow> MinecraftAccount::login(bool useDeviceCode)
{
Q_ASSERT(m_currentTask.get() == nullptr);
m_currentTask.reset(new MSAInteractive(&data));
m_currentTask.reset(new AuthFlow(&data, useDeviceCode ? AuthFlow::Action::DeviceCode : AuthFlow::Action::Login, this));
connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded);
connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed);
connect(m_currentTask.get(), &Task::aborted, this, [this] { authFailed(tr("Aborted")); });
@@ -132,29 +131,13 @@ shared_qobject_ptr<AccountTask> MinecraftAccount::loginMSA()
return m_currentTask;
}
shared_qobject_ptr<AccountTask> MinecraftAccount::loginOffline()
{
Q_ASSERT(m_currentTask.get() == nullptr);
m_currentTask.reset(new OfflineLogin(&data));
connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded);
connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed);
connect(m_currentTask.get(), &Task::aborted, this, [this] { authFailed(tr("Aborted")); });
emit activityChanged(true);
return m_currentTask;
}
shared_qobject_ptr<AccountTask> MinecraftAccount::refresh()
shared_qobject_ptr<AuthFlow> MinecraftAccount::refresh()
{
if (m_currentTask) {
return m_currentTask;
}
if (data.type == AccountType::MSA) {
m_currentTask.reset(new MSASilent(&data));
} else {
m_currentTask.reset(new OfflineRefresh(&data));
}
m_currentTask.reset(new AuthFlow(&data, AuthFlow::Action::Refresh, this));
connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded);
connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed);
@@ -163,7 +146,7 @@ shared_qobject_ptr<AccountTask> MinecraftAccount::refresh()
return m_currentTask;
}
shared_qobject_ptr<AccountTask> MinecraftAccount::currentTask()
shared_qobject_ptr<AuthFlow> MinecraftAccount::currentTask()
{
return m_currentTask;
}
@@ -189,17 +172,17 @@ void MinecraftAccount::authFailed(QString reason)
if (accountType() == AccountType::MSA) {
data.msaToken.token = QString();
data.msaToken.refresh_token = QString();
data.msaToken.validity = Katabasis::Validity::None;
data.validity_ = Katabasis::Validity::None;
data.msaToken.validity = Validity::None;
data.validity_ = Validity::None;
} else {
data.yggdrasilToken.token = QString();
data.yggdrasilToken.validity = Katabasis::Validity::None;
data.validity_ = Katabasis::Validity::None;
data.yggdrasilToken.validity = Validity::None;
data.validity_ = Validity::None;
}
emit changed();
} break;
case AccountTaskState::STATE_FAILED_GONE: {
data.validity_ = Katabasis::Validity::None;
data.validity_ = Validity::None;
emit changed();
} break;
case AccountTaskState::STATE_CREATED:
@@ -229,13 +212,13 @@ bool MinecraftAccount::shouldRefresh() const
return false;
}
switch (data.validity_) {
case Katabasis::Validity::Certain: {
case Validity::Certain: {
break;
}
case Katabasis::Validity::None: {
case Validity::None: {
return false;
}
case Katabasis::Validity::Assumed: {
case Validity::Assumed: {
return true;
}
}

View File

@@ -43,15 +43,13 @@
#include <QPixmap>
#include <QString>
#include <memory>
#include "AccountData.h"
#include "AuthSession.h"
#include "QObjectPtr.h"
#include "Usable.h"
#include "minecraft/auth/AuthFlow.h"
class Task;
class AccountTask;
class MinecraftAccount;
using MinecraftAccountPtr = shared_qobject_ptr<MinecraftAccount>;
@@ -97,13 +95,11 @@ class MinecraftAccount : public QObject, public Usable {
QJsonObject saveToJson() const;
public: /* manipulation */
shared_qobject_ptr<AccountTask> loginMSA();
shared_qobject_ptr<AuthFlow> login(bool useDeviceCode = false);
shared_qobject_ptr<AccountTask> loginOffline();
shared_qobject_ptr<AuthFlow> refresh();
shared_qobject_ptr<AccountTask> refresh();
shared_qobject_ptr<AccountTask> currentTask();
shared_qobject_ptr<AuthFlow> currentTask();
public: /* queries */
QString internalId() const { return data.internalId; }
@@ -166,7 +162,7 @@ class MinecraftAccount : public QObject, public Usable {
AccountData data;
// current task we are executing here
shared_qobject_ptr<AccountTask> m_currentTask;
shared_qobject_ptr<AuthFlow> m_currentTask;
protected: /* methods */
void incrementUses() override;

View File

@@ -79,7 +79,7 @@ bool getBool(QJsonValue value, bool& out)
// 2148916238 = child account not linked to a family
*/
bool parseXTokenResponse(QByteArray& data, Katabasis::Token& output, QString name)
bool parseXTokenResponse(QByteArray& data, Token& output, QString name)
{
qDebug() << "Parsing" << name << ":";
qCDebug(authCredentials()) << data;
@@ -135,7 +135,7 @@ bool parseXTokenResponse(QByteArray& data, Katabasis::Token& output, QString nam
qWarning() << "Missing uhs";
return false;
}
output.validity = Katabasis::Validity::Certain;
output.validity = Validity::Certain;
qDebug() << name << "is valid.";
return true;
}
@@ -213,7 +213,7 @@ bool parseMinecraftProfile(QByteArray& data, MinecraftProfile& output)
output.capes[capeOut.id] = capeOut;
}
output.currentCape = currentCape;
output.validity = Katabasis::Validity::Certain;
output.validity = Validity::Certain;
return true;
}
@@ -388,7 +388,7 @@ bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output)
output.currentCape = capeOut.alias;
}
output.validity = Katabasis::Validity::Certain;
output.validity = Validity::Certain;
return true;
}
@@ -422,7 +422,7 @@ bool parseMinecraftEntitlements(QByteArray& data, MinecraftEntitlement& output)
output.ownsMinecraft = true;
}
}
output.validity = Katabasis::Validity::Certain;
output.validity = Validity::Certain;
return true;
}
@@ -456,7 +456,7 @@ bool parseRolloutResponse(QByteArray& data, bool& result)
return true;
}
bool parseMojangResponse(QByteArray& data, Katabasis::Token& output)
bool parseMojangResponse(QByteArray& data, Token& output)
{
QJsonParseError jsonError;
qDebug() << "Parsing Mojang response...";
@@ -488,7 +488,7 @@ bool parseMojangResponse(QByteArray& data, Katabasis::Token& output)
qWarning() << "access_token is not valid";
return false;
}
output.validity = Katabasis::Validity::Certain;
output.validity = Validity::Certain;
qDebug() << "Mojang response is valid.";
return true;
}

View File

@@ -9,8 +9,8 @@ bool getNumber(QJsonValue value, double& out);
bool getNumber(QJsonValue value, int64_t& out);
bool getBool(QJsonValue value, bool& out);
bool parseXTokenResponse(QByteArray& data, Katabasis::Token& output, QString name);
bool parseMojangResponse(QByteArray& data, Katabasis::Token& output);
bool parseXTokenResponse(QByteArray& data, Token& output, QString name);
bool parseMojangResponse(QByteArray& data, Token& output);
bool parseMinecraftProfile(QByteArray& data, MinecraftProfile& output);
bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output);

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +0,0 @@
#include "Offline.h"
#include "minecraft/auth/steps/OfflineStep.h"
OfflineRefresh::OfflineRefresh(AccountData* data, QObject* parent) : AuthFlow(data, parent)
{
m_steps.append(makeShared<OfflineStep>(m_data));
}
OfflineLogin::OfflineLogin(AccountData* data, QObject* parent) : AuthFlow(data, parent)
{
m_steps.append(makeShared<OfflineStep>(m_data));
}

View File

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

View File

@@ -1,16 +1,20 @@
#include "EntitlementsStep.h"
#include <QList>
#include <QNetworkRequest>
#include <QUrl>
#include <QUuid>
#include <memory>
#include "Application.h"
#include "Logging.h"
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
#include "net/Download.h"
#include "net/StaticHeaderProxy.h"
#include "tasks/Task.h"
EntitlementsStep::EntitlementsStep(AccountData* data) : AuthStep(data) {}
EntitlementsStep::~EntitlementsStep() noexcept = default;
QString EntitlementsStep::describe()
{
return tr("Determining game ownership.");
@@ -19,35 +23,31 @@ QString EntitlementsStep::describe()
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);
m_entitlements_request_id = uuid.toString().remove('{').remove('}');
QUrl url("https://api.minecraftservices.com/entitlements/license?requestId=" + m_entitlements_request_id);
auto headers = QList<Net::HeaderPair>{ { "Content-Type", "application/json" },
{ "Accept", "application/json" },
{ "Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8() } };
m_response.reset(new QByteArray());
m_task = Net::Download::makeByteArray(url, m_response);
m_task->addHeaderProxy(new Net::StaticHeaderProxy(headers));
connect(m_task.get(), &Task::finished, this, &EntitlementsStep::onRequestDone);
m_task->setNetwork(APPLICATION->network());
m_task->start();
qDebug() << "Getting entitlements...";
}
void EntitlementsStep::rehydrate()
void EntitlementsStep::onRequestDone()
{
// NOOP, for now. We only save bools and there's nothing to check.
}
void EntitlementsStep::onRequestDone([[maybe_unused]] QNetworkReply::NetworkError error,
QByteArray data,
[[maybe_unused]] QList<QNetworkReply::RawHeaderPair> headers)
{
auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
requestor->deleteLater();
qCDebug(authCredentials()) << data;
qCDebug(authCredentials()) << *m_response;
// TODO: check presence of same entitlementsRequestId?
// TODO: validate JWTs?
Parsers::parseMinecraftEntitlements(data, m_data->minecraftEntitlement);
Parsers::parseMinecraftEntitlements(*m_response, m_data->minecraftEntitlement);
emit finished(AccountTaskState::STATE_WORKING, tr("Got entitlements"));
}

View File

@@ -1,24 +1,26 @@
#pragma once
#include <QObject>
#include <memory>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h"
#include "net/Download.h"
class EntitlementsStep : public AuthStep {
Q_OBJECT
public:
explicit EntitlementsStep(AccountData* data);
virtual ~EntitlementsStep() noexcept;
virtual ~EntitlementsStep() noexcept = default;
void perform() override;
void rehydrate() override;
QString describe() override;
private slots:
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
void onRequestDone();
private:
QString m_entitlementsRequestId;
QString m_entitlements_request_id;
std::shared_ptr<QByteArray> m_response;
Net::Download::Ptr m_task;
};

View File

@@ -3,13 +3,10 @@
#include <QNetworkRequest>
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
#include "Application.h"
GetSkinStep::GetSkinStep(AccountData* data) : AuthStep(data) {}
GetSkinStep::~GetSkinStep() noexcept = default;
QString GetSkinStep::describe()
{
return tr("Getting skin.");
@@ -17,25 +14,20 @@ QString GetSkinStep::describe()
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);
QUrl url(m_data->minecraftProfile.skin.url);
m_response.reset(new QByteArray());
m_task = Net::Download::makeByteArray(url, m_response);
connect(m_task.get(), &Task::finished, this, &GetSkinStep::onRequestDone);
m_task->setNetwork(APPLICATION->network());
m_task->start();
}
void GetSkinStep::rehydrate()
void GetSkinStep::onRequestDone()
{
// 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;
}
if (m_task->error() == QNetworkReply::NoError)
m_data->minecraftProfile.skin.data = *m_response;
emit finished(AccountTaskState::STATE_SUCCEEDED, tr("Got skin"));
}

View File

@@ -1,21 +1,25 @@
#pragma once
#include <QObject>
#include <memory>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h"
#include "net/Download.h"
class GetSkinStep : public AuthStep {
Q_OBJECT
public:
explicit GetSkinStep(AccountData* data);
virtual ~GetSkinStep() noexcept;
virtual ~GetSkinStep() noexcept = default;
void perform() override;
void rehydrate() override;
QString describe() override;
private slots:
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
void onRequestDone();
private:
std::shared_ptr<QByteArray> m_response;
Net::Download::Ptr m_task;
};

View File

@@ -1,17 +1,17 @@
#include "LauncherLoginStep.h"
#include <QNetworkRequest>
#include <QUrl>
#include "Application.h"
#include "Logging.h"
#include "minecraft/auth/AccountTask.h"
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
#include "net/NetUtils.h"
#include "net/StaticHeaderProxy.h"
#include "net/Upload.h"
LauncherLoginStep::LauncherLoginStep(AccountData* data) : AuthStep(data) {}
LauncherLoginStep::~LauncherLoginStep() noexcept = default;
QString LauncherLoginStep::describe()
{
return tr("Accessing Mojang services.");
@@ -19,7 +19,7 @@ QString LauncherLoginStep::describe()
void LauncherLoginStep::perform()
{
auto requestURL = "https://api.minecraftservices.com/launcher/login";
QUrl url("https://api.minecraftservices.com/launcher/login");
auto uhs = m_data->mojangservicesToken.extra["uhs"].toString();
auto xToken = m_data->mojangservicesToken.token;
@@ -31,40 +31,37 @@ void LauncherLoginStep::perform()
)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());
auto headers = QList<Net::HeaderPair>{
{ "Content-Type", "application/json" },
{ "Accept", "application/json" },
};
m_response.reset(new QByteArray());
m_task = Net::Upload::makeByteArray(url, m_response, requestBody.toUtf8());
m_task->addHeaderProxy(new Net::StaticHeaderProxy(headers));
connect(m_task.get(), &Task::finished, this, &LauncherLoginStep::onRequestDone);
m_task->setNetwork(APPLICATION->network());
m_task->start();
qDebug() << "Getting Minecraft access token...";
}
void LauncherLoginStep::rehydrate()
void LauncherLoginStep::onRequestDone()
{
// 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();
qCDebug(authCredentials()) << data;
if (error != QNetworkReply::NoError) {
qWarning() << "Reply error:" << error;
qCDebug(authCredentials()) << data;
if (Net::isApplicationError(error)) {
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to get Minecraft access token: %1").arg(requestor->errorString_));
qCDebug(authCredentials()) << *m_response;
if (m_task->error() != QNetworkReply::NoError) {
qWarning() << "Reply error:" << m_task->error();
if (Net::isApplicationError(m_task->error())) {
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to get Minecraft access token: %1").arg(m_task->errorString()));
} else {
emit finished(AccountTaskState::STATE_OFFLINE, tr("Failed to get Minecraft access token: %1").arg(requestor->errorString_));
emit finished(AccountTaskState::STATE_OFFLINE, tr("Failed to get Minecraft access token: %1").arg(m_task->errorString()));
}
return;
}
if (!Parsers::parseMojangResponse(data, m_data->yggdrasilToken)) {
if (!Parsers::parseMojangResponse(*m_response, m_data->yggdrasilToken)) {
qWarning() << "Could not parse login_with_xbox response...";
qCDebug(authCredentials()) << data;
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to parse the Minecraft access token response."));
return;
}

View File

@@ -1,21 +1,25 @@
#pragma once
#include <QObject>
#include <memory>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h"
#include "net/Upload.h"
class LauncherLoginStep : public AuthStep {
Q_OBJECT
public:
explicit LauncherLoginStep(AccountData* data);
virtual ~LauncherLoginStep() noexcept;
virtual ~LauncherLoginStep() noexcept = default;
void perform() override;
void rehydrate() override;
QString describe() override;
private slots:
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
void onRequestDone();
private:
std::shared_ptr<QByteArray> m_response;
Net::Upload::Ptr m_task;
};

View File

@@ -0,0 +1,270 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2024 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* 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 "MSADeviceCodeStep.h"
#include <QDateTime>
#include <QUrlQuery>
#include "Application.h"
#include "Json.h"
#include "net/StaticHeaderProxy.h"
// https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-device-code
MSADeviceCodeStep::MSADeviceCodeStep(AccountData* data) : AuthStep(data)
{
m_clientId = APPLICATION->getMSAClientID();
}
QString MSADeviceCodeStep::describe()
{
return tr("Logging in with Microsoft account(device code).");
}
void MSADeviceCodeStep::perform()
{
QUrlQuery data;
data.addQueryItem("client_id", m_clientId);
data.addQueryItem("scope", "XboxLive.SignIn XboxLive.offline_access");
auto payload = data.query(QUrl::FullyEncoded).toUtf8();
QUrl url("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode");
auto headers = QList<Net::HeaderPair>{
{ "Content-Type", "application/x-www-form-urlencoded" },
{ "Accept", "application/json" },
};
m_response.reset(new QByteArray());
m_task = Net::Upload::makeByteArray(url, m_response, payload);
m_task->addHeaderProxy(new Net::StaticHeaderProxy(headers));
connect(m_task.get(), &Task::finished, this, &MSADeviceCodeStep::deviceAutorizationFinished);
m_task->setNetwork(APPLICATION->network());
m_task->start();
}
struct DeviceAutorizationResponse {
QString device_code;
QString user_code;
QString verification_uri;
int expires_in;
int interval;
QString error;
QString error_description;
};
DeviceAutorizationResponse parseDeviceAutorizationResponse(const QByteArray& data)
{
QJsonParseError err;
QJsonDocument doc = QJsonDocument::fromJson(data, &err);
if (err.error != QJsonParseError::NoError) {
qWarning() << "Failed to parse device autorization response due to err:" << err.errorString();
return {};
}
if (!doc.isObject()) {
qWarning() << "Device autorization response is not an object";
return {};
}
auto obj = doc.object();
return {
Json::ensureString(obj, "device_code"), Json::ensureString(obj, "user_code"), Json::ensureString(obj, "verification_uri"),
Json::ensureInteger(obj, "expires_in"), Json::ensureInteger(obj, "interval"), Json::ensureString(obj, "error"),
Json::ensureString(obj, "error_description"),
};
}
void MSADeviceCodeStep::deviceAutorizationFinished()
{
auto rsp = parseDeviceAutorizationResponse(*m_response);
if (!rsp.error.isEmpty() || !rsp.error_description.isEmpty()) {
qWarning() << "Device authorization failed:" << rsp.error;
emit finished(AccountTaskState::STATE_FAILED_HARD,
tr("Device authorization failed: %1").arg(rsp.error_description.isEmpty() ? rsp.error : rsp.error_description));
return;
}
if (!m_task->wasSuccessful() || m_task->error() != QNetworkReply::NoError) {
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Failed to retrieve device authorization"));
qDebug() << *m_response;
return;
}
if (rsp.device_code.isEmpty() || rsp.user_code.isEmpty() || rsp.verification_uri.isEmpty() || rsp.expires_in == 0) {
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Device authorization failed: required fields missing"));
return;
}
if (rsp.interval != 0) {
interval = rsp.interval;
}
m_device_code = rsp.device_code;
emit authorizeWithBrowser(rsp.verification_uri, rsp.user_code, rsp.expires_in);
m_expiration_timer.setTimerType(Qt::VeryCoarseTimer);
m_expiration_timer.setInterval(rsp.expires_in * 1000);
m_expiration_timer.setSingleShot(true);
connect(&m_expiration_timer, &QTimer::timeout, this, &MSADeviceCodeStep::abort);
m_expiration_timer.start();
m_pool_timer.setTimerType(Qt::VeryCoarseTimer);
m_pool_timer.setSingleShot(true);
startPoolTimer();
}
void MSADeviceCodeStep::abort()
{
m_expiration_timer.stop();
m_pool_timer.stop();
if (m_task) {
m_task->abort();
}
m_is_aborted = true;
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Task aborted"));
}
void MSADeviceCodeStep::startPoolTimer()
{
if (m_is_aborted) {
return;
}
m_pool_timer.setInterval(interval * 1000);
connect(&m_pool_timer, &QTimer::timeout, this, &MSADeviceCodeStep::authenticateUser);
m_pool_timer.start();
}
void MSADeviceCodeStep::authenticateUser()
{
QUrlQuery data;
data.addQueryItem("client_id", m_clientId);
data.addQueryItem("grant_type", "urn:ietf:params:oauth:grant-type:device_code");
data.addQueryItem("device_code", m_device_code);
auto payload = data.query(QUrl::FullyEncoded).toUtf8();
QUrl url("https://login.microsoftonline.com/consumers/oauth2/v2.0/token");
auto headers = QList<Net::HeaderPair>{
{ "Content-Type", "application/x-www-form-urlencoded" },
{ "Accept", "application/json" },
};
m_response.reset(new QByteArray());
m_task = Net::Upload::makeByteArray(url, m_response, payload);
m_task->addHeaderProxy(new Net::StaticHeaderProxy(headers));
connect(m_task.get(), &Task::finished, this, &MSADeviceCodeStep::authenticationFinished);
m_task->setNetwork(APPLICATION->network());
m_task->start();
}
struct AuthenticationResponse {
QString access_token;
QString token_type;
QString refresh_token;
int expires_in;
QString error;
QString error_description;
QVariantMap extra;
};
AuthenticationResponse parseAuthenticationResponse(const QByteArray& data)
{
QJsonParseError err;
QJsonDocument doc = QJsonDocument::fromJson(data, &err);
if (err.error != QJsonParseError::NoError) {
qWarning() << "Failed to parse device autorization response due to err:" << err.errorString();
return {};
}
if (!doc.isObject()) {
qWarning() << "Device autorization response is not an object";
return {};
}
auto obj = doc.object();
return { Json::ensureString(obj, "access_token"),
Json::ensureString(obj, "token_type"),
Json::ensureString(obj, "refresh_token"),
Json::ensureInteger(obj, "expires_in"),
Json::ensureString(obj, "error"),
Json::ensureString(obj, "error_description"),
obj.toVariantMap() };
}
void MSADeviceCodeStep::authenticationFinished()
{
if (m_task->error() == QNetworkReply::TimeoutError) {
// rfc8628#section-3.5
// "On encountering a connection timeout, clients MUST unilaterally
// reduce their polling frequency before retrying. The use of an
// exponential backoff algorithm to achieve this, such as doubling the
// polling interval on each such connection timeout, is RECOMMENDED."
interval *= 2;
startPoolTimer();
return;
}
auto rsp = parseAuthenticationResponse(*m_response);
if (rsp.error == "slow_down") {
// rfc8628#section-3.5
// "A variant of 'authorization_pending', the authorization request is
// still pending and polling should continue, but the interval MUST
// be increased by 5 seconds for this and all subsequent requests."
interval += 5;
startPoolTimer();
return;
}
if (rsp.error == "authorization_pending") {
// keep trying - rfc8628#section-3.5
// "The authorization request is still pending as the end user hasn't
// yet completed the user-interaction steps (Section 3.3)."
startPoolTimer();
return;
}
if (!rsp.error.isEmpty() || !rsp.error_description.isEmpty()) {
qWarning() << "Device Access failed:" << rsp.error;
emit finished(AccountTaskState::STATE_FAILED_HARD,
tr("Device Access failed: %1").arg(rsp.error_description.isEmpty() ? rsp.error : rsp.error_description));
return;
}
if (!m_task->wasSuccessful() || m_task->error() != QNetworkReply::NoError) {
startPoolTimer(); // it failed so just try again without increasing the interval
return;
}
m_expiration_timer.stop();
m_data->msaClientID = m_clientId;
m_data->msaToken.issueInstant = QDateTime::currentDateTimeUtc();
m_data->msaToken.notAfter = QDateTime::currentDateTime().addSecs(rsp.expires_in);
m_data->msaToken.extra = rsp.extra;
m_data->msaToken.refresh_token = rsp.refresh_token;
m_data->msaToken.token = rsp.access_token;
emit finished(AccountTaskState::STATE_WORKING, tr("Got"));
}

View File

@@ -0,0 +1,76 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2024 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* 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 <QObject>
#include <QTimer>
#include "minecraft/auth/AuthStep.h"
#include "net/Upload.h"
class MSADeviceCodeStep : public AuthStep {
Q_OBJECT
public:
explicit MSADeviceCodeStep(AccountData* data);
virtual ~MSADeviceCodeStep() noexcept = default;
void perform() override;
QString describe() override;
public slots:
void abort();
signals:
void authorizeWithBrowser(QString url, QString code, int expiresIn);
private slots:
void deviceAutorizationFinished();
void startPoolTimer();
void authenticateUser();
void authenticationFinished();
private:
QString m_clientId;
QString m_device_code;
bool m_is_aborted = false;
int interval = 5;
QTimer m_pool_timer;
QTimer m_expiration_timer;
std::shared_ptr<QByteArray> m_response;
Net::Upload::Ptr m_task;
};

View File

@@ -35,123 +35,74 @@
#include "MSAStep.h"
#include <QtNetworkAuth/qoauthhttpserverreplyhandler.h>
#include <QAbstractOAuth2>
#include <QNetworkRequest>
#include "BuildConfig.h"
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
#include "Application.h"
#include "Logging.h"
using OAuth2 = Katabasis::DeviceFlow;
using Activity = Katabasis::Activity;
MSAStep::MSAStep(AccountData* data, Action action) : AuthStep(data), m_action(action)
MSAStep::MSAStep(AccountData* data, bool silent) : AuthStep(data), m_silent(silent)
{
m_clientId = APPLICATION->getMSAClientID();
OAuth2::Options opts;
opts.scope = "XboxLive.signin offline_access";
opts.clientIdentifier = m_clientId;
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());
auto replyHandler = new QOAuthHttpServerReplyHandler(1337, this);
replyHandler->setCallbackText(
" <iframe src=\"https://prismlauncher.org/successful-login\" title=\"PrismLauncher Microsoft login\" style=\"position:fixed; "
"top:0; left:0; bottom:0; right:0; width:100%; height:100%; border:none; margin:0; padding:0; overflow:hidden; "
"z-index:999999;\"/> ");
oauth2.setReplyHandler(replyHandler);
oauth2.setAuthorizationUrl(QUrl("https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize"));
oauth2.setAccessTokenUrl(QUrl("https://login.microsoftonline.com/consumers/oauth2/v2.0/token"));
oauth2.setScope("XboxLive.SignIn XboxLive.offline_access");
oauth2.setClientIdentifier(m_clientId);
oauth2.setNetworkAccessManager(APPLICATION->network().get());
connect(m_oauth2, &OAuth2::activityChanged, this, &MSAStep::onOAuthActivityChanged);
connect(m_oauth2, &OAuth2::showVerificationUriAndCode, this, &MSAStep::showVerificationUriAndCode);
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::granted, this, [this] {
m_data->msaClientID = oauth2.clientIdentifier();
m_data->msaToken.issueInstant = QDateTime::currentDateTimeUtc();
m_data->msaToken.notAfter = oauth2.expirationAt();
m_data->msaToken.extra = oauth2.extraTokens();
m_data->msaToken.refresh_token = oauth2.refreshToken();
m_data->msaToken.token = oauth2.token();
emit finished(AccountTaskState::STATE_WORKING, tr("Got "));
});
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, this, &MSAStep::authorizeWithBrowser);
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::requestFailed, this, [this](const QAbstractOAuth2::Error err) {
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Microsoft user authentication failed."));
});
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::extraTokensChanged, this,
[this](const QVariantMap& tokens) { m_data->msaToken.extra = tokens; });
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::clientIdentifierChanged, this,
[this](const QString& clientIdentifier) { m_data->msaClientID = clientIdentifier; });
}
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: {
if (m_data->msaClientID != m_clientId) {
emit hideVerificationUriAndCode();
emit finished(AccountTaskState::STATE_DISABLED,
tr("Microsoft user authentication failed - client identification has changed."));
}
m_oauth2->refresh();
return;
if (m_silent) {
if (m_data->msaClientID != m_clientId) {
emit finished(AccountTaskState::STATE_DISABLED,
tr("Microsoft user authentication failed - client identification has changed."));
}
case Login: {
QVariantMap extraOpts;
extraOpts["prompt"] = "select_account";
m_oauth2->setExtraRequestParams(extraOpts);
oauth2.setRefreshToken(m_data->msaToken.refresh_token);
oauth2.refreshAccessToken();
} else {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) // QMultiMap param changed in 6.0
oauth2.setModifyParametersFunction([](QAbstractOAuth::Stage stage, QMultiMap<QString, QVariant>* map) {
#else
oauth2.setModifyParametersFunction([](QAbstractOAuth::Stage stage, QMap<QString, QVariant>* map) {
#endif
map->insert("prompt", "select_account");
});
*m_data = AccountData();
m_data->msaClientID = m_clientId;
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();
if (!extraTokens.isEmpty()) {
qCDebug(authCredentials()) << "Extra tokens in response:";
foreach (QString key, extraTokens.keys()) {
qCDebug(authCredentials()) << "\t" << key << ":" << extraTokens.value(key);
}
}
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;
}
*m_data = AccountData();
m_data->msaClientID = m_clientId;
oauth2.grant();
}
}

View File

@@ -36,30 +36,24 @@
#pragma once
#include <QObject>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h"
#include <katabasis/DeviceFlow.h>
#include <QtNetworkAuth/qoauth2authorizationcodeflow.h>
class MSAStep : public AuthStep {
Q_OBJECT
public:
enum Action { Refresh, Login };
public:
explicit MSAStep(AccountData* data, Action action);
virtual ~MSAStep() noexcept;
explicit MSAStep(AccountData* data, bool silent = false);
virtual ~MSAStep() noexcept = default;
void perform() override;
void rehydrate() override;
QString describe() override;
private slots:
void onOAuthActivityChanged(Katabasis::Activity activity);
signals:
void authorizeWithBrowser(const QUrl& url);
private:
Katabasis::DeviceFlow* m_oauth2 = nullptr;
Action m_action;
bool m_silent;
QString m_clientId;
QOAuth2AuthorizationCodeFlow oauth2;
};

View File

@@ -2,15 +2,13 @@
#include <QNetworkRequest>
#include "Logging.h"
#include "minecraft/auth/AuthRequest.h"
#include "Application.h"
#include "minecraft/auth/Parsers.h"
#include "net/NetUtils.h"
#include "net/StaticHeaderProxy.h"
MinecraftProfileStep::MinecraftProfileStep(AccountData* data) : AuthStep(data) {}
MinecraftProfileStep::~MinecraftProfileStep() noexcept = default;
QString MinecraftProfileStep::describe()
{
return tr("Fetching the Minecraft profile.");
@@ -18,52 +16,47 @@ QString MinecraftProfileStep::describe()
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());
QUrl url("https://api.minecraftservices.com/minecraft/profile");
auto headers = QList<Net::HeaderPair>{ { "Content-Type", "application/json" },
{ "Accept", "application/json" },
{ "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);
m_response.reset(new QByteArray());
m_task = Net::Download::makeByteArray(url, m_response);
m_task->addHeaderProxy(new Net::StaticHeaderProxy(headers));
connect(m_task.get(), &Task::finished, this, &MinecraftProfileStep::onRequestDone);
m_task->setNetwork(APPLICATION->network());
m_task->start();
}
void MinecraftProfileStep::rehydrate()
void MinecraftProfileStep::onRequestDone()
{
// 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();
qCDebug(authCredentials()) << data;
if (error == QNetworkReply::ContentNotFoundError) {
if (m_task->error() == QNetworkReply::ContentNotFoundError) {
// NOTE: Succeed even if we do not have a profile. This is a valid account state.
m_data->minecraftProfile = MinecraftProfile();
emit finished(AccountTaskState::STATE_SUCCEEDED, tr("Account has no Minecraft profile."));
return;
}
if (error != QNetworkReply::NoError) {
if (m_task->error() != QNetworkReply::NoError) {
qWarning() << "Error getting profile:";
qWarning() << " HTTP Status: " << requestor->httpStatus_;
qWarning() << " Internal error no.: " << error;
qWarning() << " Error string: " << requestor->errorString_;
qWarning() << " HTTP Status: " << m_task->replyStatusCode();
qWarning() << " Internal error no.: " << m_task->error();
qWarning() << " Error string: " << m_task->errorString();
qWarning() << " Response:";
qWarning() << QString::fromUtf8(data);
qWarning() << QString::fromUtf8(*m_response);
if (Net::isApplicationError(error)) {
if (Net::isApplicationError(m_task->error())) {
emit finished(AccountTaskState::STATE_FAILED_SOFT,
tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_));
tr("Minecraft Java profile acquisition failed: %1").arg(m_task->errorString()));
} else {
emit finished(AccountTaskState::STATE_OFFLINE,
tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_));
emit finished(AccountTaskState::STATE_OFFLINE, tr("Minecraft Java profile acquisition failed: %1").arg(m_task->errorString()));
}
return;
}
if (!Parsers::parseMinecraftProfile(data, m_data->minecraftProfile)) {
if (!Parsers::parseMinecraftProfile(*m_response, m_data->minecraftProfile)) {
m_data->minecraftProfile = MinecraftProfile();
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Minecraft Java profile response could not be parsed"));
return;

View File

@@ -1,21 +1,25 @@
#pragma once
#include <QObject>
#include <memory>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h"
#include "net/Download.h"
class MinecraftProfileStep : public AuthStep {
Q_OBJECT
public:
explicit MinecraftProfileStep(AccountData* data);
virtual ~MinecraftProfileStep() noexcept;
virtual ~MinecraftProfileStep() noexcept = default;
void perform() override;
void rehydrate() override;
QString describe() override;
private slots:
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
void onRequestDone();
private:
std::shared_ptr<QByteArray> m_response;
Net::Download::Ptr m_task;
};

View File

@@ -1,21 +0,0 @@
#include "OfflineStep.h"
#include "Application.h"
OfflineStep::OfflineStep(AccountData* data) : AuthStep(data) {}
OfflineStep::~OfflineStep() noexcept = default;
QString OfflineStep::describe()
{
return tr("Creating offline account.");
}
void OfflineStep::rehydrate()
{
// NOOP
}
void OfflineStep::perform()
{
emit finished(AccountTaskState::STATE_WORKING, tr("Created offline account."));
}

View File

@@ -1,19 +0,0 @@
#pragma once
#include <QObject>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h"
#include <katabasis/DeviceFlow.h>
class OfflineStep : public AuthStep {
Q_OBJECT
public:
explicit OfflineStep(AccountData* data);
virtual ~OfflineStep() noexcept;
void perform() override;
void rehydrate() override;
QString describe() override;
};

View File

@@ -4,27 +4,22 @@
#include <QJsonParseError>
#include <QNetworkRequest>
#include "Application.h"
#include "Logging.h"
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
#include "net/NetUtils.h"
#include "net/StaticHeaderProxy.h"
#include "net/Upload.h"
XboxAuthorizationStep::XboxAuthorizationStep(AccountData* data, Katabasis::Token* token, QString relyingParty, QString authorizationKind)
XboxAuthorizationStep::XboxAuthorizationStep(AccountData* data, 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(
@@ -41,40 +36,44 @@ void XboxAuthorizationStep::perform()
)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());
QUrl url("https://xsts.auth.xboxlive.com/xsts/authorize");
auto headers = QList<Net::HeaderPair>{
{ "Content-Type", "application/json" },
{ "Accept", "application/json" },
};
m_response.reset(new QByteArray());
m_task = Net::Upload::makeByteArray(url, m_response, xbox_auth_data.toUtf8());
m_task->addHeaderProxy(new Net::StaticHeaderProxy(headers));
connect(m_task.get(), &Task::finished, this, &XboxAuthorizationStep::onRequestDone);
m_task->setNetwork(APPLICATION->network());
m_task->start();
qDebug() << "Getting authorization token for " << m_relyingParty;
}
void XboxAuthorizationStep::onRequestDone(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers)
void XboxAuthorizationStep::onRequestDone()
{
auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
requestor->deleteLater();
qCDebug(authCredentials()) << data;
if (error != QNetworkReply::NoError) {
qWarning() << "Reply error:" << error;
if (Net::isApplicationError(error)) {
if (!processSTSError(error, data, headers)) {
qCDebug(authCredentials()) << *m_response;
if (m_task->error() != QNetworkReply::NoError) {
qWarning() << "Reply error:" << m_task->error();
if (Net::isApplicationError(m_task->error())) {
if (!processSTSError()) {
emit finished(AccountTaskState::STATE_FAILED_SOFT,
tr("Failed to get authorization for %1 services. Error %2.").arg(m_authorizationKind, error));
tr("Failed to get authorization for %1 services. Error %2.").arg(m_authorizationKind, m_task->error()));
} else {
emit finished(AccountTaskState::STATE_FAILED_SOFT,
tr("Unknown STS error for %1 services: %2").arg(m_authorizationKind, requestor->errorString_));
tr("Unknown STS error for %1 services: %2").arg(m_authorizationKind, m_task->errorString()));
}
} else {
emit finished(AccountTaskState::STATE_OFFLINE,
tr("Failed to get authorization for %1 services: %2").arg(m_authorizationKind, requestor->errorString_));
tr("Failed to get authorization for %1 services: %2").arg(m_authorizationKind, m_task->errorString()));
}
return;
}
Katabasis::Token temp;
if (!Parsers::parseXTokenResponse(data, temp, m_authorizationKind)) {
Token temp;
if (!Parsers::parseXTokenResponse(*m_response, temp, m_authorizationKind)) {
emit finished(AccountTaskState::STATE_FAILED_SOFT,
tr("Could not parse authorization response for access to %1 services.").arg(m_authorizationKind));
return;
@@ -91,11 +90,11 @@ void XboxAuthorizationStep::onRequestDone(QNetworkReply::NetworkError error, QBy
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)
bool XboxAuthorizationStep::processSTSError()
{
if (error == QNetworkReply::AuthenticationRequiredError) {
if (m_task->error() == QNetworkReply::AuthenticationRequiredError) {
QJsonParseError jsonError;
QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
QJsonDocument doc = QJsonDocument::fromJson(*m_response, &jsonError);
if (jsonError.error) {
qWarning() << "Cannot parse error XSTS response as JSON: " << jsonError.errorString();
emit finished(AccountTaskState::STATE_FAILED_SOFT,
@@ -126,7 +125,35 @@ bool XboxAuthorizationStep::processSTSError(QNetworkReply::NetworkError error, Q
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>"));
.arg("<a href=\"https://help.minecraft.net/hc/en-us/articles/4408968616077\">help.minecraft.net</a>"));
return true;
}
// the following codes where copied from: https://github.com/PrismarineJS/prismarine-auth/pull/44
case 2148916236: {
emit finished(AccountTaskState::STATE_FAILED_SOFT,
tr("This Microsoft account requires proof of age to play. Please login to %1 to provide proof of age.")
.arg("<a href=\"https://login.live.com/login.srf\">login.live.com</a>"));
return true;
}
case 2148916237:
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("This Microsoft account has reached its limit for playtime. This "
"Microsoft account has been blocked from logging in."));
return true;
case 2148916227: {
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("This Microsoft account was banned by Xbox for violating one or more "
"Community Standards for Xbox and is unable to be used."));
return true;
}
case 2148916229: {
emit finished(AccountTaskState::STATE_FAILED_SOFT,
tr("This Microsoft account is currently restricted and your guardian has not given you permission to play "
"online. Login to %1 and have your guardian change your permissions.")
.arg("<a href=\"https://account.microsoft.com/family/\">account.microsoft.com</a>"));
return true;
}
case 2148916234: {
emit finished(AccountTaskState::STATE_FAILED_SOFT,
tr("This Microsoft account has not accepted Xbox's Terms of Service. Please login and accept them."));
return true;
}
default: {

View File

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

View File

@@ -3,28 +3,21 @@
#include <QNetworkRequest>
#include <QUrlQuery>
#include "Application.h"
#include "Logging.h"
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
#include "net/NetUtils.h"
#include "net/StaticHeaderProxy.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");
QUrl url("https://profile.xboxlive.com/users/me/profile/settings");
QUrlQuery q;
q.addQueryItem("settings",
"GameDisplayName,AppDisplayName,AppDisplayPicRaw,GameDisplayPicRaw,"
@@ -33,36 +26,38 @@ void XboxProfileStep::perform()
"PreferredColor,Location,Bio,Watermarks,"
"RealName,RealNameOverride,IsQuarantined");
url.setQuery(q);
auto headers = QList<Net::HeaderPair>{
{ "Content-Type", "application/json" },
{ "Accept", "application/json" },
{ "x-xbl-contract-version", "3" },
{ "Authorization", QString("XBL3.0 x=%1;%2").arg(m_data->userToken.extra["uhs"].toString(), m_data->xboxApiToken.token).toUtf8() }
};
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);
m_response.reset(new QByteArray());
m_task = Net::Download::makeByteArray(url, m_response);
m_task->addHeaderProxy(new Net::StaticHeaderProxy(headers));
connect(m_task.get(), &Task::finished, this, &XboxProfileStep::onRequestDone);
m_task->setNetwork(APPLICATION->network());
m_task->start();
qDebug() << "Getting Xbox profile...";
}
void XboxProfileStep::onRequestDone(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers)
void XboxProfileStep::onRequestDone()
{
auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
requestor->deleteLater();
if (error != QNetworkReply::NoError) {
qWarning() << "Reply error:" << error;
qCDebug(authCredentials()) << data;
if (Net::isApplicationError(error)) {
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to retrieve the Xbox profile: %1").arg(requestor->errorString_));
if (m_task->error() != QNetworkReply::NoError) {
qWarning() << "Reply error:" << m_task->error();
qCDebug(authCredentials()) << *m_response;
if (Net::isApplicationError(m_task->error())) {
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to retrieve the Xbox profile: %1").arg(m_task->errorString()));
} else {
emit finished(AccountTaskState::STATE_OFFLINE, tr("Failed to retrieve the Xbox profile: %1").arg(requestor->errorString_));
emit finished(AccountTaskState::STATE_OFFLINE, tr("Failed to retrieve the Xbox profile: %1").arg(m_task->errorString()));
}
return;
}
qCDebug(authCredentials()) << "XBox profile: " << data;
qCDebug(authCredentials()) << "XBox profile: " << *m_response;
emit finished(AccountTaskState::STATE_WORKING, tr("Got Xbox profile"));
}

View File

@@ -1,21 +1,25 @@
#pragma once
#include <QObject>
#include <memory>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h"
#include "net/Download.h"
class XboxProfileStep : public AuthStep {
Q_OBJECT
public:
explicit XboxProfileStep(AccountData* data);
virtual ~XboxProfileStep() noexcept;
virtual ~XboxProfileStep() noexcept = default;
void perform() override;
void rehydrate() override;
QString describe() override;
private slots:
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
void onRequestDone();
private:
std::shared_ptr<QByteArray> m_response;
Net::Download::Ptr m_task;
};

View File

@@ -2,24 +2,18 @@
#include <QNetworkRequest>
#include "minecraft/auth/AuthRequest.h"
#include "Application.h"
#include "minecraft/auth/Parsers.h"
#include "net/NetUtils.h"
#include "net/StaticHeaderProxy.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(
@@ -35,36 +29,39 @@ void XboxUserStep::perform()
)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");
// set contract-version header (prevent err 400 bad-request?)
// https://learn.microsoft.com/en-us/gaming/gdk/_content/gc/reference/live/rest/additional/httpstandardheaders
request.setRawHeader("x-xbl-contract-version", "1");
QUrl url("https://user.auth.xboxlive.com/user/authenticate");
auto headers = QList<Net::HeaderPair>{
{ "Content-Type", "application/json" },
{ "Accept", "application/json" },
// set contract-version header (prevent err 400 bad-request?)
// https://learn.microsoft.com/en-us/gaming/gdk/_content/gc/reference/live/rest/additional/httpstandardheaders
{ "x-xbl-contract-version", "1" }
};
m_response.reset(new QByteArray());
m_task = Net::Upload::makeByteArray(url, m_response, xbox_auth_data.toUtf8());
m_task->addHeaderProxy(new Net::StaticHeaderProxy(headers));
auto* requestor = new AuthRequest(this);
connect(requestor, &AuthRequest::finished, this, &XboxUserStep::onRequestDone);
requestor->post(request, xbox_auth_data.toUtf8());
connect(m_task.get(), &Task::finished, this, &XboxUserStep::onRequestDone);
m_task->setNetwork(APPLICATION->network());
m_task->start();
qDebug() << "First layer of XBox auth ... commencing.";
}
void XboxUserStep::onRequestDone(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers)
void XboxUserStep::onRequestDone()
{
auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
requestor->deleteLater();
if (error != QNetworkReply::NoError) {
qWarning() << "Reply error:" << error;
if (Net::isApplicationError(error)) {
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("XBox user authentication failed: %1").arg(requestor->errorString_));
if (m_task->error() != QNetworkReply::NoError) {
qWarning() << "Reply error:" << m_task->error();
if (Net::isApplicationError(m_task->error())) {
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("XBox user authentication failed: %1").arg(m_task->errorString()));
} else {
emit finished(AccountTaskState::STATE_OFFLINE, tr("XBox user authentication failed: %1").arg(requestor->errorString_));
emit finished(AccountTaskState::STATE_OFFLINE, tr("XBox user authentication failed: %1").arg(m_task->errorString()));
}
return;
}
Katabasis::Token temp;
if (!Parsers::parseXTokenResponse(data, temp, "UToken")) {
Token temp;
if (!Parsers::parseXTokenResponse(*m_response, 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;

View File

@@ -1,21 +1,25 @@
#pragma once
#include <QObject>
#include <memory>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h"
#include "net/Upload.h"
class XboxUserStep : public AuthStep {
Q_OBJECT
public:
explicit XboxUserStep(AccountData* data);
virtual ~XboxUserStep() noexcept;
virtual ~XboxUserStep() noexcept = default;
void perform() override;
void rehydrate() override;
QString describe() override;
private slots:
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
void onRequestDone();
private:
std::shared_ptr<QByteArray> m_response;
Net::Upload::Ptr m_task;
};