It compiles, Yggdrasil login working except error handling

This commit is contained in:
Evan Goode 2024-10-26 00:17:24 -04:00
parent 7dbaa896a6
commit c32aaf244d
27 changed files with 392 additions and 883 deletions

View File

@ -109,6 +109,8 @@
#include "icons/IconList.h"
#include "net/HttpMetaCache.h"
#include "ui/GuiUtil.h"
#include "java/JavaInstallList.h"
#include "updater/ExternalUpdater.h"

View File

@ -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

View File

@ -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()

View File

@ -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;
};

View File

@ -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"

View File

@ -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);

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

@ -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);

View File

@ -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;

View File

@ -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")); });

View File

@ -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();

View File

@ -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."));
}
}

View File

@ -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;
};

View File

@ -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."));
}

View File

@ -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>);
};

View File

@ -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."));
}

View File

@ -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;
};

View File

@ -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."));*/
/*}*/
}

View File

@ -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;
};

View File

@ -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;
}
}

View File

@ -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)
{

View File

@ -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);

View File

@ -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>

View File

@ -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;
}

View File

@ -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()) {

View File

@ -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();

View File

@ -53,7 +53,6 @@
<bool>false</bool>
</attribute>
<addaction name="actionAddMicrosoft"/>
<addaction name="actionAddMojang"/>
<addaction name="actionAddAuthlibInjector"/>
<addaction name="actionAddOffline"/>
<addaction name="actionRefresh"/>