It compiles, Yggdrasil login working except error handling
This commit is contained in:
parent
7dbaa896a6
commit
c32aaf244d
@ -109,6 +109,8 @@
|
||||
#include "icons/IconList.h"
|
||||
#include "net/HttpMetaCache.h"
|
||||
|
||||
#include "ui/GuiUtil.h"
|
||||
|
||||
#include "java/JavaInstallList.h"
|
||||
|
||||
#include "updater/ExternalUpdater.h"
|
||||
|
@ -233,8 +233,6 @@ set(MINECRAFT_SOURCES
|
||||
minecraft/auth/MinecraftAccount.h
|
||||
minecraft/auth/Parsers.cpp
|
||||
minecraft/auth/Parsers.h
|
||||
minecraft/auth/Yggdrasil.cpp
|
||||
minecraft/auth/Yggdrasil.h
|
||||
|
||||
minecraft/auth/AuthFlow.cpp
|
||||
minecraft/auth/AuthFlow.h
|
||||
@ -257,6 +255,8 @@ set(MINECRAFT_SOURCES
|
||||
minecraft/auth/steps/XboxProfileStep.h
|
||||
minecraft/auth/steps/XboxUserStep.cpp
|
||||
minecraft/auth/steps/XboxUserStep.h
|
||||
minecraft/auth/steps/YggdrasilMinecraftProfileStep.cpp
|
||||
minecraft/auth/steps/YggdrasilMinecraftProfileStep.h
|
||||
minecraft/auth/steps/YggdrasilStep.cpp
|
||||
minecraft/auth/steps/YggdrasilStep.h
|
||||
|
||||
@ -1071,8 +1071,6 @@ SET(LAUNCHER_SOURCES
|
||||
ui/dialogs/IconPickerDialog.h
|
||||
ui/dialogs/ImportResourceDialog.cpp
|
||||
ui/dialogs/ImportResourceDialog.h
|
||||
ui/dialogs/LoginDialog.cpp
|
||||
ui/dialogs/LoginDialog.h
|
||||
ui/dialogs/AuthlibInjectorLoginDialog.cpp
|
||||
ui/dialogs/AuthlibInjectorLoginDialog.h
|
||||
ui/dialogs/MSALoginDialog.cpp
|
||||
|
@ -24,13 +24,16 @@
|
||||
#include <QNetworkRequest>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
#include "net/ByteArraySink.h"
|
||||
|
||||
#include "Application.h"
|
||||
#include "BuildConfig.h"
|
||||
|
||||
CreateAuthlibInjectorAccount::CreateAuthlibInjectorAccount(QUrl url, MinecraftAccountPtr account, QString username)
|
||||
: NetRequest(), m_url(url), m_account(account), m_username(username)
|
||||
{}
|
||||
: NetRequest(), m_account(account), m_username(username)
|
||||
{
|
||||
m_url = url;
|
||||
m_sink.reset(new Sink(*this));
|
||||
}
|
||||
|
||||
QNetworkReply* CreateAuthlibInjectorAccount::getReply(QNetworkRequest& request)
|
||||
{
|
||||
@ -43,26 +46,34 @@ CreateAuthlibInjectorAccount::Ptr CreateAuthlibInjectorAccount::make(QUrl url, M
|
||||
return CreateAuthlibInjectorAccount::Ptr(new CreateAuthlibInjectorAccount(url, account, username));
|
||||
}
|
||||
|
||||
void CreateAuthlibInjectorAccount::downloadFinished()
|
||||
auto CreateAuthlibInjectorAccount::Sink::init(QNetworkRequest& request) -> Task::State
|
||||
{
|
||||
if (m_state != State::Failed) {
|
||||
QVariant header = m_reply->rawHeader("X-Authlib-Injector-API-Location");
|
||||
if (header.isValid()) {
|
||||
auto location = header.toString();
|
||||
m_url = m_url.resolved(location);
|
||||
} else {
|
||||
qDebug() << "X-Authlib-Injector-API-Location header not found!";
|
||||
}
|
||||
m_account.reset(MinecraftAccount::createFromUsernameAuthlibInjector(m_username, m_url.toString()));
|
||||
m_state = State::Succeeded;
|
||||
emit succeeded();
|
||||
return;
|
||||
return Task::State::Running;
|
||||
}
|
||||
|
||||
auto CreateAuthlibInjectorAccount::Sink::write(QByteArray& data) -> Task::State
|
||||
{
|
||||
return Task::State::Running;
|
||||
}
|
||||
|
||||
auto CreateAuthlibInjectorAccount::Sink::abort() -> Task::State
|
||||
{
|
||||
return Task::State::Failed;
|
||||
}
|
||||
|
||||
auto CreateAuthlibInjectorAccount::Sink::finalize(QNetworkReply& reply) -> Task::State
|
||||
{
|
||||
QVariant header = reply.rawHeader("X-Authlib-Injector-API-Location");
|
||||
QUrl url = m_outer.m_url;
|
||||
if (header.isValid()) {
|
||||
auto location = header.toString();
|
||||
url = url.resolved(location);
|
||||
} else {
|
||||
qDebug() << m_reply->readAll();
|
||||
m_reply.reset();
|
||||
emitFailed();
|
||||
return;
|
||||
qDebug() << "X-Authlib-Injector-API-Location header not found!";
|
||||
}
|
||||
|
||||
m_outer.m_account.reset(MinecraftAccount::createFromUsernameAuthlibInjector(m_outer.m_username, url.toString()));
|
||||
return Task::State::Succeeded;
|
||||
}
|
||||
|
||||
MinecraftAccountPtr CreateAuthlibInjectorAccount::getAccount()
|
||||
|
@ -32,12 +32,26 @@ class CreateAuthlibInjectorAccount : public Net::NetRequest {
|
||||
|
||||
MinecraftAccountPtr getAccount();
|
||||
|
||||
class Sink : public Net::Sink {
|
||||
public:
|
||||
Sink(CreateAuthlibInjectorAccount& outer) : m_outer(outer) {}
|
||||
virtual ~Sink() = default;
|
||||
|
||||
public:
|
||||
auto init(QNetworkRequest& request) -> Task::State override;
|
||||
auto write(QByteArray& data) -> Task::State override;
|
||||
auto abort() -> Task::State override;
|
||||
auto finalize(QNetworkReply& reply) -> Task::State override;
|
||||
auto hasLocalData() -> bool override { return false; }
|
||||
|
||||
private:
|
||||
CreateAuthlibInjectorAccount& m_outer;
|
||||
};
|
||||
|
||||
protected slots:
|
||||
virtual QNetworkReply* getReply(QNetworkRequest&) override;
|
||||
void downloadFinished();
|
||||
|
||||
private:
|
||||
QUrl m_url;
|
||||
MinecraftAccountPtr m_account;
|
||||
QString m_username;
|
||||
};
|
||||
|
@ -35,12 +35,12 @@
|
||||
*/
|
||||
|
||||
#include "LaunchController.h"
|
||||
#include <meta/Index.h>
|
||||
#include "Application.h"
|
||||
#include "launch/steps/PrintServers.h"
|
||||
#include "minecraft/auth/AccountData.h"
|
||||
#include "minecraft/auth/AccountList.h"
|
||||
#include "settings/MissingAuthlibInjectorBehavior.h"
|
||||
#include "ui/pages/instance/VersionPage.h"
|
||||
|
||||
#include "ui/InstanceWindow.h"
|
||||
#include "ui/MainWindow.h"
|
||||
|
@ -72,7 +72,7 @@ int main(int argc, char* argv[])
|
||||
Q_INIT_RESOURCE(multimc);
|
||||
Q_INIT_RESOURCE(backgrounds);
|
||||
Q_INIT_RESOURCE(documents);
|
||||
Q_INIT_RESOURCE(prismlauncher);
|
||||
Q_INIT_RESOURCE(fjordlauncher);
|
||||
|
||||
Q_INIT_RESOURCE(pe_dark);
|
||||
Q_INIT_RESOURCE(pe_light);
|
||||
|
@ -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;
|
||||
};
|
@ -13,6 +13,7 @@
|
||||
#include "minecraft/auth/steps/XboxAuthorizationStep.h"
|
||||
#include "minecraft/auth/steps/XboxProfileStep.h"
|
||||
#include "minecraft/auth/steps/XboxUserStep.h"
|
||||
#include "minecraft/auth/steps/YggdrasilMinecraftProfileStep.h"
|
||||
#include "minecraft/auth/steps/YggdrasilStep.h"
|
||||
#include "tasks/Task.h"
|
||||
|
||||
@ -20,7 +21,7 @@
|
||||
|
||||
#include <Application.h>
|
||||
|
||||
AuthFlow::AuthFlow(AccountData* data, Action action, QObject* parent) : Task(parent), m_data(data)
|
||||
AuthFlow::AuthFlow(AccountData* data, Action action, QObject* parent, const std::optional<QString> password) : Task(parent), m_data(data)
|
||||
{
|
||||
if (data->type == AccountType::MSA) {
|
||||
if (action == Action::DeviceCode) {
|
||||
@ -43,8 +44,8 @@ AuthFlow::AuthFlow(AccountData* data, Action action, QObject* parent) : Task(par
|
||||
m_steps.append(makeShared<MinecraftProfileStep>(m_data));
|
||||
m_steps.append(makeShared<GetSkinStep>(m_data));
|
||||
} else if (data->type == AccountType::AuthlibInjector) {
|
||||
m_steps.append(makeShared<YggdrasilStep>(m_data, QString()));
|
||||
m_steps.append(makeShared<MinecraftProfileStepMojang>(m_data));
|
||||
m_steps.append(makeShared<YggdrasilStep>(m_data, password));
|
||||
m_steps.append(makeShared<YggdrasilMinecraftProfileStep>(m_data));
|
||||
m_steps.append(makeShared<GetSkinStep>(m_data));
|
||||
}
|
||||
changeState(AccountTaskState::STATE_CREATED);
|
||||
|
@ -17,7 +17,10 @@ class AuthFlow : public Task {
|
||||
public:
|
||||
enum class Action { Refresh, Login, DeviceCode };
|
||||
|
||||
explicit AuthFlow(AccountData* data, Action action = Action::Refresh, QObject* parent = 0);
|
||||
explicit AuthFlow(AccountData* data,
|
||||
Action action = Action::Refresh,
|
||||
QObject* parent = 0,
|
||||
std::optional<QString> password = std::nullopt);
|
||||
virtual ~AuthFlow() = default;
|
||||
|
||||
void executeTask() override;
|
||||
|
@ -67,15 +67,6 @@ MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json)
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
MinecraftAccountPtr MinecraftAccount::createFromUsername(const QString& username)
|
||||
{
|
||||
auto account = makeShared<MinecraftAccount>();
|
||||
account->data.type = AccountType::Mojang;
|
||||
account->data.yggdrasilToken.extra["userName"] = username;
|
||||
account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]"));
|
||||
return account;
|
||||
}
|
||||
|
||||
MinecraftAccountPtr MinecraftAccount::createFromUsernameAuthlibInjector(const QString& username, const QString& authlibInjectorUrl)
|
||||
{
|
||||
auto account = makeShared<MinecraftAccount>();
|
||||
@ -143,11 +134,11 @@ QPixmap MinecraftAccount::getFace() const
|
||||
return skin.scaled(64, 64, Qt::KeepAspectRatio);
|
||||
}
|
||||
|
||||
shared_qobject_ptr<AuthFlow> MinecraftAccount::login(bool useDeviceCode)
|
||||
shared_qobject_ptr<AuthFlow> MinecraftAccount::login(bool useDeviceCode, std::optional<QString> password)
|
||||
{
|
||||
Q_ASSERT(m_currentTask.get() == nullptr);
|
||||
|
||||
m_currentTask.reset(new AuthFlow(&data, useDeviceCode ? AuthFlow::Action::DeviceCode : AuthFlow::Action::Login, this));
|
||||
m_currentTask.reset(new AuthFlow(&data, useDeviceCode ? AuthFlow::Action::DeviceCode : AuthFlow::Action::Login, this, password));
|
||||
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")); });
|
||||
|
@ -83,7 +83,6 @@ class MinecraftAccount : public QObject, public Usable {
|
||||
//! Default constructor
|
||||
explicit MinecraftAccount(QObject* parent = 0);
|
||||
|
||||
static MinecraftAccountPtr createFromUsername(const QString& username);
|
||||
static MinecraftAccountPtr createFromUsernameAuthlibInjector(const QString& username, const QString& authlibInjectorUrl);
|
||||
|
||||
static MinecraftAccountPtr createBlankMSA();
|
||||
@ -98,7 +97,7 @@ class MinecraftAccount : public QObject, public Usable {
|
||||
QJsonObject saveToJson() const;
|
||||
|
||||
public: /* manipulation */
|
||||
shared_qobject_ptr<AuthFlow> login(bool useDeviceCode = false);
|
||||
shared_qobject_ptr<AuthFlow> login(bool useDeviceCode = false, std::optional<QString> password = std::nullopt);
|
||||
|
||||
shared_qobject_ptr<AuthFlow> refresh();
|
||||
|
||||
|
@ -1,354 +0,0 @@
|
||||
/* 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 "Yggdrasil.h"
|
||||
#include "AccountData.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkReply>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
#include "Application.h"
|
||||
#include "BuildConfig.h"
|
||||
|
||||
Yggdrasil::Yggdrasil(AccountData* data, QObject* parent) : AccountTask(data, parent)
|
||||
{
|
||||
changeState(AccountTaskState::STATE_CREATED);
|
||||
}
|
||||
|
||||
void Yggdrasil::sendRequest(QUrl endpoint, QByteArray content)
|
||||
{
|
||||
changeState(AccountTaskState::STATE_WORKING);
|
||||
|
||||
QNetworkRequest netRequest(endpoint);
|
||||
netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
m_netReply = APPLICATION->network()->post(netRequest, content);
|
||||
connect(m_netReply, &QNetworkReply::finished, this, &Yggdrasil::processReply);
|
||||
connect(m_netReply, &QNetworkReply::uploadProgress, this, &Yggdrasil::refreshTimers);
|
||||
connect(m_netReply, &QNetworkReply::downloadProgress, this, &Yggdrasil::refreshTimers);
|
||||
connect(m_netReply, &QNetworkReply::sslErrors, this, &Yggdrasil::sslErrors);
|
||||
timeout_keeper.setSingleShot(true);
|
||||
timeout_keeper.start(timeout_max);
|
||||
counter.setSingleShot(false);
|
||||
counter.start(time_step);
|
||||
progress(0, timeout_max);
|
||||
connect(&timeout_keeper, &QTimer::timeout, this, &Yggdrasil::abortByTimeout);
|
||||
connect(&counter, &QTimer::timeout, this, &Yggdrasil::heartbeat);
|
||||
}
|
||||
|
||||
void Yggdrasil::executeTask() {}
|
||||
|
||||
void Yggdrasil::refresh()
|
||||
{
|
||||
start();
|
||||
/*
|
||||
* {
|
||||
* "clientToken": "client identifier"
|
||||
* "accessToken": "current access token to be refreshed"
|
||||
* "selectedProfile": // specifying this causes errors
|
||||
* {
|
||||
* "id": "profile ID"
|
||||
* "name": "profile name"
|
||||
* }
|
||||
* "requestUser": true/false // request the user structure
|
||||
* }
|
||||
*/
|
||||
QJsonObject req;
|
||||
req.insert("clientToken", m_data->clientToken());
|
||||
req.insert("accessToken", m_data->accessToken());
|
||||
/*
|
||||
{
|
||||
auto currentProfile = m_account->currentProfile();
|
||||
QJsonObject profile;
|
||||
profile.insert("id", currentProfile->id());
|
||||
profile.insert("name", currentProfile->name());
|
||||
req.insert("selectedProfile", profile);
|
||||
}
|
||||
*/
|
||||
req.insert("requestUser", false);
|
||||
QJsonDocument doc(req);
|
||||
|
||||
QUrl reqUrl(m_data->authServerUrl() + "/refresh");
|
||||
QByteArray requestData = doc.toJson();
|
||||
|
||||
sendRequest(reqUrl, requestData);
|
||||
}
|
||||
|
||||
void Yggdrasil::login(QString password)
|
||||
{
|
||||
start();
|
||||
/*
|
||||
* {
|
||||
* "agent": { // optional
|
||||
* "name": "Minecraft", // So far this is the only encountered value
|
||||
* "version": 1 // This number might be increased
|
||||
* // by the vanilla client in the future
|
||||
* },
|
||||
* "username": "mojang account name", // Can be an email address or player name for
|
||||
* // unmigrated accounts
|
||||
* "password": "mojang account password",
|
||||
* "clientToken": "client identifier", // optional
|
||||
* "requestUser": true/false // request the user structure
|
||||
* }
|
||||
*/
|
||||
QJsonObject req;
|
||||
|
||||
{
|
||||
QJsonObject agent;
|
||||
// C++ makes string literals void* for some stupid reason, so we have to tell it
|
||||
// QString... Thanks Obama.
|
||||
agent.insert("name", QString("Minecraft"));
|
||||
agent.insert("version", 1);
|
||||
req.insert("agent", agent);
|
||||
}
|
||||
|
||||
req.insert("username", m_data->userName());
|
||||
req.insert("password", password);
|
||||
req.insert("requestUser", false);
|
||||
|
||||
// If we already have a client token, give it to the server.
|
||||
// Otherwise, let the server give us one.
|
||||
|
||||
m_data->generateClientTokenIfMissing();
|
||||
req.insert("clientToken", m_data->clientToken());
|
||||
|
||||
QJsonDocument doc(req);
|
||||
|
||||
QUrl reqUrl(m_data->authServerUrl() + "/authenticate");
|
||||
QNetworkRequest netRequest(reqUrl);
|
||||
QByteArray requestData = doc.toJson();
|
||||
|
||||
sendRequest(reqUrl, requestData);
|
||||
}
|
||||
|
||||
void Yggdrasil::refreshTimers(qint64, qint64)
|
||||
{
|
||||
timeout_keeper.stop();
|
||||
timeout_keeper.start(timeout_max);
|
||||
progress(count = 0, timeout_max);
|
||||
}
|
||||
|
||||
void Yggdrasil::heartbeat()
|
||||
{
|
||||
count += time_step;
|
||||
progress(count, timeout_max);
|
||||
}
|
||||
|
||||
bool Yggdrasil::abort()
|
||||
{
|
||||
progress(timeout_max, timeout_max);
|
||||
// TODO: actually use this in a meaningful way
|
||||
m_aborted = Yggdrasil::BY_USER;
|
||||
m_netReply->abort();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Yggdrasil::abortByTimeout()
|
||||
{
|
||||
progress(timeout_max, timeout_max);
|
||||
// TODO: actually use this in a meaningful way
|
||||
m_aborted = Yggdrasil::BY_TIMEOUT;
|
||||
m_netReply->abort();
|
||||
}
|
||||
|
||||
void Yggdrasil::sslErrors(QList<QSslError> errors)
|
||||
{
|
||||
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 Yggdrasil::processResponse(QJsonObject responseData)
|
||||
{
|
||||
// Read the response data. We need to get the client token, access token, and the selected
|
||||
// profile.
|
||||
qDebug() << "Processing authentication response.";
|
||||
|
||||
// qDebug() << responseData;
|
||||
// If we already have a client token, make sure the one the server gave us matches our
|
||||
// existing one.
|
||||
QString clientToken = responseData.value("clientToken").toString("");
|
||||
if (clientToken.isEmpty()) {
|
||||
// Fail if the server gave us an empty 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(AccountTaskState::STATE_FAILED_HARD,
|
||||
tr("Authentication server attempted to change the client token. This isn't supported."));
|
||||
return;
|
||||
}
|
||||
|
||||
// Now, we set the access token.
|
||||
qDebug() << "Getting access token.";
|
||||
QString accessToken = responseData.value("accessToken").toString("");
|
||||
if (accessToken.isEmpty()) {
|
||||
// Fail if the server didn't give us an access token.
|
||||
changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send an access token."));
|
||||
return;
|
||||
}
|
||||
// Set the access token.
|
||||
m_data->yggdrasilToken.token = accessToken;
|
||||
m_data->yggdrasilToken.validity = Katabasis::Validity::Certain;
|
||||
m_data->yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc();
|
||||
|
||||
// Get UUID here since we need it for later
|
||||
// FIXME: Here is a simple workaround for now,, which uses the first available profile when selectedProfile is not provided
|
||||
auto profile = responseData.value("selectedProfile");
|
||||
if (!profile.isObject()) {
|
||||
auto profiles = responseData.value("availableProfiles");
|
||||
if (!profiles.isArray()) {
|
||||
changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send available profiles."));
|
||||
return;
|
||||
} else {
|
||||
if (profiles.toArray().isEmpty()) {
|
||||
changeState(AccountTaskState::STATE_FAILED_HARD, tr("Account has no available profile."));
|
||||
return;
|
||||
} else {
|
||||
profile = profiles.toArray().first();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto profileObj = profile.toObject();
|
||||
for (auto i = profileObj.constBegin(); i != profileObj.constEnd(); ++i) {
|
||||
if (i.key() == "name" && i.value().isString()) {
|
||||
m_data->minecraftProfile.name = i->toString();
|
||||
} else if (i.key() == "id" && i.value().isString()) {
|
||||
m_data->minecraftProfile.id = i->toString();
|
||||
}
|
||||
}
|
||||
|
||||
if (m_data->minecraftProfile.id.isEmpty()) {
|
||||
changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send a UUID in selected profile."));
|
||||
return;
|
||||
}
|
||||
|
||||
// We've made it through the minefield of possible errors. Return true to indicate that
|
||||
// we've succeeded.
|
||||
qDebug() << "Finished reading authentication response.";
|
||||
changeState(AccountTaskState::STATE_SUCCEEDED);
|
||||
}
|
||||
|
||||
void Yggdrasil::processReply()
|
||||
{
|
||||
changeState(AccountTaskState::STATE_WORKING);
|
||||
|
||||
switch (m_netReply->error()) {
|
||||
case QNetworkReply::NoError:
|
||||
break;
|
||||
case QNetworkReply::TimeoutError:
|
||||
changeState(AccountTaskState::STATE_FAILED_SOFT, tr("Authentication operation timed out."));
|
||||
return;
|
||||
case QNetworkReply::OperationCanceledError:
|
||||
changeState(AccountTaskState::STATE_FAILED_SOFT, tr("Authentication operation cancelled."));
|
||||
return;
|
||||
case QNetworkReply::SslHandshakeFailedError:
|
||||
changeState(AccountTaskState::STATE_FAILED_SOFT,
|
||||
tr("<b>SSL Handshake failed.</b><br/>There might be a few causes for it:<br/>"
|
||||
"<ul>"
|
||||
"<li>You use Windows and need to update your root certificates, please install any outstanding updates.</li>"
|
||||
"<li>Some device on your network is interfering with SSL traffic. In that case, "
|
||||
"you have bigger worries than Minecraft not starting.</li>"
|
||||
"<li>Possibly something else. Check the log file for details</li>"
|
||||
"</ul>"));
|
||||
return;
|
||||
// used for invalid credentials and similar errors. Fall through.
|
||||
case QNetworkReply::ContentAccessDenied:
|
||||
case QNetworkReply::ContentOperationNotPermittedError:
|
||||
break;
|
||||
case QNetworkReply::ContentGoneError: {
|
||||
changeState(AccountTaskState::STATE_FAILED_GONE,
|
||||
tr("The Mojang account no longer exists. It may have been migrated to a Microsoft account."));
|
||||
return;
|
||||
}
|
||||
default:
|
||||
changeState(AccountTaskState::STATE_FAILED_SOFT, tr("Authentication operation failed due to a network error: %1 (%2)")
|
||||
.arg(m_netReply->errorString())
|
||||
.arg(m_netReply->error()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to parse the response regardless of the response code.
|
||||
// Sometimes the auth server will give more information and an error code.
|
||||
QJsonParseError jsonError;
|
||||
QByteArray replyData = m_netReply->readAll();
|
||||
QJsonDocument doc = QJsonDocument::fromJson(replyData, &jsonError);
|
||||
// Check the response code.
|
||||
int responseCode = m_netReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
|
||||
if (responseCode == 200) {
|
||||
// If the response code was 200, then there shouldn't be an error. Make sure
|
||||
// anyways.
|
||||
// Also, sometimes an empty reply indicates success. If there was no data received,
|
||||
// pass an empty json object to the processResponse function.
|
||||
if (jsonError.error == QJsonParseError::NoError || replyData.size() == 0) {
|
||||
processResponse(replyData.size() > 0 ? doc.object() : QJsonObject());
|
||||
return;
|
||||
} else {
|
||||
changeState(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;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If the response code was not 200, then Yggdrasil may have given us information
|
||||
// about the error.
|
||||
// If we can parse the response, then get information from it. Otherwise just say
|
||||
// there was an unknown error.
|
||||
if (jsonError.error == QJsonParseError::NoError) {
|
||||
// We were able to parse the server's response. Woo!
|
||||
// Call processError. If a subclass has overridden it then they'll handle their
|
||||
// stuff there.
|
||||
qDebug() << "The request failed, but the server gave us an error message. Processing error.";
|
||||
processError(doc.object());
|
||||
} else {
|
||||
// The server didn't say anything regarding the error. Give the user an unknown
|
||||
// error.
|
||||
qDebug() << "The request failed and the server gave no error message. Unknown error.";
|
||||
changeState(
|
||||
AccountTaskState::STATE_FAILED_SOFT,
|
||||
tr("An unknown error occurred when trying to communicate with the authentication server: %1").arg(m_netReply->errorString()));
|
||||
}
|
||||
}
|
||||
|
||||
void Yggdrasil::processError(QJsonObject responseData)
|
||||
{
|
||||
QJsonValue errorVal = responseData.value("error");
|
||||
QJsonValue errorMessageValue = responseData.value("errorMessage");
|
||||
QJsonValue causeVal = responseData.value("cause");
|
||||
|
||||
if (errorVal.isString() && errorMessageValue.isString()) {
|
||||
m_error = std::shared_ptr<Error>(new Error{ errorVal.toString(""), errorMessageValue.toString(""), causeVal.toString("") });
|
||||
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(AccountTaskState::STATE_FAILED_HARD, tr("An unknown Yggdrasil error occurred."));
|
||||
}
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
/* 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 "AccountTask.h"
|
||||
|
||||
#include <qsslerror.h>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
|
||||
#include "MinecraftAccount.h"
|
||||
|
||||
class QNetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
|
||||
/**
|
||||
* A Yggdrasil task is a task that performs an operation on a given mojang account.
|
||||
*/
|
||||
class Yggdrasil : public AccountTask {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit Yggdrasil(AccountData* data, QObject* parent = 0);
|
||||
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;
|
||||
|
||||
/**
|
||||
* Processes the response received from the server.
|
||||
* If an error occurred, this should emit a failed signal.
|
||||
* If Yggdrasil gave an error response, it should call setError() first, and then return false.
|
||||
* Otherwise, it should return true.
|
||||
* Note: If the response from the server was blank, and the HTTP code was 200, this function is called with
|
||||
* an empty QJsonObject.
|
||||
*/
|
||||
void processResponse(QJsonObject responseData);
|
||||
|
||||
/**
|
||||
* Processes an error response received from the server.
|
||||
* The default implementation will read data from Yggdrasil's standard error response format and set it as this task's Error.
|
||||
* \returns a QString error message that will be passed to emitFailed.
|
||||
*/
|
||||
virtual void processError(QJsonObject responseData);
|
||||
|
||||
protected slots:
|
||||
void processReply();
|
||||
void refreshTimers(qint64, qint64);
|
||||
void heartbeat();
|
||||
void sslErrors(QList<QSslError>);
|
||||
void abortByTimeout();
|
||||
|
||||
public slots:
|
||||
virtual bool abort() override;
|
||||
|
||||
private:
|
||||
void sendRequest(QUrl endpoint, QByteArray content);
|
||||
|
||||
protected:
|
||||
QNetworkReply* m_netReply = nullptr;
|
||||
QTimer timeout_keeper;
|
||||
QTimer counter;
|
||||
int count = 0; // num msec since time reset
|
||||
|
||||
const int timeout_max = 30000;
|
||||
const int time_step = 50;
|
||||
};
|
@ -1,88 +0,0 @@
|
||||
#include "MinecraftProfileStepMojang.h"
|
||||
|
||||
#include <QNetworkRequest>
|
||||
|
||||
#include "BuildConfig.h"
|
||||
#include "Logging.h"
|
||||
#include "minecraft/auth/AuthRequest.h"
|
||||
#include "minecraft/auth/Parsers.h"
|
||||
#include "net/NetUtils.h"
|
||||
|
||||
MinecraftProfileStepMojang::MinecraftProfileStepMojang(AccountData* data) : AuthStep(data) {}
|
||||
|
||||
MinecraftProfileStepMojang::~MinecraftProfileStepMojang() noexcept = default;
|
||||
|
||||
QString MinecraftProfileStepMojang::describe()
|
||||
{
|
||||
return tr("Fetching the Minecraft profile.");
|
||||
}
|
||||
|
||||
void MinecraftProfileStepMojang::perform()
|
||||
{
|
||||
if (m_data->minecraftProfile.id.isEmpty()) {
|
||||
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("A UUID is required to get the profile."));
|
||||
return;
|
||||
}
|
||||
|
||||
// use session server instead of profile due to profile endpoint being locked for locked Mojang accounts
|
||||
QUrl url = QUrl(m_data->sessionServerUrl() + "/session/minecraft/profile/" + m_data->minecraftProfile.id);
|
||||
QNetworkRequest req = QNetworkRequest(url);
|
||||
AuthRequest* request = new AuthRequest(this);
|
||||
connect(request, &AuthRequest::finished, this, &MinecraftProfileStepMojang::onRequestDone);
|
||||
request->get(req);
|
||||
}
|
||||
|
||||
void MinecraftProfileStepMojang::rehydrate()
|
||||
{
|
||||
// NOOP, for now. We only save bools and there's nothing to check.
|
||||
}
|
||||
|
||||
void MinecraftProfileStepMojang::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) {
|
||||
// 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) {
|
||||
qWarning() << "Error getting profile:";
|
||||
qWarning() << " HTTP Status: " << requestor->httpStatus_;
|
||||
qWarning() << " Internal error no.: " << error;
|
||||
qWarning() << " Error string: " << requestor->errorString_;
|
||||
|
||||
qWarning() << " Response:";
|
||||
qWarning() << QString::fromUtf8(data);
|
||||
|
||||
if (Net::isApplicationError(error)) {
|
||||
emit finished(AccountTaskState::STATE_FAILED_SOFT,
|
||||
tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_));
|
||||
} else {
|
||||
emit finished(AccountTaskState::STATE_OFFLINE,
|
||||
tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!Parsers::parseMinecraftProfileMojang(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."));
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
#pragma once
|
||||
#include <QObject>
|
||||
|
||||
#include "QObjectPtr.h"
|
||||
#include "minecraft/auth/AuthStep.h"
|
||||
|
||||
class MinecraftProfileStepMojang : public AuthStep {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MinecraftProfileStepMojang(AccountData* data);
|
||||
virtual ~MinecraftProfileStepMojang() noexcept;
|
||||
|
||||
void perform() override;
|
||||
void rehydrate() override;
|
||||
|
||||
QString describe() override;
|
||||
|
||||
private slots:
|
||||
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
|
||||
};
|
@ -0,0 +1,73 @@
|
||||
#include "YggdrasilMinecraftProfileStep.h"
|
||||
|
||||
#include <QNetworkRequest>
|
||||
|
||||
#include "Application.h"
|
||||
#include "minecraft/auth/Parsers.h"
|
||||
#include "net/NetUtils.h"
|
||||
#include "net/RawHeaderProxy.h"
|
||||
|
||||
YggdrasilMinecraftProfileStep::YggdrasilMinecraftProfileStep(AccountData* data) : AuthStep(data) {}
|
||||
|
||||
QString YggdrasilMinecraftProfileStep::describe()
|
||||
{
|
||||
return tr("Fetching the Minecraft profile.");
|
||||
}
|
||||
|
||||
void YggdrasilMinecraftProfileStep::perform()
|
||||
{
|
||||
if (m_data->minecraftProfile.id.isEmpty()) {
|
||||
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("A UUID is required to get the profile."));
|
||||
return;
|
||||
}
|
||||
|
||||
QUrl url = QUrl(m_data->sessionServerUrl() + "/session/minecraft/profile/" + m_data->minecraftProfile.id);
|
||||
auto headers = QList<Net::HeaderPair>{ { "Content-Type", "application/json" }, { "Accept", "application/json" } };
|
||||
|
||||
m_response.reset(new QByteArray());
|
||||
m_request = Net::Download::makeByteArray(url, m_response);
|
||||
m_request->addHeaderProxy(new Net::RawHeaderProxy(headers));
|
||||
|
||||
m_task.reset(new NetJob("MinecraftProfileStep", APPLICATION->network()));
|
||||
m_task->setAskRetry(false);
|
||||
m_task->addNetAction(m_request);
|
||||
|
||||
connect(m_task.get(), &Task::finished, this, &YggdrasilMinecraftProfileStep::onRequestDone);
|
||||
|
||||
m_task->start();
|
||||
}
|
||||
|
||||
void YggdrasilMinecraftProfileStep::onRequestDone()
|
||||
{
|
||||
if (m_request->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_WORKING, tr("Account has no Minecraft profile."));
|
||||
return;
|
||||
}
|
||||
if (m_request->error() != QNetworkReply::NoError) {
|
||||
qWarning() << "Error getting profile:";
|
||||
qWarning() << " HTTP Status: " << m_request->replyStatusCode();
|
||||
qWarning() << " Internal error no.: " << m_request->error();
|
||||
qWarning() << " Error string: " << m_request->errorString();
|
||||
|
||||
qWarning() << " Response:";
|
||||
qWarning() << QString::fromUtf8(*m_response);
|
||||
|
||||
if (Net::isApplicationError(m_request->error())) {
|
||||
emit finished(AccountTaskState::STATE_FAILED_SOFT,
|
||||
tr("Minecraft Java profile acquisition failed: %1").arg(m_request->errorString()));
|
||||
} else {
|
||||
emit finished(AccountTaskState::STATE_OFFLINE,
|
||||
tr("Minecraft Java profile acquisition failed: %1").arg(m_request->errorString()));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!Parsers::parseMinecraftProfileMojang(*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;
|
||||
}
|
||||
|
||||
emit finished(AccountTaskState::STATE_WORKING, tr("Minecraft Java profile acquisition succeeded."));
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
#include <QObject>
|
||||
|
||||
#include "minecraft/auth/AuthStep.h"
|
||||
#include "net/Download.h"
|
||||
#include "net/NetJob.h"
|
||||
|
||||
class YggdrasilMinecraftProfileStep : public AuthStep {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit YggdrasilMinecraftProfileStep(AccountData* data);
|
||||
virtual ~YggdrasilMinecraftProfileStep() noexcept = default;
|
||||
|
||||
void perform() override;
|
||||
|
||||
QString describe() override;
|
||||
|
||||
private slots:
|
||||
void onRequestDone();
|
||||
|
||||
private:
|
||||
std::shared_ptr<QByteArray> m_response;
|
||||
Net::Download::Ptr m_request;
|
||||
NetJob::Ptr m_task;
|
||||
};
|
@ -1,16 +1,9 @@
|
||||
#include "YggdrasilStep.h"
|
||||
#include "Application.h"
|
||||
|
||||
#include "minecraft/auth/Parsers.h"
|
||||
#include "minecraft/auth/Yggdrasil.h"
|
||||
#include "net/RawHeaderProxy.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);
|
||||
connect(m_yggdrasil, &Task::aborted, this, &YggdrasilStep::onAuthFailed);
|
||||
}
|
||||
YggdrasilStep::YggdrasilStep(AccountData* data, std::optional<QString> password) : AuthStep(data), m_password(password) {}
|
||||
|
||||
QString YggdrasilStep::describe()
|
||||
{
|
||||
@ -19,27 +12,195 @@ QString YggdrasilStep::describe()
|
||||
|
||||
void YggdrasilStep::perform()
|
||||
{
|
||||
if (m_password.size()) {
|
||||
m_yggdrasil->login(m_password);
|
||||
if (m_password) {
|
||||
login(*m_password);
|
||||
} else {
|
||||
m_yggdrasil->refresh();
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
void YggdrasilStep::onAuthSucceeded()
|
||||
void YggdrasilStep::login(QString password)
|
||||
{
|
||||
emit finished(AccountTaskState::STATE_WORKING, tr("Logged in with Mojang"));
|
||||
}
|
||||
QUrl url(m_data->authServerUrl() + "/authenticate");
|
||||
auto headers = QList<Net::HeaderPair>{ { "Content-Type", "application/json" }, { "Accept", "application/json" } };
|
||||
|
||||
void YggdrasilStep::onAuthFailed()
|
||||
{
|
||||
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.");
|
||||
/*
|
||||
* {
|
||||
* "agent": { // optional
|
||||
* "name": "Minecraft", // So far this is the only encountered value
|
||||
* "version": 1 // This number might be increased
|
||||
* // by the vanilla client in the future
|
||||
* },
|
||||
* "username": "mojang account name", // Can be an email address or player name for
|
||||
* // unmigrated accounts
|
||||
* "password": "mojang account password",
|
||||
* "clientToken": "client identifier", // optional
|
||||
* "requestUser": true/false // request the user structure
|
||||
* }
|
||||
*/
|
||||
QJsonObject req;
|
||||
{
|
||||
QJsonObject agent;
|
||||
agent.insert("name", QString("Minecraft"));
|
||||
agent.insert("version", 1);
|
||||
req.insert("agent", agent);
|
||||
}
|
||||
emit finished(state, errorMessage);
|
||||
req.insert("username", m_data->userName());
|
||||
req.insert("password", password);
|
||||
req.insert("requestUser", false);
|
||||
//
|
||||
// If we already have a client token, give it to the server.
|
||||
// Otherwise, let the server give us one.
|
||||
|
||||
m_data->generateClientTokenIfMissing();
|
||||
req.insert("clientToken", m_data->clientToken());
|
||||
|
||||
QJsonDocument doc(req);
|
||||
QByteArray requestData = doc.toJson();
|
||||
|
||||
m_response.reset(new QByteArray());
|
||||
m_request = Net::Upload::makeByteArray(url, m_response, requestData);
|
||||
m_request->addHeaderProxy(new Net::RawHeaderProxy(headers));
|
||||
|
||||
m_task.reset(new NetJob("YggdrasilStep", APPLICATION->network()));
|
||||
m_task->setAskRetry(false);
|
||||
m_task->addNetAction(m_request);
|
||||
|
||||
connect(m_task.get(), &Task::finished, this, &YggdrasilStep::onRequestDone);
|
||||
|
||||
m_task->start();
|
||||
}
|
||||
|
||||
void YggdrasilStep::refresh()
|
||||
{
|
||||
QUrl url(m_data->authServerUrl() + "/refresh");
|
||||
auto headers = QList<Net::HeaderPair>{ { "Content-Type", "application/json" }, { "Accept", "application/json" } };
|
||||
|
||||
/*
|
||||
* {
|
||||
* "clientToken": "client identifier"
|
||||
* "accessToken": "current access token to be refreshed"
|
||||
* "selectedProfile": // specifying this causes errors
|
||||
* {
|
||||
* "id": "profile ID"
|
||||
* "name": "profile name"
|
||||
* }
|
||||
* "requestUser": true/false // request the user structure
|
||||
* }
|
||||
*/
|
||||
QJsonObject req;
|
||||
req.insert("clientToken", m_data->clientToken());
|
||||
req.insert("accessToken", m_data->accessToken());
|
||||
req.insert("requestUser", false);
|
||||
|
||||
QJsonDocument doc(req);
|
||||
QByteArray requestData = doc.toJson();
|
||||
|
||||
m_response.reset(new QByteArray());
|
||||
m_request = Net::Upload::makeByteArray(url, m_response, requestData);
|
||||
m_request->addHeaderProxy(new Net::RawHeaderProxy(headers));
|
||||
|
||||
m_task.reset(new NetJob("YggdrasilStep", APPLICATION->network()));
|
||||
m_task->setAskRetry(false);
|
||||
m_task->addNetAction(m_request);
|
||||
|
||||
connect(m_task.get(), &Task::finished, this, &YggdrasilStep::onRequestDone);
|
||||
|
||||
m_task->start();
|
||||
}
|
||||
|
||||
void YggdrasilStep::onRequestDone()
|
||||
{
|
||||
// TODO handle errors
|
||||
|
||||
QJsonParseError jsonError;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(*m_response, &jsonError);
|
||||
YggdrasilStep::processResponse(doc.object());
|
||||
}
|
||||
|
||||
void YggdrasilStep::processResponse(QJsonObject responseData)
|
||||
{
|
||||
// Read the response data. We need to get the client token, access token, and the selected
|
||||
// profile.
|
||||
qDebug() << "Processing authentication response.";
|
||||
|
||||
// qDebug() << responseData;
|
||||
// If we already have a client token, make sure the one the server gave us matches our
|
||||
// existing one.
|
||||
QString clientToken = responseData.value("clientToken").toString("");
|
||||
if (clientToken.isEmpty()) {
|
||||
// Fail if the server gave us an empty client token
|
||||
emit finished(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()) {
|
||||
emit finished(AccountTaskState::STATE_FAILED_HARD,
|
||||
tr("Authentication server attempted to change the client token. This isn't supported."));
|
||||
return;
|
||||
}
|
||||
|
||||
// Now, we set the access token.
|
||||
qDebug() << "Getting access token.";
|
||||
QString accessToken = responseData.value("accessToken").toString("");
|
||||
if (accessToken.isEmpty()) {
|
||||
// Fail if the server didn't give us an access token.
|
||||
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send an access token."));
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the access token.
|
||||
m_data->yggdrasilToken.token = accessToken;
|
||||
m_data->yggdrasilToken.validity = Validity::Certain;
|
||||
m_data->yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc();
|
||||
|
||||
// Get UUID here since we need it for later
|
||||
// FIXME: Here is a simple workaround for now,, which uses the first available profile when selectedProfile is not provided
|
||||
auto profile = responseData.value("selectedProfile");
|
||||
if (!profile.isObject()) {
|
||||
auto profiles = responseData.value("availableProfiles");
|
||||
if (!profiles.isArray()) {
|
||||
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send available profiles."));
|
||||
return;
|
||||
} else {
|
||||
if (profiles.toArray().isEmpty()) {
|
||||
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Account has no available profile."));
|
||||
return;
|
||||
} else {
|
||||
profile = profiles.toArray().first();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto profileObj = profile.toObject();
|
||||
for (auto i = profileObj.constBegin(); i != profileObj.constEnd(); ++i) {
|
||||
if (i.key() == "name" && i.value().isString()) {
|
||||
m_data->minecraftProfile.name = i->toString();
|
||||
} else if (i.key() == "id" && i.value().isString()) {
|
||||
m_data->minecraftProfile.id = i->toString();
|
||||
}
|
||||
}
|
||||
|
||||
if (m_data->minecraftProfile.id.isEmpty()) {
|
||||
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send a UUID in selected profile."));
|
||||
return;
|
||||
}
|
||||
|
||||
emit finished(AccountTaskState::STATE_WORKING, "Logged in");
|
||||
}
|
||||
|
||||
void YggdrasilStep::processError(QJsonObject responseData)
|
||||
{
|
||||
/*QJsonValue errorVal = responseData.value("error");*/
|
||||
/*QJsonValue errorMessageValue = responseData.value("errorMessage");*/
|
||||
/*QJsonValue causeVal = responseData.value("cause");*/
|
||||
/**/
|
||||
/*if (errorVal.isString() && errorMessageValue.isString()) {*/
|
||||
/* m_error = std::shared_ptr<Error>(new Error{ errorVal.toString(""), errorMessageValue.toString(""), causeVal.toString("") });*/
|
||||
/* 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(AccountTaskState::STATE_FAILED_HARD, tr("An unknown Yggdrasil error occurred."));*/
|
||||
/*}*/
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
#pragma once
|
||||
#include <QObject>
|
||||
|
||||
#include "QObjectPtr.h"
|
||||
#include "minecraft/auth/AuthStep.h"
|
||||
#include "net/NetJob.h"
|
||||
#include "net/Upload.h"
|
||||
|
||||
class Yggdrasil;
|
||||
|
||||
@ -10,7 +11,7 @@ class YggdrasilStep : public AuthStep {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit YggdrasilStep(AccountData* data, QString password);
|
||||
explicit YggdrasilStep(AccountData* data, std::optional<QString> password);
|
||||
virtual ~YggdrasilStep() noexcept = default;
|
||||
|
||||
void perform() override;
|
||||
@ -18,10 +19,17 @@ class YggdrasilStep : public AuthStep {
|
||||
QString describe() override;
|
||||
|
||||
private slots:
|
||||
void onAuthSucceeded();
|
||||
void onAuthFailed();
|
||||
void onRequestDone();
|
||||
|
||||
private:
|
||||
Yggdrasil* m_yggdrasil = nullptr;
|
||||
QString m_password;
|
||||
void login(QString password);
|
||||
void refresh();
|
||||
|
||||
void processResponse(QJsonObject responseData);
|
||||
void processError(QJsonObject responseData);
|
||||
|
||||
std::optional<QString> m_password;
|
||||
std::shared_ptr<QByteArray> m_response;
|
||||
Net::Upload::Ptr m_request;
|
||||
NetJob::Ptr m_task;
|
||||
};
|
||||
|
@ -139,7 +139,6 @@ void PackInstallTask::resolveMods()
|
||||
m_file_id_map.clear();
|
||||
|
||||
Flame::Manifest manifest;
|
||||
int index = 0;
|
||||
|
||||
for (auto const& file : m_version.files) {
|
||||
if (!file.serverOnly && file.url.isEmpty()) {
|
||||
@ -151,15 +150,12 @@ void PackInstallTask::resolveMods()
|
||||
Flame::File flame_file;
|
||||
flame_file.projectId = file.curseforge.project_id;
|
||||
flame_file.fileId = file.curseforge.file_id;
|
||||
flame_file.hash = file.sha1;
|
||||
|
||||
manifest.files.insert(flame_file.fileId, flame_file);
|
||||
m_file_id_map.append(flame_file.fileId);
|
||||
} else {
|
||||
m_file_id_map.append(-1);
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
m_mod_id_resolver_task.reset(new Flame::FileResolvingTask(APPLICATION->network(), manifest));
|
||||
@ -188,11 +184,11 @@ void PackInstallTask::onResolveModsSucceeded()
|
||||
VersionFile& local_file = m_version.files[index];
|
||||
|
||||
// First check for blocked mods
|
||||
if (!results_file.resolved || results_file.url.isEmpty()) {
|
||||
if (!results_file.version.downloadUrl.isEmpty()) {
|
||||
BlockedMod blocked_mod;
|
||||
blocked_mod.name = local_file.name;
|
||||
blocked_mod.websiteUrl = results_file.websiteUrl;
|
||||
blocked_mod.hash = results_file.hash;
|
||||
blocked_mod.websiteUrl = results_file.pack.websiteUrl;
|
||||
blocked_mod.hash = results_file.version.hash;
|
||||
blocked_mod.matched = false;
|
||||
blocked_mod.localPath = "";
|
||||
blocked_mod.targetFolder = results_file.targetFolder;
|
||||
@ -201,7 +197,7 @@ void PackInstallTask::onResolveModsSucceeded()
|
||||
|
||||
anyBlocked = true;
|
||||
} else {
|
||||
local_file.url = results_file.url.toString();
|
||||
local_file.url = results_file.version.downloadUrl;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,14 +21,13 @@
|
||||
#include "ui_AuthlibInjectorLoginDialog.h"
|
||||
|
||||
#include "CreateAuthlibInjectorAccount.h"
|
||||
#include "minecraft/auth/AccountTask.h"
|
||||
|
||||
#include <QtWidgets/QPushButton>
|
||||
|
||||
AuthlibInjectorLoginDialog::AuthlibInjectorLoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::AuthlibInjectorLoginDialog)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
ui->progressBar->setVisible(false);
|
||||
ui->loadingLabel->setVisible(false);
|
||||
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
|
||||
setAcceptDrops(true);
|
||||
|
||||
@ -104,10 +103,12 @@ void AuthlibInjectorLoginDialog::accept()
|
||||
return;
|
||||
|
||||
setUserInputsEnabled(false);
|
||||
ui->progressBar->setVisible(true);
|
||||
ui->loadingLabel->setVisible(true);
|
||||
|
||||
// Get the authlib-injector API root
|
||||
auto netJob = NetJob::Ptr(new NetJob("Get authlib-injector API root", APPLICATION->network()));
|
||||
netJob->setAskRetry(false);
|
||||
|
||||
auto username = ui->userTextBox->text();
|
||||
m_createAuthlibInjectorAccountTask = CreateAuthlibInjectorAccount::make(fixedAuthlibInjectorUrl, m_account, username);
|
||||
netJob->addNetAction(m_createAuthlibInjectorAccountTask);
|
||||
@ -159,17 +160,16 @@ void AuthlibInjectorLoginDialog::onTaskFailed(const QString& reason)
|
||||
|
||||
// Re-enable user-interaction
|
||||
setUserInputsEnabled(true);
|
||||
ui->progressBar->setVisible(false);
|
||||
ui->loadingLabel->setVisible(false);
|
||||
}
|
||||
|
||||
void AuthlibInjectorLoginDialog::onUrlTaskSucceeded()
|
||||
{
|
||||
m_account = m_createAuthlibInjectorAccountTask->getAccount();
|
||||
m_loginTask = m_account->loginAuthlibInjector(ui->passTextBox->text());
|
||||
m_loginTask = m_account->login(false, ui->passTextBox->text());
|
||||
connect(m_loginTask.get(), &Task::failed, this, &AuthlibInjectorLoginDialog::onTaskFailed);
|
||||
connect(m_loginTask.get(), &Task::succeeded, this, &AuthlibInjectorLoginDialog::onTaskSucceeded);
|
||||
connect(m_loginTask.get(), &Task::status, this, &AuthlibInjectorLoginDialog::onTaskStatus);
|
||||
connect(m_loginTask.get(), &Task::progress, this, &AuthlibInjectorLoginDialog::onTaskProgress);
|
||||
m_loginTask->start();
|
||||
}
|
||||
|
||||
@ -183,12 +183,6 @@ void AuthlibInjectorLoginDialog::onTaskStatus(const QString& status)
|
||||
ui->label->setText(status);
|
||||
}
|
||||
|
||||
void AuthlibInjectorLoginDialog::onTaskProgress(qint64 current, qint64 total)
|
||||
{
|
||||
ui->progressBar->setMaximum(total);
|
||||
ui->progressBar->setValue(current);
|
||||
}
|
||||
|
||||
// Public interface
|
||||
MinecraftAccountPtr AuthlibInjectorLoginDialog::newAccount(QWidget* parent, QString msg)
|
||||
{
|
||||
|
@ -51,7 +51,6 @@ class AuthlibInjectorLoginDialog : public QDialog {
|
||||
void onUrlTaskSucceeded();
|
||||
void onTaskSucceeded();
|
||||
void onTaskStatus(const QString& status);
|
||||
void onTaskProgress(qint64 current, qint64 total);
|
||||
|
||||
void on_userTextBox_textEdited(const QString& newText);
|
||||
void on_passTextBox_textEdited(const QString& newText);
|
||||
|
@ -60,14 +60,24 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QProgressBar" name="progressBar">
|
||||
<property name="value">
|
||||
<number>24</number>
|
||||
</property>
|
||||
<property name="textVisible">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="loadingLabel">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>16</pointsize>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Please wait...</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
|
@ -1,115 +0,0 @@
|
||||
/* 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 "LoginDialog.h"
|
||||
#include "ui_LoginDialog.h"
|
||||
|
||||
#include "minecraft/auth/AccountTask.h"
|
||||
|
||||
#include <QtWidgets/QPushButton>
|
||||
|
||||
LoginDialog::LoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::LoginDialog)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
ui->progressBar->setVisible(false);
|
||||
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
|
||||
|
||||
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
}
|
||||
|
||||
LoginDialog::~LoginDialog()
|
||||
{
|
||||
delete ui;
|
||||
}
|
||||
|
||||
// Stage 1: User interaction
|
||||
void LoginDialog::accept()
|
||||
{
|
||||
setUserInputsEnabled(false);
|
||||
ui->progressBar->setVisible(true);
|
||||
|
||||
// Setup the login task and start it
|
||||
m_account = MinecraftAccount::createFromUsername(ui->userTextBox->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);
|
||||
connect(m_loginTask.get(), &Task::progress, this, &LoginDialog::onTaskProgress);
|
||||
m_loginTask->start();
|
||||
}
|
||||
|
||||
void LoginDialog::setUserInputsEnabled(bool enable)
|
||||
{
|
||||
ui->userTextBox->setEnabled(enable);
|
||||
ui->passTextBox->setEnabled(enable);
|
||||
ui->buttonBox->setEnabled(enable);
|
||||
}
|
||||
|
||||
// Enable the OK button only when both textboxes contain something.
|
||||
void LoginDialog::on_userTextBox_textEdited(const QString& newText)
|
||||
{
|
||||
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!newText.isEmpty() && !ui->passTextBox->text().isEmpty());
|
||||
}
|
||||
void LoginDialog::on_passTextBox_textEdited(const QString& newText)
|
||||
{
|
||||
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!newText.isEmpty() && !ui->userTextBox->text().isEmpty());
|
||||
}
|
||||
|
||||
void LoginDialog::onTaskFailed(const QString& reason)
|
||||
{
|
||||
// Set message
|
||||
auto lines = reason.split('\n');
|
||||
QString processed;
|
||||
for (auto line : lines) {
|
||||
if (line.size()) {
|
||||
processed += "<font color='red'>" + line + "</font><br />";
|
||||
} else {
|
||||
processed += "<br />";
|
||||
}
|
||||
}
|
||||
ui->label->setText(processed);
|
||||
|
||||
// Re-enable user-interaction
|
||||
setUserInputsEnabled(true);
|
||||
ui->progressBar->setVisible(false);
|
||||
}
|
||||
|
||||
void LoginDialog::onTaskSucceeded()
|
||||
{
|
||||
QDialog::accept();
|
||||
}
|
||||
|
||||
void LoginDialog::onTaskStatus(const QString& status)
|
||||
{
|
||||
ui->label->setText(status);
|
||||
}
|
||||
|
||||
void LoginDialog::onTaskProgress(qint64 current, qint64 total)
|
||||
{
|
||||
ui->progressBar->setMaximum(total);
|
||||
ui->progressBar->setValue(current);
|
||||
}
|
||||
|
||||
// Public interface
|
||||
MinecraftAccountPtr LoginDialog::newAccount(QWidget* parent, QString msg)
|
||||
{
|
||||
LoginDialog dlg(parent);
|
||||
dlg.ui->label->setText(msg);
|
||||
if (dlg.exec() == QDialog::Accepted) {
|
||||
return dlg.m_account;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
@ -133,19 +133,6 @@ void AccountListPage::listChanged()
|
||||
updateButtonStates();
|
||||
}
|
||||
|
||||
void AccountListPage::on_actionAddMojang_triggered()
|
||||
{
|
||||
MinecraftAccountPtr account =
|
||||
LoginDialog::newAccount(this, tr("Please enter your Mojang account email and password to add your account."));
|
||||
|
||||
if (account) {
|
||||
m_accounts->addAccount(account);
|
||||
if (m_accounts->count() == 1) {
|
||||
m_accounts->setDefaultAccount(account);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AccountListPage::on_actionAddAuthlibInjector_triggered()
|
||||
{
|
||||
if (!m_accounts->anyAccountIsValid()) {
|
||||
|
@ -70,7 +70,6 @@ class AccountListPage : public QMainWindow, public BasePage {
|
||||
void retranslate() override;
|
||||
|
||||
public slots:
|
||||
void on_actionAddMojang_triggered();
|
||||
void on_actionAddAuthlibInjector_triggered();
|
||||
void on_actionAddMicrosoft_triggered();
|
||||
void on_actionAddOffline_triggered();
|
||||
|
@ -53,7 +53,6 @@
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<addaction name="actionAddMicrosoft"/>
|
||||
<addaction name="actionAddMojang"/>
|
||||
<addaction name="actionAddAuthlibInjector"/>
|
||||
<addaction name="actionAddOffline"/>
|
||||
<addaction name="actionRefresh"/>
|
||||
|
Loading…
Reference in New Issue
Block a user