Error handling for Yggdrasil accounts

This commit is contained in:
Evan Goode 2024-10-26 13:40:09 -04:00
parent c32aaf244d
commit d40b6d8eb3
10 changed files with 167 additions and 53 deletions

View File

@ -45,8 +45,8 @@ set(CORE_SOURCES
ResourceDownloadTask.h
ResourceDownloadTask.cpp
CreateAuthlibInjectorAccount.cpp
CreateAuthlibInjectorAccount.h
GetAuthlibInjectorApiLocation.cpp
GetAuthlibInjectorApiLocation.h
# Use tracking separate from memory management
Usable.h

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "CreateAuthlibInjectorAccount.h"
#include "GetAuthlibInjectorApiLocation.h"
#include <QDebug>
#include <QJsonDocument>
@ -28,40 +28,40 @@
#include "Application.h"
CreateAuthlibInjectorAccount::CreateAuthlibInjectorAccount(QUrl url, MinecraftAccountPtr account, QString username)
GetAuthlibInjectorApiLocation::GetAuthlibInjectorApiLocation(QUrl url, MinecraftAccountPtr account, QString username)
: NetRequest(), m_account(account), m_username(username)
{
m_url = url;
m_sink.reset(new Sink(*this));
}
QNetworkReply* CreateAuthlibInjectorAccount::getReply(QNetworkRequest& request)
QNetworkReply* GetAuthlibInjectorApiLocation::getReply(QNetworkRequest& request)
{
setStatus(tr("Getting authlib-injector server details"));
return m_network->get(request);
}
CreateAuthlibInjectorAccount::Ptr CreateAuthlibInjectorAccount::make(QUrl url, MinecraftAccountPtr account, QString username)
GetAuthlibInjectorApiLocation::Ptr GetAuthlibInjectorApiLocation::make(QUrl url, MinecraftAccountPtr account, QString username)
{
return CreateAuthlibInjectorAccount::Ptr(new CreateAuthlibInjectorAccount(url, account, username));
return GetAuthlibInjectorApiLocation::Ptr(new GetAuthlibInjectorApiLocation(url, account, username));
}
auto CreateAuthlibInjectorAccount::Sink::init(QNetworkRequest& request) -> Task::State
auto GetAuthlibInjectorApiLocation::Sink::init(QNetworkRequest& request) -> Task::State
{
return Task::State::Running;
}
auto CreateAuthlibInjectorAccount::Sink::write(QByteArray& data) -> Task::State
auto GetAuthlibInjectorApiLocation::Sink::write(QByteArray& data) -> Task::State
{
return Task::State::Running;
}
auto CreateAuthlibInjectorAccount::Sink::abort() -> Task::State
auto GetAuthlibInjectorApiLocation::Sink::abort() -> Task::State
{
return Task::State::Failed;
}
auto CreateAuthlibInjectorAccount::Sink::finalize(QNetworkReply& reply) -> Task::State
auto GetAuthlibInjectorApiLocation::Sink::finalize(QNetworkReply& reply) -> Task::State
{
QVariant header = reply.rawHeader("X-Authlib-Injector-API-Location");
QUrl url = m_outer.m_url;
@ -76,7 +76,7 @@ auto CreateAuthlibInjectorAccount::Sink::finalize(QNetworkReply& reply) -> Task:
return Task::State::Succeeded;
}
MinecraftAccountPtr CreateAuthlibInjectorAccount::getAccount()
MinecraftAccountPtr GetAuthlibInjectorApiLocation::getAccount()
{
return m_account;
}

View File

@ -21,20 +21,20 @@
#include "minecraft/auth/MinecraftAccount.h"
#include "net/NetRequest.h"
class CreateAuthlibInjectorAccount : public Net::NetRequest {
class GetAuthlibInjectorApiLocation : public Net::NetRequest {
Q_OBJECT
public:
using Ptr = shared_qobject_ptr<CreateAuthlibInjectorAccount>;
CreateAuthlibInjectorAccount(QUrl url, MinecraftAccountPtr account, QString username);
virtual ~CreateAuthlibInjectorAccount() = default;
using Ptr = shared_qobject_ptr<GetAuthlibInjectorApiLocation>;
GetAuthlibInjectorApiLocation(QUrl url, MinecraftAccountPtr account, QString username);
virtual ~GetAuthlibInjectorApiLocation() = default;
static CreateAuthlibInjectorAccount::Ptr make(QUrl url, MinecraftAccountPtr account, QString username);
static GetAuthlibInjectorApiLocation::Ptr make(QUrl url, MinecraftAccountPtr account, QString username);
MinecraftAccountPtr getAccount();
class Sink : public Net::Sink {
public:
Sink(CreateAuthlibInjectorAccount& outer) : m_outer(outer) {}
Sink(GetAuthlibInjectorApiLocation& outer) : m_outer(outer) {}
virtual ~Sink() = default;
public:
@ -45,7 +45,7 @@ class CreateAuthlibInjectorAccount : public Net::NetRequest {
auto hasLocalData() -> bool override { return false; }
private:
CreateAuthlibInjectorAccount& m_outer;
GetAuthlibInjectorApiLocation& m_outer;
};
protected slots:

View File

@ -131,7 +131,7 @@ bool AuthFlow::changeState(AccountTaskState newState, QString reason)
return false;
}
case AccountTaskState::STATE_FAILED_HARD: {
setStatus(tr("Failed to authenticate. The session has expired."));
setStatus(tr("Failed to authenticate."));
m_data->errorString = reason;
m_data->accountState = AccountState::Expired;
emitFailed(reason);

View File

@ -7,7 +7,7 @@ YggdrasilStep::YggdrasilStep(AccountData* data, std::optional<QString> password)
QString YggdrasilStep::describe()
{
return tr("Logging in with Mojang account.");
return tr("Logging in with Yggdrasil.");
}
void YggdrasilStep::perform()
@ -64,6 +64,7 @@ void YggdrasilStep::login(QString password)
m_task.reset(new NetJob("YggdrasilStep", APPLICATION->network()));
m_task->setAskRetry(false);
m_task->setAutoRetryLimit(0);
m_task->addNetAction(m_request);
connect(m_task.get(), &Task::finished, this, &YggdrasilStep::onRequestDone);
@ -102,6 +103,7 @@ void YggdrasilStep::refresh()
m_task.reset(new NetJob("YggdrasilStep", APPLICATION->network()));
m_task->setAskRetry(false);
m_task->setAutoRetryLimit(0);
m_task->addNetAction(m_request);
connect(m_task.get(), &Task::finished, this, &YggdrasilStep::onRequestDone);
@ -111,10 +113,90 @@ void YggdrasilStep::refresh()
void YggdrasilStep::onRequestDone()
{
// TODO handle errors
qDebug() << "Yggdrasil request done";
switch (m_request->error()) {
case QNetworkReply::NoError:
break;
case QNetworkReply::AuthenticationRequiredError:
// These cases will be handled as usual
break;
case QNetworkReply::TimeoutError:
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Authentication operation timed out."));
return;
case QNetworkReply::OperationCanceledError:
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Authentication operation cancelled."));
return;
case QNetworkReply::SslHandshakeFailedError:
emit finished(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: {
emit finished(AccountTaskState::STATE_FAILED_GONE,
tr("The Mojang account no longer exists. It may have been migrated to a Microsoft account."));
return;
}
default:
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Authentication operation failed due to a network error: %1 (%2)")
.arg(m_request->errorString())
.arg(m_request->error()));
return;
}
// Try to parse the response regardless of the response code.
// Sometimes the auth server will give more information and an error code.
// Check the response code.
QJsonParseError jsonError;
QJsonDocument doc = QJsonDocument::fromJson(*m_response, &jsonError);
// Check the response code.
int responseCode = m_request->replyStatusCode();
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 || m_response->size() == 0) {
processResponse(m_response->size() > 0 ? doc.object() : QJsonObject());
return;
} else {
emit finished(AccountTaskState::STATE_FAILED_SOFT,
tr("Failed to parse authentication server response JSON response: %1 at offset %2.")
.arg(jsonError.errorString())
.arg(jsonError.offset));
qCritical() << *m_response;
}
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.";
emit finished(
AccountTaskState::STATE_FAILED_SOFT,
tr("An unknown error occurred when trying to communicate with the authentication server: %1").arg(m_request->errorString()));
}
YggdrasilStep::processResponse(doc.object());
}
@ -122,9 +204,7 @@ 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("");
@ -192,15 +272,15 @@ void YggdrasilStep::processResponse(QJsonObject responseData)
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."));*/
/*}*/
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("") });*/
emit finished(AccountTaskState::STATE_FAILED_HARD, errorMessageValue.toString(""));
} else {
// Error is not in standard format. Don't set m_error and return unknown error.
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("An unknown Yggdrasil error occurred."));
}
}

View File

@ -66,8 +66,8 @@ auto NetJob::addNetAction(Net::NetRequest::Ptr action) -> bool
void NetJob::executeNextSubTask()
{
// We're finished, check for failures and retry if we can (up to 3 times)
if (isRunning() && m_queue.isEmpty() && m_doing.isEmpty() && !m_failed.isEmpty() && m_try < 3) {
// We're finished, check for failures and retry if we can (up to m_auto_retry_limit times)
if (isRunning() && m_queue.isEmpty() && m_doing.isEmpty() && !m_failed.isEmpty() && m_try < m_auto_retry_limit) {
m_try += 1;
while (!m_failed.isEmpty()) {
auto task = m_failed.take(*m_failed.keyBegin());
@ -188,3 +188,8 @@ void NetJob::setAskRetry(bool askRetry)
{
m_ask_retry = askRetry;
}
void NetJob::setAutoRetryLimit(int autoRetryLimit)
{
m_auto_retry_limit = autoRetryLimit;
}

View File

@ -63,6 +63,7 @@ class NetJob : public ConcurrentTask {
auto getFailedActions() -> QList<Net::NetRequest*>;
auto getFailedFiles() -> QList<QString>;
void setAskRetry(bool askRetry);
void setAutoRetryLimit(int autoRetryLimit);
public slots:
// Qt can't handle auto at the start for some reason?
@ -81,5 +82,6 @@ class NetJob : public ConcurrentTask {
int m_try = 1;
bool m_ask_retry = true;
int m_auto_retry_limit = 3;
int m_manual_try = 0;
};

View File

@ -20,14 +20,17 @@
#include "ui/dialogs/CustomMessageBox.h"
#include "ui_AuthlibInjectorLoginDialog.h"
#include "CreateAuthlibInjectorAccount.h"
#include "Application.h"
#include "GetAuthlibInjectorApiLocation.h"
#include <QtWidgets/QPushButton>
AuthlibInjectorLoginDialog::AuthlibInjectorLoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::AuthlibInjectorLoginDialog)
{
ui->setupUi(this);
ui->userTextBox->setFocus();
ui->loadingLabel->setVisible(false);
ui->errorMessage->setVisible(false);
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
setAcceptDrops(true);
@ -88,6 +91,7 @@ void AuthlibInjectorLoginDialog::dropEvent(QDropEvent* event)
// Stage 1: User interaction
void AuthlibInjectorLoginDialog::accept()
{
ui->errorMessage->setVisible(false);
auto fixedAuthlibInjectorUrl = AuthlibInjectorLoginDialog::fixUrl(ui->authlibInjectorTextBox->text());
auto response = CustomMessageBox::selectable(this, QObject::tr("Confirm account creation"),
@ -108,15 +112,16 @@ void AuthlibInjectorLoginDialog::accept()
// Get the authlib-injector API root
auto netJob = NetJob::Ptr(new NetJob("Get authlib-injector API root", APPLICATION->network()));
netJob->setAskRetry(false);
netJob->setAutoRetryLimit(0);
auto username = ui->userTextBox->text();
m_createAuthlibInjectorAccountTask = CreateAuthlibInjectorAccount::make(fixedAuthlibInjectorUrl, m_account, username);
netJob->addNetAction(m_createAuthlibInjectorAccountTask);
m_apiLocationRequest = GetAuthlibInjectorApiLocation::make(fixedAuthlibInjectorUrl, m_account, username);
netJob->addNetAction(m_apiLocationRequest);
m_createAuthlibInjectorAccountNetJob.reset(netJob);
connect(netJob.get(), &NetJob::succeeded, this, &AuthlibInjectorLoginDialog::onUrlTaskSucceeded);
connect(netJob.get(), &NetJob::failed, this, &AuthlibInjectorLoginDialog::onTaskFailed);
m_createAuthlibInjectorAccountNetJob->start();
m_apiLocationTask.reset(netJob);
connect(netJob.get(), &NetJob::succeeded, this, &AuthlibInjectorLoginDialog::onApiLocationTaskSucceeded);
connect(netJob.get(), &NetJob::failed, this, &AuthlibInjectorLoginDialog::onApiLocationTaskFailed);
m_apiLocationTask->start();
}
void AuthlibInjectorLoginDialog::setUserInputsEnabled(bool enable)
@ -144,6 +149,11 @@ void AuthlibInjectorLoginDialog::on_authlibInjectorTextBox_textEdited(const QStr
->setEnabled(!newText.isEmpty() && !ui->passTextBox->text().isEmpty() && !ui->authlibInjectorTextBox->text().isEmpty());
}
void AuthlibInjectorLoginDialog::onApiLocationTaskFailed(const QString& reason)
{
onTaskFailed(m_apiLocationRequest->errorString());
}
void AuthlibInjectorLoginDialog::onTaskFailed(const QString& reason)
{
// Set message
@ -156,16 +166,17 @@ void AuthlibInjectorLoginDialog::onTaskFailed(const QString& reason)
processed += "<br />";
}
}
ui->label->setText(processed);
ui->errorMessage->setText(processed);
ui->errorMessage->setVisible(true);
// Re-enable user-interaction
setUserInputsEnabled(true);
ui->loadingLabel->setVisible(false);
}
void AuthlibInjectorLoginDialog::onUrlTaskSucceeded()
void AuthlibInjectorLoginDialog::onApiLocationTaskSucceeded()
{
m_account = m_createAuthlibInjectorAccountTask->getAccount();
m_account = m_apiLocationRequest->getAccount();
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);
@ -180,7 +191,7 @@ void AuthlibInjectorLoginDialog::onTaskSucceeded()
void AuthlibInjectorLoginDialog::onTaskStatus(const QString& status)
{
ui->label->setText(status);
ui->errorMessage->setText(status);
}
// Public interface

View File

@ -22,8 +22,7 @@
#include <QtWidgets/QDialog>
#include <net/NetJob.h>
#include "Application.h"
#include "CreateAuthlibInjectorAccount.h"
#include "GetAuthlibInjectorApiLocation.h"
#include "minecraft/auth/MinecraftAccount.h"
#include "tasks/Task.h"
@ -47,8 +46,9 @@ class AuthlibInjectorLoginDialog : public QDialog {
protected slots:
void accept();
void onApiLocationTaskSucceeded();
void onApiLocationTaskFailed(const QString&);
void onTaskFailed(const QString& reason);
void onUrlTaskSucceeded();
void onTaskSucceeded();
void onTaskStatus(const QString& status);
@ -65,6 +65,6 @@ class AuthlibInjectorLoginDialog : public QDialog {
Ui::AuthlibInjectorLoginDialog* ui;
MinecraftAccountPtr m_account;
Task::Ptr m_loginTask;
Task::Ptr m_createAuthlibInjectorAccountNetJob;
CreateAuthlibInjectorAccount::Ptr m_createAuthlibInjectorAccountTask;
Task::Ptr m_apiLocationTask;
GetAuthlibInjectorApiLocation::Ptr m_apiLocationRequest;
};

View File

@ -80,6 +80,22 @@
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="errorMessage">
<property name="text">
<string notr="true">Error message label placeholder.</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">