diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 02dcb28f6..8ae461488 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -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 diff --git a/launcher/CreateAuthlibInjectorAccount.cpp b/launcher/GetAuthlibInjectorApiLocation.cpp similarity index 67% rename from launcher/CreateAuthlibInjectorAccount.cpp rename to launcher/GetAuthlibInjectorApiLocation.cpp index 3d0c8e545..235e8c9e5 100644 --- a/launcher/CreateAuthlibInjectorAccount.cpp +++ b/launcher/GetAuthlibInjectorApiLocation.cpp @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -#include "CreateAuthlibInjectorAccount.h" +#include "GetAuthlibInjectorApiLocation.h" #include #include @@ -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; } diff --git a/launcher/CreateAuthlibInjectorAccount.h b/launcher/GetAuthlibInjectorApiLocation.h similarity index 74% rename from launcher/CreateAuthlibInjectorAccount.h rename to launcher/GetAuthlibInjectorApiLocation.h index a02f69ccd..f35e22f40 100644 --- a/launcher/CreateAuthlibInjectorAccount.h +++ b/launcher/GetAuthlibInjectorApiLocation.h @@ -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(QUrl url, MinecraftAccountPtr account, QString username); - virtual ~CreateAuthlibInjectorAccount() = default; + using Ptr = shared_qobject_ptr; + 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: diff --git a/launcher/minecraft/auth/AuthFlow.cpp b/launcher/minecraft/auth/AuthFlow.cpp index ab3898971..c3a159197 100644 --- a/launcher/minecraft/auth/AuthFlow.cpp +++ b/launcher/minecraft/auth/AuthFlow.cpp @@ -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); diff --git a/launcher/minecraft/auth/steps/YggdrasilStep.cpp b/launcher/minecraft/auth/steps/YggdrasilStep.cpp index 2bd2b7496..1d7392e76 100644 --- a/launcher/minecraft/auth/steps/YggdrasilStep.cpp +++ b/launcher/minecraft/auth/steps/YggdrasilStep.cpp @@ -7,7 +7,7 @@ YggdrasilStep::YggdrasilStep(AccountData* data, std::optional 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("SSL Handshake failed.
There might be a few causes for it:
" + "
    " + "
  • You use Windows and need to update your root certificates, please install any outstanding updates.
  • " + "
  • Some device on your network is interfering with SSL traffic. In that case, " + "you have bigger worries than Minecraft not starting.
  • " + "
  • Possibly something else. Check the log file for details
  • " + "
")); + 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(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(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.")); + } } diff --git a/launcher/net/NetJob.cpp b/launcher/net/NetJob.cpp index 3cd3958f7..38b73c6d9 100644 --- a/launcher/net/NetJob.cpp +++ b/launcher/net/NetJob.cpp @@ -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; +} diff --git a/launcher/net/NetJob.h b/launcher/net/NetJob.h index 59213ba15..7366fe26a 100644 --- a/launcher/net/NetJob.h +++ b/launcher/net/NetJob.h @@ -63,6 +63,7 @@ class NetJob : public ConcurrentTask { auto getFailedActions() -> QList; auto getFailedFiles() -> QList; 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; }; diff --git a/launcher/ui/dialogs/AuthlibInjectorLoginDialog.cpp b/launcher/ui/dialogs/AuthlibInjectorLoginDialog.cpp index bd2f76fba..b6d3b3f75 100644 --- a/launcher/ui/dialogs/AuthlibInjectorLoginDialog.cpp +++ b/launcher/ui/dialogs/AuthlibInjectorLoginDialog.cpp @@ -20,14 +20,17 @@ #include "ui/dialogs/CustomMessageBox.h" #include "ui_AuthlibInjectorLoginDialog.h" -#include "CreateAuthlibInjectorAccount.h" +#include "Application.h" +#include "GetAuthlibInjectorApiLocation.h" #include 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 += "
"; } } - 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 diff --git a/launcher/ui/dialogs/AuthlibInjectorLoginDialog.h b/launcher/ui/dialogs/AuthlibInjectorLoginDialog.h index 34f4e479b..55082a602 100644 --- a/launcher/ui/dialogs/AuthlibInjectorLoginDialog.h +++ b/launcher/ui/dialogs/AuthlibInjectorLoginDialog.h @@ -22,8 +22,7 @@ #include #include -#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; }; diff --git a/launcher/ui/dialogs/AuthlibInjectorLoginDialog.ui b/launcher/ui/dialogs/AuthlibInjectorLoginDialog.ui index 271056c0e..5965b8d3a 100644 --- a/launcher/ui/dialogs/AuthlibInjectorLoginDialog.ui +++ b/launcher/ui/dialogs/AuthlibInjectorLoginDialog.ui @@ -80,6 +80,22 @@ + + + + Error message label placeholder. + + + Qt::RichText + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + true + + +