From bfc9e1e5d598f354dd39e5c2eb51d5e51585359b Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 28 Nov 2013 20:45:52 -0600 Subject: [PATCH] Verify access tokens before launching Minecraft Kind of an important thing to do... Heh... --- CMakeLists.txt | 5 ++ gui/MainWindow.cpp | 74 ++++++++++++++++++-- gui/MainWindow.h | 18 ++++- gui/dialogs/PasswordDialog.cpp | 38 +++++++++++ gui/dialogs/PasswordDialog.h | 40 +++++++++++ gui/dialogs/PasswordDialog.ui | 78 +++++++++++++++++++++ logic/auth/ValidateTask.cpp | 66 ++++++++++++++++++ logic/auth/ValidateTask.h | 44 ++++++++++++ logic/auth/YggdrasilTask.cpp | 119 +++++++++++++++------------------ logic/auth/YggdrasilTask.h | 2 + logic/tasks/Task.cpp | 15 +++++ logic/tasks/Task.h | 14 ++++ 12 files changed, 440 insertions(+), 73 deletions(-) create mode 100644 gui/dialogs/PasswordDialog.cpp create mode 100644 gui/dialogs/PasswordDialog.h create mode 100644 gui/dialogs/PasswordDialog.ui create mode 100644 logic/auth/ValidateTask.cpp create mode 100644 logic/auth/ValidateTask.h diff --git a/CMakeLists.txt b/CMakeLists.txt index e5f9dcf0e..720f3f1f6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -217,6 +217,8 @@ gui/dialogs/EditNotesDialog.h gui/dialogs/EditNotesDialog.cpp gui/dialogs/CustomMessageBox.h gui/dialogs/CustomMessageBox.cpp +gui/dialogs/PasswordDialog.h +gui/dialogs/PasswordDialog.cpp gui/dialogs/AccountListDialog.h gui/dialogs/AccountListDialog.cpp gui/dialogs/AccountSelectDialog.h @@ -280,6 +282,8 @@ logic/auth/YggdrasilTask.h logic/auth/YggdrasilTask.cpp logic/auth/AuthenticateTask.h logic/auth/AuthenticateTask.cpp +logic/auth/ValidateTask.h +logic/auth/ValidateTask.cpp # legacy instances @@ -366,6 +370,7 @@ gui/dialogs/SettingsDialog.ui gui/dialogs/CopyInstanceDialog.ui gui/dialogs/NewInstanceDialog.ui gui/dialogs/LoginDialog.ui +gui/dialogs/PasswordDialog.ui gui/dialogs/AboutDialog.ui gui/dialogs/VersionSelectDialog.ui gui/dialogs/LwjglSelectDialog.ui diff --git a/gui/MainWindow.cpp b/gui/MainWindow.cpp index fb25ae365..72e754ee5 100644 --- a/gui/MainWindow.cpp +++ b/gui/MainWindow.cpp @@ -60,6 +60,7 @@ #include "gui/dialogs/CopyInstanceDialog.h" #include "gui/dialogs/AccountListDialog.h" #include "gui/dialogs/AccountSelectDialog.h" +#include "gui/dialogs/PasswordDialog.h" #include "gui/ConsoleWindow.h" @@ -69,6 +70,9 @@ #include "logic/lists/IconList.h" #include "logic/lists/JavaVersionList.h" +#include "logic/auth/AuthenticateTask.h" +#include "logic/auth/ValidateTask.h" + #include "logic/net/LoginTask.h" #include "logic/BaseInstance.h" @@ -709,7 +713,7 @@ void MainWindow::instanceActivated(QModelIndex index) NagUtils::checkJVMArgs(inst->settings().get("JvmArgs").toString(), this); - doLogin(); + doLaunch(); } void MainWindow::on_actionLaunchInstance_triggered() @@ -717,11 +721,11 @@ void MainWindow::on_actionLaunchInstance_triggered() if (m_selectedInstance) { NagUtils::checkJVMArgs(m_selectedInstance->settings().get("JvmArgs").toString(), this); - doLogin(); + doLaunch(); } } -void MainWindow::doLogin(const QString &errorMsg) +void MainWindow::doLaunch() { if (!m_selectedInstance) return; @@ -761,11 +765,69 @@ void MainWindow::doLogin(const QString &errorMsg) if (account.get() != nullptr) { - // We'll need to validate the access token to make sure the account is still logged in. - // TODO: Do that ^ - + doLaunchInst(m_selectedInstance, account); + } +} + +void MainWindow::doLaunchInst(BaseInstance* instance, MojangAccountPtr account) +{ + // We'll need to validate the access token to make sure the account is still logged in. + ProgressDialog progDialog(this); + ValidateTask validateTask(account, &progDialog); + progDialog.exec(&validateTask); + + if (validateTask.successful()) + { prepareLaunch(m_selectedInstance, account); } + else + { + YggdrasilTask::Error* error = validateTask.getError(); + + if (error != nullptr) + { + if (error->getErrorMessage().contains("invalid token", Qt::CaseInsensitive)) + { + // TODO: Allow the user to enter their password and "refresh" their access token. + if (doRefreshToken(account, tr("Your account's access token is invalid. Please enter your password to log in again."))) + doLaunchInst(instance, account); + } + else + { + CustomMessageBox::selectable(this, tr("Access Token Validation Error"), + tr("There was an error when trying to validate your access token.\n" + "Details: %s").arg(error->getDisplayMessage()), + QMessageBox::Warning, QMessageBox::Ok)->exec(); + } + } + else + { + CustomMessageBox::selectable(this, tr("Access Token Validation Error"), + tr("There was an unknown error when trying to validate your access token." + "The authentication server might be down, or you might not be connected to the Internet."), + QMessageBox::Warning, QMessageBox::Ok)->exec(); + } + } +} + +bool MainWindow::doRefreshToken(MojangAccountPtr account, const QString& errorMsg) +{ + PasswordDialog passDialog(errorMsg, this); + if (passDialog.exec() == QDialog::Accepted) + { + // To refresh the token, we just create an authenticate task with the given account and the user's password. + ProgressDialog progDialog(this); + AuthenticateTask authTask(account, passDialog.password(), &progDialog); + progDialog.exec(&authTask); + if (authTask.successful()) + return true; + else + { + // If the authentication task failed, recurse with the task's error message. + return doRefreshToken(account, authTask.failReason()); + } + } + else return false; } void MainWindow::prepareLaunch(BaseInstance* instance, MojangAccountPtr account) diff --git a/gui/MainWindow.h b/gui/MainWindow.h index 4191e5902..650fdee25 100644 --- a/gui/MainWindow.h +++ b/gui/MainWindow.h @@ -106,7 +106,23 @@ slots: void on_actionEditInstNotes_triggered(); - void doLogin(const QString &errorMsg = ""); + /*! + * Launches the currently selected instance with the default account. + * If no default account is selected, prompts the user to pick an account. + */ + void doLaunch(); + + /*! + * Launches the given instance with the given account. + */ + void doLaunchInst(BaseInstance* instance, MojangAccountPtr account); + + /*! + * Opens an input dialog, allowing the user to input their password and refresh its access token. + * This function will execute the proper Yggdrasil task to refresh the access token. + * Returns true if successful. False if the user cancelled. + */ + bool doRefreshToken(MojangAccountPtr account, const QString& errorMsg=""); /*! * Launches the given instance with the given account. diff --git a/gui/dialogs/PasswordDialog.cpp b/gui/dialogs/PasswordDialog.cpp new file mode 100644 index 000000000..c67fc6a29 --- /dev/null +++ b/gui/dialogs/PasswordDialog.cpp @@ -0,0 +1,38 @@ +/* Copyright 2013 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 "PasswordDialog.h" +#include "ui_PasswordDialog.h" + +PasswordDialog::PasswordDialog(const QString& errorMsg, QWidget *parent) : + QDialog(parent), + ui(new Ui::PasswordDialog) +{ + ui->setupUi(this); + + ui->errorLabel->setText(errorMsg); + ui->errorLabel->setVisible(!errorMsg.isEmpty()); +} + +PasswordDialog::~PasswordDialog() +{ + delete ui; +} + +QString PasswordDialog::password() const +{ + return ui->passTextBox->text(); +} + diff --git a/gui/dialogs/PasswordDialog.h b/gui/dialogs/PasswordDialog.h new file mode 100644 index 000000000..0919e6e42 --- /dev/null +++ b/gui/dialogs/PasswordDialog.h @@ -0,0 +1,40 @@ +/* Copyright 2013 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 + +namespace Ui { +class PasswordDialog; +} + +class PasswordDialog : public QDialog +{ + Q_OBJECT + +public: + explicit PasswordDialog(const QString& errorMsg="", QWidget *parent = 0); + ~PasswordDialog(); + + /*! + * Gets the text entered in the dialog's password field. + */ + QString password() const; + +private: + Ui::PasswordDialog *ui; +}; + diff --git a/gui/dialogs/PasswordDialog.ui b/gui/dialogs/PasswordDialog.ui new file mode 100644 index 000000000..6c70b0335 --- /dev/null +++ b/gui/dialogs/PasswordDialog.ui @@ -0,0 +1,78 @@ + + + PasswordDialog + + + + 0 + 0 + 400 + 94 + + + + Dialog + + + + + + Error message here... + + + + + + + QLineEdit::Password + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + PasswordDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + PasswordDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/logic/auth/ValidateTask.cpp b/logic/auth/ValidateTask.cpp new file mode 100644 index 000000000..994d24e42 --- /dev/null +++ b/logic/auth/ValidateTask.cpp @@ -0,0 +1,66 @@ + +/* Copyright 2013 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 + +#include + +#include +#include +#include +#include +#include + +#include "logger/QsLog.h" + +ValidateTask::ValidateTask(MojangAccountPtr account, QObject* parent) : + YggdrasilTask(account, parent) +{ +} + +QJsonObject ValidateTask::getRequestContent() const +{ + QJsonObject req; + req.insert("accessToken", getMojangAccount()->accessToken()); + return req; +} + +bool ValidateTask::processResponse(QJsonObject responseData) +{ + // Assume that if processError wasn't called, then the request was successful. + emitSucceeded(); + return true; +} + +QString ValidateTask::getEndpoint() const +{ + return "validate"; +} + +QString ValidateTask::getStateMessage(const YggdrasilTask::State state) const +{ + switch (state) + { + case STATE_SENDING_REQUEST: + return tr("Validating Access Token: Sending request."); + case STATE_PROCESSING_RESPONSE: + return tr("Validating Access Token: Processing response."); + default: + return YggdrasilTask::getStateMessage(state); + } +} + + diff --git a/logic/auth/ValidateTask.h b/logic/auth/ValidateTask.h new file mode 100644 index 000000000..5bbc69c7d --- /dev/null +++ b/logic/auth/ValidateTask.h @@ -0,0 +1,44 @@ +/* Copyright 2013 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 + +#include +#include +#include + +/** + * The validate task takes a MojangAccount and checks to make sure its access token is valid. + */ +class ValidateTask : public YggdrasilTask +{ +Q_OBJECT +public: + ValidateTask(MojangAccountPtr account, QObject* parent=0); + +protected: + virtual QJsonObject getRequestContent() const; + + virtual QString getEndpoint() const; + + virtual bool processResponse(QJsonObject responseData); + + QString getStateMessage(const YggdrasilTask::State state) const; + +private: +}; + diff --git a/logic/auth/YggdrasilTask.cpp b/logic/auth/YggdrasilTask.cpp index 39dfb7491..31c8fbab8 100644 --- a/logic/auth/YggdrasilTask.cpp +++ b/logic/auth/YggdrasilTask.cpp @@ -64,78 +64,65 @@ void YggdrasilTask::processReply(QNetworkReply* reply) // Wrong reply for some reason... return; - // Check for errors. - switch (reply->error()) + if (reply->error() == QNetworkReply::OperationCanceledError) { - case QNetworkReply::NoError: + emitFailed("Yggdrasil task cancelled."); + return; + } + else + { + // 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 = reply->readAll(); + QJsonDocument doc = QJsonDocument::fromJson(replyData, &jsonError); + + // Check the response code. + int responseCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + if (responseCode == 200) { - // 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 = reply->readAll(); - QJsonDocument doc = QJsonDocument::fromJson(replyData, &jsonError); - - // Check the response code. - int responseCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - switch (responseCode) + // 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) { - case 200: + if (!processResponse(replyData.size() > 0 ? doc.object() : QJsonObject())) { - // If the response code was 200, then there shouldn't be an error. Make sure anyways. - switch (jsonError.error) - { - case QJsonParseError::NoError: - if (!processResponse(doc.object())) - { - YggdrasilTask::Error* err = getError(); - if (err) - emitFailed(err->getErrorMessage()); - else - emitFailed(tr("An unknown error occurred when processing the response from the authentication server.")); - } - else - { - emitSucceeded(); - } - break; - - default: - emitFailed(tr("Failed to parse Yggdrasil JSON response: \"%1\".").arg(jsonError.errorString())); - break; - } - break; + YggdrasilTask::Error* err = getError(); + if (err) + emitFailed(err->getErrorMessage()); + else + emitFailed(tr("An unknown error occurred when processing the response from the authentication server.")); + } + else + { + emitSucceeded(); } - - default: - // If the response code was something else, 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. - switch (jsonError.error) - { - case 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. - processError(doc.object()); - break; - - default: - // The server didn't say anything regarding the error. Give the user an unknown error. - emitFailed(tr("Login failed: Unknown HTTP code %1 encountered.").arg(responseCode)); - break; - } - break; } - - break; + else + { + emitFailed(tr("Failed to parse Yggdrasil JSON response: %1 at offset %2.").arg(jsonError.errorString()).arg(jsonError.offset)); + } + } + else + { + // 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. + QLOG_DEBUG() << "The request failed, but the server gave us an error message. Processing error."; + emitFailed(processError(doc.object())); + } + else + { + // The server didn't say anything regarding the error. Give the user an unknown error. + QLOG_DEBUG() << "The request failed and the server gave no error message. Unknown error."; + emitFailed(tr("An unknown error occurred when trying to communicate with the authentication server: %1").arg(reply->errorString())); + } } - - case QNetworkReply::OperationCanceledError: - emitFailed(tr("Login canceled.")); - break; - - default: - emitFailed(tr("An unknown error occurred when trying to communicate with the authentication server.")); - break; } } @@ -145,7 +132,7 @@ QString YggdrasilTask::processError(QJsonObject responseData) QJsonValue msgVal = responseData.value("errorMessage"); QJsonValue causeVal = responseData.value("cause"); - if (errorVal.isString() && msgVal.isString() && causeVal.isString()) + if (errorVal.isString() && msgVal.isString()) { m_error = new Error(errorVal.toString(""), msgVal.toString(""), causeVal.toString("")); return m_error->getDisplayMessage(); diff --git a/logic/auth/YggdrasilTask.h b/logic/auth/YggdrasilTask.h index 6aebae165..5a433aeb6 100644 --- a/logic/auth/YggdrasilTask.h +++ b/logic/auth/YggdrasilTask.h @@ -99,6 +99,8 @@ protected: * If an error occurred, this should emit a failed signal and return false. * 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. */ virtual bool processResponse(QJsonObject responseData) = 0; diff --git a/logic/tasks/Task.cpp b/logic/tasks/Task.cpp index 47214723e..cb7a54431 100644 --- a/logic/tasks/Task.cpp +++ b/logic/tasks/Task.cpp @@ -53,6 +53,8 @@ void Task::start() void Task::emitFailed(QString reason) { m_running = false; + m_succeeded = false; + m_failReason = reason; QLOG_ERROR() << "Task failed: " << reason; emit failed(reason); } @@ -60,6 +62,8 @@ void Task::emitFailed(QString reason) void Task::emitSucceeded() { m_running = false; + m_succeeded = true; + QLOG_INFO() << "Task succeeded"; emit succeeded(); } @@ -67,3 +71,14 @@ bool Task::isRunning() const { return m_running; } + +bool Task::successful() const +{ + return m_succeeded; +} + +QString Task::failReason() const +{ + return m_failReason; +} + diff --git a/logic/tasks/Task.h b/logic/tasks/Task.h index 980b2af86..d08ef5605 100644 --- a/logic/tasks/Task.h +++ b/logic/tasks/Task.h @@ -29,6 +29,18 @@ public: virtual void getProgress(qint64 ¤t, qint64 &total); virtual bool isRunning() const; + /*! + * True if this task was successful. + * If the task failed or is still running, returns false. + */ + virtual bool successful() const; + + /*! + * Returns the string that was passed to emitFailed as the error message when the task failed. + * If the task hasn't failed, returns an empty string. + */ + virtual QString failReason() const; + public slots: virtual void start(); @@ -48,4 +60,6 @@ protected: QString m_status; int m_progress = 0; bool m_running = false; + bool m_succeeded = false; + QString m_failReason = ""; };