diff options
Diffstat (limited to 'logic')
26 files changed, 462 insertions, 396 deletions
diff --git a/logic/BaseInstance.h b/logic/BaseInstance.h index 2d7537d6..603f6105 100644 --- a/logic/BaseInstance.h +++ b/logic/BaseInstance.h @@ -150,7 +150,7 @@ public: virtual SettingsObject &settings() const; /// returns a valid update task if update is needed, NULL otherwise - virtual Task *doUpdate(bool prepare_for_launch) = 0; + virtual std::shared_ptr<Task> doUpdate(bool prepare_for_launch) = 0; /// returns a valid minecraft process, ready for launch with the given account. virtual MinecraftProcess *prepareForLaunch(MojangAccountPtr account) = 0; diff --git a/logic/LegacyInstance.cpp b/logic/LegacyInstance.cpp index 72b6c51a..55523048 100644 --- a/logic/LegacyInstance.cpp +++ b/logic/LegacyInstance.cpp @@ -44,12 +44,12 @@ LegacyInstance::LegacyInstance(const QString &rootDir, SettingsObject *settings, settings->registerSetting(new Setting("IntendedJarVersion", "")); } -Task *LegacyInstance::doUpdate(bool prepare_for_launch) +std::shared_ptr<Task> LegacyInstance::doUpdate(bool prepare_for_launch) { // make sure the jar mods list is initialized by asking for it. auto list = jarModList(); // create an update task - return new LegacyUpdate(this, prepare_for_launch , this); + return std::shared_ptr<Task> (new LegacyUpdate(this, prepare_for_launch , this)); } MinecraftProcess *LegacyInstance::prepareForLaunch(MojangAccountPtr account) @@ -105,7 +105,7 @@ MinecraftProcess *LegacyInstance::prepareForLaunch(MojangAccountPtr account) #endif args << "-jar" << LAUNCHER_FILE; - args << account->currentProfile()->name(); + args << account->currentProfile()->name; args << account->sessionId(); args << windowTitle; args << windowSize; diff --git a/logic/LegacyInstance.h b/logic/LegacyInstance.h index a17ef281..948d4be4 100644 --- a/logic/LegacyInstance.h +++ b/logic/LegacyInstance.h @@ -76,7 +76,7 @@ public: virtual bool shouldUpdate() const override; virtual void setShouldUpdate(bool val) override; - virtual Task *doUpdate(bool prepare_for_launch) override; + virtual std::shared_ptr<Task> doUpdate(bool prepare_for_launch) override; virtual MinecraftProcess *prepareForLaunch(MojangAccountPtr account) override; virtual void cleanupAfterRun() override; diff --git a/logic/MinecraftProcess.cpp b/logic/MinecraftProcess.cpp index 5d99bfae..209929b7 100644 --- a/logic/MinecraftProcess.cpp +++ b/logic/MinecraftProcess.cpp @@ -75,20 +75,22 @@ QString MinecraftProcess::censorPrivateInfo(QString in) { if(!m_account) return in; - else + + QString sessionId = m_account->sessionId(); + QString accessToken = m_account->accessToken(); + QString clientToken = m_account->clientToken(); + in.replace(sessionId, "<SESSION ID>"); + in.replace(accessToken, "<ACCESS TOKEN>"); + in.replace(clientToken, "<CLIENT TOKEN>"); + auto profile = m_account->currentProfile(); + if(profile) { - QString sessionId = m_account->sessionId(); - QString accessToken = m_account->accessToken(); - QString clientToken = m_account->clientToken(); - QString profileId = m_account->currentProfile()->id(); - QString profileName = m_account->currentProfile()->name(); - in.replace(sessionId, "<SESSION ID>"); - in.replace(accessToken, "<ACCESS TOKEN>"); - in.replace(clientToken, "<CLIENT TOKEN>"); + QString profileId = profile->id; + QString profileName = profile->name; in.replace(profileId, "<PROFILE ID>"); in.replace(profileName, "<PROFILE NAME>"); - return in; } + return in; } // console window diff --git a/logic/OneSixInstance.cpp b/logic/OneSixInstance.cpp index 08b63bf9..7a1d7dad 100644 --- a/logic/OneSixInstance.cpp +++ b/logic/OneSixInstance.cpp @@ -37,9 +37,9 @@ OneSixInstance::OneSixInstance(const QString &rootDir, SettingsObject *setting_o reloadFullVersion(); } -Task *OneSixInstance::doUpdate(bool prepare_for_launch) +std::shared_ptr<Task> OneSixInstance::doUpdate(bool prepare_for_launch) { - return new OneSixUpdate(this, prepare_for_launch); + return std::shared_ptr<Task> (new OneSixUpdate(this, prepare_for_launch)); } QString replaceTokensIn(QString text, QMap<QString, QString> with) @@ -77,8 +77,8 @@ QStringList OneSixInstance::processMinecraftArgs(MojangAccountPtr account) token_mapping["auth_username"] = account->username(); token_mapping["auth_session"] = account->sessionId(); token_mapping["auth_access_token"] = account->accessToken(); - token_mapping["auth_player_name"] = account->currentProfile()->name(); - token_mapping["auth_uuid"] = account->currentProfile()->id(); + token_mapping["auth_player_name"] = account->currentProfile()->name; + token_mapping["auth_uuid"] = account->currentProfile()->id; // this is for offline?: /* diff --git a/logic/OneSixInstance.h b/logic/OneSixInstance.h index 042c104b..7ea2d08b 100644 --- a/logic/OneSixInstance.h +++ b/logic/OneSixInstance.h @@ -39,7 +39,7 @@ public: QString loaderModsDir() const; virtual QString instanceConfigFolder() const override; - virtual Task *doUpdate(bool prepare_for_launch) override; + virtual std::shared_ptr<Task> doUpdate(bool prepare_for_launch) override; virtual MinecraftProcess *prepareForLaunch(MojangAccountPtr account) override; virtual void cleanupAfterRun() override; diff --git a/logic/OneSixUpdate.h b/logic/OneSixUpdate.h index b86c205f..5fd2c59f 100644 --- a/logic/OneSixUpdate.h +++ b/logic/OneSixUpdate.h @@ -48,6 +48,7 @@ slots: // extract the appropriate libraries void prepareForLaunch(); + private: NetJobPtr specificVersionDownloadJob; NetJobPtr jarlibDownloadJob; diff --git a/logic/auth/MojangAccount.cpp b/logic/auth/MojangAccount.cpp index 4a61cf19..b1acfb25 100644 --- a/logic/auth/MojangAccount.cpp +++ b/logic/auth/MojangAccount.cpp @@ -16,113 +16,16 @@ */ #include "MojangAccount.h" +#include "flows/RefreshTask.h" +#include "flows/AuthenticateTask.h" #include <QUuid> #include <QJsonObject> #include <QJsonArray> +#include <QRegExp> #include <logger/QsLog.h> -MojangAccount::MojangAccount(const QString &username, QObject *parent) : QObject(parent) -{ - // Generate a client token. - m_clientToken = QUuid::createUuid().toString(); - - m_username = username; - - m_currentProfile = -1; -} - -MojangAccount::MojangAccount(const QString &username, const QString &clientToken, - const QString &accessToken, QObject *parent) - : QObject(parent) -{ - m_username = username; - m_clientToken = clientToken; - m_accessToken = accessToken; - - m_currentProfile = -1; -} - -MojangAccount::MojangAccount(const MojangAccount &other, QObject *parent) -{ - m_username = other.username(); - m_clientToken = other.clientToken(); - m_accessToken = other.accessToken(); - - m_profiles = other.m_profiles; - m_currentProfile = other.m_currentProfile; -} - -QString MojangAccount::username() const -{ - return m_username; -} - -QString MojangAccount::clientToken() const -{ - return m_clientToken; -} - -void MojangAccount::setClientToken(const QString &clientToken) -{ - m_clientToken = clientToken; -} - -QString MojangAccount::accessToken() const -{ - return m_accessToken; -} - -void MojangAccount::setAccessToken(const QString &accessToken) -{ - m_accessToken = accessToken; -} - -QString MojangAccount::sessionId() const -{ - return "token:" + m_accessToken + ":" + currentProfile()->id(); -} - -const QList<AccountProfile> MojangAccount::profiles() const -{ - return m_profiles; -} - -const AccountProfile *MojangAccount::currentProfile() const -{ - if (m_currentProfile < 0) - { - if (m_profiles.length() > 0) - return &m_profiles.at(0); - else - return nullptr; - } - else - return &m_profiles.at(m_currentProfile); -} - -bool MojangAccount::setProfile(const QString &profileId) -{ - const QList<AccountProfile> &profiles = this->profiles(); - for (int i = 0; i < profiles.length(); i++) - { - if (profiles.at(i).id() == profileId) - { - m_currentProfile = i; - return true; - } - } - return false; -} - -void MojangAccount::loadProfiles(const ProfileList &profiles) -{ - m_profiles.clear(); - for (auto profile : profiles) - m_profiles.append(profile); -} - MojangAccountPtr MojangAccount::loadFromJson(const QJsonObject &object) { // The JSON object must at least have a username for it to be valid. @@ -143,7 +46,7 @@ MojangAccountPtr MojangAccount::loadFromJson(const QJsonObject &object) return nullptr; } - ProfileList profiles; + QList<AccountProfile> profiles; for (QJsonValue profileVal : profileArray) { QJsonObject profileObject = profileVal.toObject(); @@ -154,67 +57,116 @@ MojangAccountPtr MojangAccount::loadFromJson(const QJsonObject &object) QLOG_WARN() << "Unable to load a profile because it was missing an ID or a name."; continue; } - profiles.append(AccountProfile(id, name)); + profiles.append({id, name}); } - MojangAccountPtr account(new MojangAccount(username, clientToken, accessToken)); - account->loadProfiles(profiles); + MojangAccountPtr account(new MojangAccount()); + account->m_username = username; + account->m_clientToken = clientToken; + account->m_accessToken = accessToken; + account->m_profiles = profiles; // Get the currently selected profile. QString currentProfile = object.value("activeProfile").toString(""); if (!currentProfile.isEmpty()) - account->setProfile(currentProfile); + account->setCurrentProfile(currentProfile); return account; } -QJsonObject MojangAccount::saveToJson() +MojangAccountPtr MojangAccount::createFromUsername(const QString& username) +{ + MojangAccountPtr account(new MojangAccount()); + account->m_clientToken = QUuid::createUuid().toString().remove(QRegExp("[{}-]")); + account->m_username = username; + return account; +} + +QJsonObject MojangAccount::saveToJson() const { QJsonObject json; - json.insert("username", username()); - json.insert("clientToken", clientToken()); - json.insert("accessToken", accessToken()); + json.insert("username", m_username); + json.insert("clientToken", m_clientToken); + json.insert("accessToken", m_accessToken); QJsonArray profileArray; for (AccountProfile profile : m_profiles) { QJsonObject profileObj; - profileObj.insert("id", profile.id()); - profileObj.insert("name", profile.name()); + profileObj.insert("id", profile.id); + profileObj.insert("name", profile.name); profileArray.append(profileObj); } json.insert("profiles", profileArray); - if (currentProfile() != nullptr) - json.insert("activeProfile", currentProfile()->id()); + if (m_currentProfile != -1) + json.insert("activeProfile", currentProfile()->id); return json; } - -AccountProfile::AccountProfile(const QString& id, const QString& name) +bool MojangAccount::setCurrentProfile(const QString &profileId) { - m_id = id; - m_name = name; + for (int i = 0; i < m_profiles.length(); i++) + { + if (m_profiles[i].id == profileId) + { + m_currentProfile = i; + return true; + } + } + return false; } -AccountProfile::AccountProfile(const AccountProfile &other) +const AccountProfile* MojangAccount::currentProfile() const { - m_id = other.m_id; - m_name = other.m_name; + if(m_currentProfile == -1) + return nullptr; + return &m_profiles[m_currentProfile]; } -QString AccountProfile::id() const +AccountStatus MojangAccount::accountStatus() const { - return m_id; + if(m_accessToken.isEmpty()) + return NotVerified; + if(!m_online) + return Verified; + return Online; } -QString AccountProfile::name() const +std::shared_ptr<Task> MojangAccount::login(QString password) { - return m_name; + if(m_currentTask) + return m_currentTask; + if(password.isEmpty()) + { + m_currentTask.reset(new RefreshTask(this)); + } + else + { + m_currentTask.reset(new AuthenticateTask(this, password)); + } + connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); + connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); + return m_currentTask; } -void MojangAccount::propagateChange() +void MojangAccount::authSucceeded() { + m_online = true; + m_currentTask.reset(); emit changed(); } + +void MojangAccount::authFailed(QString reason) +{ + // This is emitted when the yggdrasil tasks time out or are cancelled. + // -> we treat the error as no-op + if(reason != "Yggdrasil task cancelled.") + { + m_online = false; + m_accessToken = QString(); + emit changed(); + } + m_currentTask.reset(); +} diff --git a/logic/auth/MojangAccount.h b/logic/auth/MojangAccount.h index 25a85790..8c44ce58 100644 --- a/logic/auth/MojangAccount.h +++ b/logic/auth/MojangAccount.h @@ -23,34 +23,26 @@ #include <memory> +class Task; +class YggdrasilTask; class MojangAccount; typedef std::shared_ptr<MojangAccount> MojangAccountPtr; Q_DECLARE_METATYPE(MojangAccountPtr) /** - * Class that represents a profile within someone's Mojang account. + * A profile within someone's Mojang account. * * Currently, the profile system has not been implemented by Mojang yet, * but we might as well add some things for it in MultiMC right now so * we don't have to rip the code to pieces to add it later. */ -class AccountProfile +struct AccountProfile { -public: - AccountProfile(const QString &id, const QString &name); - AccountProfile(const AccountProfile &other); - - QString id() const; - QString name() const; - -protected: - QString m_id; - QString m_name; + QString id; + QString name; }; -typedef QList<AccountProfile> ProfileList; - struct User { QString id; @@ -59,6 +51,13 @@ struct User QList<QPair<QString, QString>> properties; }; +enum AccountStatus +{ + NotVerified, + Verified, + Online +}; + /** * Object that stores information about a certain Mojang account. * @@ -68,106 +67,112 @@ struct User class MojangAccount : public QObject { Q_OBJECT -public: - /** - * Constructs a new MojangAccount with the given username. - * The client token will be generated automatically and the access token will be blank. - */ - explicit MojangAccount(const QString &username, QObject *parent = 0); +public: /* construction */ + //! Do not copy accounts. ever. + explicit MojangAccount(const MojangAccount &other, QObject *parent) = delete; - /** - * Constructs a new MojangAccount with the given username, client token, and access token. - */ - explicit MojangAccount(const QString &username, const QString &clientToken, - const QString &accessToken, QObject *parent = 0); + //! Default constructor + explicit MojangAccount(QObject *parent = 0) : QObject(parent) {}; - /** - * Constructs a new MojangAccount matching the given account. - */ - MojangAccount(const MojangAccount &other, QObject *parent); + //! Creates an empty account for the specified user name. + static MojangAccountPtr createFromUsername(const QString &username); - /** - * Loads a MojangAccount from the given JSON object. - */ + //! Loads a MojangAccount from the given JSON object. static MojangAccountPtr loadFromJson(const QJsonObject &json); - /** - * Saves a MojangAccount to a JSON object and returns it. - */ - QJsonObject saveToJson(); + //! Saves a MojangAccount to a JSON object and returns it. + QJsonObject saveToJson() const; +public: /* manipulation */ /** - * Update the account on disk and lists (it changed, for whatever reason) - * This is called by various Yggdrasil tasks. - */ - void propagateChange(); - - /** - * This MojangAccount's username. May be an email address if the account is migrated. + * Sets the currently selected profile to the profile with the given ID string. + * If profileId is not in the list of available profiles, the function will simply return + * false. */ - QString username() const; + bool setCurrentProfile(const QString &profileId); /** - * This MojangAccount's client token. This is a UUID used by Mojang's auth servers to identify this client. - * This is unique for each MojangAccount. + * Attempt to login. Empty password means we use the token. + * If the attempt fails because we already are performing some task, it returns false. */ - QString clientToken() const; + std::shared_ptr<Task> login(QString password = QString()); - /** - * Sets the MojangAccount's client token to the given value. - */ - void setClientToken(const QString &token); +public: /* queries */ + const QString &username() const + { + return m_username; + } - /** - * This MojangAccount's access token. - * If the user has not chosen to stay logged in, this will be an empty string. - */ - QString accessToken() const; + const QString &clientToken() const + { + return m_clientToken; + } - /** - * Changes this MojangAccount's access token to the given value. - */ - void setAccessToken(const QString &token); + const QString &accessToken() const + { + return m_accessToken; + } - /** - * Get full session ID - */ - QString sessionId() const; + const QList<AccountProfile> &profiles() const + { + return m_profiles; + } - /** - * Returns a list of the available account profiles. - */ - const ProfileList profiles() const; + //! Get the session ID required for legacy Minecraft versions + QString sessionId() const + { + if (m_currentProfile != -1 && !m_accessToken.isEmpty()) + return "token:" + m_accessToken + ":" + m_profiles[m_currentProfile].id; + return "-"; + } - /** - * Returns a pointer to the currently selected profile. - * If no profile is selected, returns the first profile in the profile list or nullptr if there are none. - */ + //! Returns the currently selected profile (if none, returns nullptr) const AccountProfile *currentProfile() const; - /** - * Sets the currently selected profile to the profile with the given ID string. - * If profileId is not in the list of available profiles, the function will simply return false. - */ - bool setProfile(const QString &profileId); - - /** - * Clears the current account profile list and replaces it with the given profile list. - */ - void loadProfiles(const ProfileList &profiles); + //! Returns whether the account is NotVerified, Verified or Online + AccountStatus accountStatus() const; signals: /** - * This isgnal is emitted whrn the account changes + * This signal is emitted when the account changes */ void changed(); -protected: + // TODO: better signalling for the various possible state changes - especially errors + +protected: /* variables */ QString m_username; + + // Used to identify the client - the user can have multiple clients for the same account + // Think: different launchers, all connecting to the same account/profile QString m_clientToken; - QString m_accessToken; // Blank if not logged in. - int m_currentProfile; // Index of the selected profile within the list of available - // profiles. -1 if nothing is selected. - ProfileList m_profiles; // List of available profiles. - User m_user; // the user structure, whatever it is. + + // Blank if not logged in. + QString m_accessToken; + + // Index of the selected profile within the list of available + // profiles. -1 if nothing is selected. + int m_currentProfile = -1; + + // List of available profiles. + QList<AccountProfile> m_profiles; + + // the user structure, whatever it is. + User m_user; + + // true when the account is verified + bool m_online = false; + + // current task we are executing here + std::shared_ptr<YggdrasilTask> m_currentTask; + +private slots: + void authSucceeded(); + void authFailed(QString reason); + +public: + friend class YggdrasilTask; + friend class AuthenticateTask; + friend class ValidateTask; + friend class RefreshTask; }; diff --git a/logic/auth/YggdrasilTask.cpp b/logic/auth/YggdrasilTask.cpp index 797e84cd..6b938ea7 100644 --- a/logic/auth/YggdrasilTask.cpp +++ b/logic/auth/YggdrasilTask.cpp @@ -25,16 +25,9 @@ #include <MultiMC.h> #include <logic/auth/MojangAccount.h> -YggdrasilTask::YggdrasilTask(MojangAccountPtr account, QObject *parent) : Task(parent) +YggdrasilTask::YggdrasilTask(MojangAccount *account, QObject *parent) + : Task(parent), m_account(account) { - m_error = nullptr; - m_account = account; -} - -YggdrasilTask::~YggdrasilTask() -{ - if (m_error) - delete m_error; } void YggdrasilTask::executeTask() @@ -45,97 +38,124 @@ void YggdrasilTask::executeTask() QJsonDocument doc(getRequestContent()); auto worker = MMC->qnam(); - connect(worker.get(), SIGNAL(finished(QNetworkReply *)), this, - SLOT(processReply(QNetworkReply *))); - QUrl reqUrl("https://authserver.mojang.com/" + getEndpoint()); QNetworkRequest netRequest(reqUrl); netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); QByteArray requestData = doc.toJson(); m_netReply = worker->post(netRequest, requestData); + connect(m_netReply, &QNetworkReply::finished, this, &YggdrasilTask::processReply); + connect(m_netReply, &QNetworkReply::uploadProgress, this, &YggdrasilTask::refreshTimers); + connect(m_netReply, &QNetworkReply::downloadProgress, this, &YggdrasilTask::refreshTimers); + 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, &YggdrasilTask::abort); + connect(&counter, &QTimer::timeout, this, &YggdrasilTask::heartbeat); } -void YggdrasilTask::processReply(QNetworkReply *reply) +void YggdrasilTask::refreshTimers(qint64, qint64) { - setStatus(getStateMessage(STATE_PROCESSING_RESPONSE)); + timeout_keeper.stop(); + timeout_keeper.start(timeout_max); + progress(count = 0, timeout_max); +} +void YggdrasilTask::heartbeat() +{ + count += time_step; + progress(count, timeout_max); +} - if (m_netReply != reply) - // Wrong reply for some reason... - return; +void YggdrasilTask::abort() +{ + progress(timeout_max, timeout_max); + m_netReply->abort(); +} - if (reply->error() == QNetworkReply::OperationCanceledError) +void YggdrasilTask::processReply() +{ + setStatus(getStateMessage(STATE_PROCESSING_RESPONSE)); + + if (m_netReply->error() == QNetworkReply::OperationCanceledError) { + // WARNING/FIXME: the value here is used in MojangAccount to detect the cancel/timeout 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 = m_netReply->readAll(); + QJsonDocument doc = QJsonDocument::fromJson(replyData, &jsonError); + // Check the response code. + int responseCode = m_netReply->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(); - - 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) { - // 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) - { - if (!processResponse(replyData.size() > 0 ? doc.object() : QJsonObject())) - { - 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(); - } - } - else + if (processResponse(replyData.size() > 0 ? doc.object() : QJsonObject())) { - emitFailed(tr("Failed to parse Yggdrasil JSON response: %1 at offset %2.").arg(jsonError.errorString()).arg(jsonError.offset)); + emitSucceeded(); + return; } + + // errors happened anyway? + emitFailed(m_error ? m_error->m_errorMessageVerbose + : tr("An unknown error occurred when processing the response " + "from the authentication server.")); } 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())); - } + emitFailed(tr("Failed to parse Yggdrasil JSON response: %1 at offset %2.") + .arg(jsonError.errorString()) + .arg(jsonError.offset)); } + 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. + 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(m_netReply->errorString())); } } QString YggdrasilTask::processError(QJsonObject responseData) { QJsonValue errorVal = responseData.value("error"); - QJsonValue msgVal = responseData.value("errorMessage"); + QJsonValue errorMessageValue = responseData.value("errorMessage"); QJsonValue causeVal = responseData.value("cause"); - if (errorVal.isString() && msgVal.isString()) + if (errorVal.isString() && errorMessageValue.isString()) { - m_error = new Error(errorVal.toString(""), msgVal.toString(""), causeVal.toString("")); - return m_error->getDisplayMessage(); + m_error = std::shared_ptr<Error>(new Error{ + errorVal.toString(""), errorMessageValue.toString(""), causeVal.toString("")}); + return m_error->m_errorMessageVerbose; } else { @@ -156,13 +176,3 @@ QString YggdrasilTask::getStateMessage(const YggdrasilTask::State state) const return tr("Processing. Please wait."); } } - -YggdrasilTask::Error *YggdrasilTask::getError() const -{ - return this->m_error; -} - -MojangAccountPtr YggdrasilTask::getMojangAccount() const -{ - return this->m_account; -} diff --git a/logic/auth/YggdrasilTask.h b/logic/auth/YggdrasilTask.h index 62638c9d..1f81a2d0 100644 --- a/logic/auth/YggdrasilTask.h +++ b/logic/auth/YggdrasilTask.h @@ -19,6 +19,7 @@ #include <QString> #include <QJsonObject> +#include <QTimer> #include "logic/auth/MojangAccount.h" @@ -31,45 +32,18 @@ class YggdrasilTask : public Task { Q_OBJECT public: - explicit YggdrasilTask(MojangAccountPtr account, QObject *parent = 0); - ~YggdrasilTask(); + explicit YggdrasilTask(MojangAccount * account, QObject *parent = 0); /** * Class describing a Yggdrasil error response. */ - class Error + struct Error { - public: - Error(const QString& shortError, const QString& errorMessage, const QString& cause) : - m_shortError(shortError), m_errorMessage(errorMessage), m_cause(cause) {} - - QString getShortError() const { return m_shortError; } - QString getErrorMessage() const { return m_errorMessage; } - QString getCause() const { return m_cause; } - - /// Gets the string to display in the GUI for describing this error. - QString getDisplayMessage() - { - return getErrorMessage(); - } - - protected: - QString m_shortError; - QString m_errorMessage; + QString m_errorMessageShort; + QString m_errorMessageVerbose; QString m_cause; }; - /** - * Gets the Mojang account that this task is operating on. - */ - virtual MojangAccountPtr getMojangAccount() const; - - /** - * Returns a pointer to a YggdrasilTask::Error object if an error has occurred. - * If no error has occurred, returns a null pointer. - */ - virtual Error *getError() const; - protected: /** * Enum for describing the state of the current task. @@ -120,13 +94,25 @@ protected: */ virtual QString getStateMessage(const State state) const; - MojangAccountPtr m_account; - - QNetworkReply *m_netReply; - - Error *m_error; - protected slots: - void processReply(QNetworkReply *reply); + void processReply(); + void refreshTimers(qint64, qint64); + void heartbeat(); + +public +slots: + virtual void abort() override; + +protected: + // FIXME: segfault disaster waiting to happen + MojangAccount *m_account = nullptr; + QNetworkReply *m_netReply = nullptr; + std::shared_ptr<Error> m_error; + QTimer timeout_keeper; + QTimer counter; + int count = 0; // num msec since time reset + + const int timeout_max = 10000; + const int time_step = 50; }; diff --git a/logic/auth/flows/AuthenticateTask.cpp b/logic/auth/flows/AuthenticateTask.cpp index ec2004d6..966548ec 100644 --- a/logic/auth/flows/AuthenticateTask.cpp +++ b/logic/auth/flows/AuthenticateTask.cpp @@ -26,7 +26,7 @@ #include "logger/QsLog.h" -AuthenticateTask::AuthenticateTask(MojangAccountPtr account, const QString &password, +AuthenticateTask::AuthenticateTask(MojangAccount * account, const QString &password, QObject *parent) : YggdrasilTask(account, parent), m_password(password) { @@ -59,14 +59,14 @@ QJsonObject AuthenticateTask::getRequestContent() const req.insert("agent", agent); } - req.insert("username", getMojangAccount()->username()); + req.insert("username", m_account->username()); req.insert("password", m_password); req.insert("requestUser", true); // If we already have a client token, give it to the server. // Otherwise, let the server give us one. - if (!getMojangAccount()->clientToken().isEmpty()) - req.insert("clientToken", getMojangAccount()->clientToken()); + if (!m_account->m_clientToken.isEmpty()) + req.insert("clientToken", m_account->m_clientToken); return req; } @@ -88,8 +88,7 @@ bool AuthenticateTask::processResponse(QJsonObject responseData) QLOG_ERROR() << "Server didn't send a client token."; return false; } - if (!getMojangAccount()->clientToken().isEmpty() && - clientToken != getMojangAccount()->clientToken()) + if (!m_account->m_clientToken.isEmpty() && clientToken != m_account->m_clientToken) { // The server changed our client token! Obey its wishes, but complain. That's what I do // for my parents, so... @@ -97,7 +96,7 @@ bool AuthenticateTask::processResponse(QJsonObject responseData) << "'. This shouldn't happen, but it isn't really a big deal."; } // Set the client token. - getMojangAccount()->setClientToken(clientToken); + m_account->m_clientToken = clientToken; // Now, we set the access token. QLOG_DEBUG() << "Getting access token."; @@ -109,7 +108,7 @@ bool AuthenticateTask::processResponse(QJsonObject responseData) QLOG_ERROR() << "Server didn't send an access token."; } // Set the access token. - getMojangAccount()->setAccessToken(accessToken); + m_account->m_accessToken = accessToken; // Now we load the list of available profiles. // Mojang hasn't yet implemented the profile system, @@ -117,7 +116,7 @@ bool AuthenticateTask::processResponse(QJsonObject responseData) // don't have trouble implementing it later. QLOG_DEBUG() << "Loading profile list."; QJsonArray availableProfiles = responseData.value("availableProfiles").toArray(); - ProfileList loadedProfiles; + QList<AccountProfile> loadedProfiles; for (auto iter : availableProfiles) { QJsonObject profile = iter.toObject(); @@ -135,10 +134,10 @@ bool AuthenticateTask::processResponse(QJsonObject responseData) } // Now, add a new AccountProfile entry to the list. - loadedProfiles.append(AccountProfile(id, name)); + loadedProfiles.append({id, name}); } // Put the list of profiles we loaded into the MojangAccount object. - getMojangAccount()->loadProfiles(loadedProfiles); + m_account->m_profiles = loadedProfiles; // Finally, we set the current profile to the correct value. This is pretty simple. // We do need to make sure that the current profile that the server gave us @@ -153,7 +152,7 @@ bool AuthenticateTask::processResponse(QJsonObject responseData) QLOG_ERROR() << "Server didn't specify a currently selected profile."; return false; } - if (!getMojangAccount()->setProfile(currentProfileId)) + if (!m_account->setCurrentProfile(currentProfileId)) { // TODO: Set an error to display to the user. QLOG_ERROR() << "Server specified a selected profile that wasn't in the available " diff --git a/logic/auth/flows/AuthenticateTask.h b/logic/auth/flows/AuthenticateTask.h index 3b99caad..b6564657 100644 --- a/logic/auth/flows/AuthenticateTask.h +++ b/logic/auth/flows/AuthenticateTask.h @@ -30,7 +30,7 @@ class AuthenticateTask : public YggdrasilTask { Q_OBJECT public: - AuthenticateTask(MojangAccountPtr account, const QString &password, QObject *parent = 0); + AuthenticateTask(MojangAccount *account, const QString &password, QObject *parent = 0); protected: virtual QJsonObject getRequestContent() const; diff --git a/logic/auth/flows/InvalidateTask.cpp b/logic/auth/flows/InvalidateTask.cpp deleted file mode 100644 index e69de29b..00000000 --- a/logic/auth/flows/InvalidateTask.cpp +++ /dev/null diff --git a/logic/auth/flows/InvalidateTask.h b/logic/auth/flows/InvalidateTask.h deleted file mode 100644 index e69de29b..00000000 --- a/logic/auth/flows/InvalidateTask.h +++ /dev/null diff --git a/logic/auth/flows/RefreshTask.cpp b/logic/auth/flows/RefreshTask.cpp index b56ed9bc..bd38eb10 100644 --- a/logic/auth/flows/RefreshTask.cpp +++ b/logic/auth/flows/RefreshTask.cpp @@ -25,7 +25,7 @@ #include "logger/QsLog.h" -RefreshTask::RefreshTask(MojangAccountPtr account, QObject *parent) +RefreshTask::RefreshTask(MojangAccount *account, QObject *parent) : YggdrasilTask(account, parent) { } @@ -44,13 +44,12 @@ QJsonObject RefreshTask::getRequestContent() const * "requestUser": true/false // request the user structure * } */ - auto account = getMojangAccount(); QJsonObject req; - req.insert("clientToken", account->clientToken()); - req.insert("accessToken", account->accessToken()); + req.insert("clientToken", m_account->m_clientToken); + req.insert("accessToken", m_account->m_accessToken); /* { - auto currentProfile = account->currentProfile(); + auto currentProfile = m_account->currentProfile(); QJsonObject profile; profile.insert("id", currentProfile->id()); profile.insert("name", currentProfile->name()); @@ -64,8 +63,6 @@ QJsonObject RefreshTask::getRequestContent() const bool RefreshTask::processResponse(QJsonObject responseData) { - auto account = getMojangAccount(); - // Read the response data. We need to get the client token, access token, and the selected // profile. QLOG_DEBUG() << "Processing authentication response."; @@ -80,7 +77,7 @@ bool RefreshTask::processResponse(QJsonObject responseData) QLOG_ERROR() << "Server didn't send a client token."; return false; } - if (!account->clientToken().isEmpty() && clientToken != account->clientToken()) + if (!m_account->m_clientToken.isEmpty() && clientToken != m_account->m_clientToken) { // The server changed our client token! Obey its wishes, but complain. That's what I do // for my parents, so... @@ -104,7 +101,7 @@ bool RefreshTask::processResponse(QJsonObject responseData) // profile) QJsonObject currentProfile = responseData.value("selectedProfile").toObject(); QString currentProfileId = currentProfile.value("id").toString(""); - if (account->currentProfile()->id() != currentProfileId) + if (m_account->currentProfile()->id != currentProfileId) { // TODO: Set an error to display to the user. QLOG_ERROR() << "Server didn't specify the same selected profile as ours."; @@ -132,8 +129,7 @@ bool RefreshTask::processResponse(QJsonObject responseData) // we've succeeded. QLOG_DEBUG() << "Finished reading refresh response."; // Reset the access token. - account->setAccessToken(accessToken); - account->propagateChange(); + m_account->m_accessToken = accessToken; return true; } @@ -147,9 +143,9 @@ QString RefreshTask::getStateMessage(const YggdrasilTask::State state) const switch (state) { case STATE_SENDING_REQUEST: - return tr("Refreshing: Sending request."); + return tr("Refreshing login token."); case STATE_PROCESSING_RESPONSE: - return tr("Refreshing: Processing response."); + return tr("Refreshing login token: Processing response."); default: return YggdrasilTask::getStateMessage(state); } diff --git a/logic/auth/flows/RefreshTask.h b/logic/auth/flows/RefreshTask.h index 2596f6c7..2fd50c60 100644 --- a/logic/auth/flows/RefreshTask.h +++ b/logic/auth/flows/RefreshTask.h @@ -30,7 +30,7 @@ class RefreshTask : public YggdrasilTask { Q_OBJECT public: - RefreshTask(MojangAccountPtr account, QObject *parent = 0); + RefreshTask(MojangAccount * account, QObject *parent = 0); protected: virtual QJsonObject getRequestContent() const; diff --git a/logic/auth/flows/ValidateTask.cpp b/logic/auth/flows/ValidateTask.cpp index d9e0e46b..84d5e703 100644 --- a/logic/auth/flows/ValidateTask.cpp +++ b/logic/auth/flows/ValidateTask.cpp @@ -26,7 +26,7 @@ #include "logger/QsLog.h" -ValidateTask::ValidateTask(MojangAccountPtr account, QObject *parent) +ValidateTask::ValidateTask(MojangAccount * account, QObject *parent) : YggdrasilTask(account, parent) { } @@ -34,7 +34,7 @@ ValidateTask::ValidateTask(MojangAccountPtr account, QObject *parent) QJsonObject ValidateTask::getRequestContent() const { QJsonObject req; - req.insert("accessToken", getMojangAccount()->accessToken()); + req.insert("accessToken", m_account->m_accessToken); return req; } diff --git a/logic/auth/flows/ValidateTask.h b/logic/auth/flows/ValidateTask.h index 3ff78c6a..0e34f0c3 100644 --- a/logic/auth/flows/ValidateTask.h +++ b/logic/auth/flows/ValidateTask.h @@ -13,6 +13,10 @@ * limitations under the License. */ +/* + * :FIXME: DEAD CODE, DEAD CODE, DEAD CODE! :FIXME: + */ + #pragma once #include <logic/auth/YggdrasilTask.h> @@ -28,7 +32,7 @@ class ValidateTask : public YggdrasilTask { Q_OBJECT public: - ValidateTask(MojangAccountPtr account, QObject *parent = 0); + ValidateTask(MojangAccount *account, QObject *parent = 0); protected: virtual QJsonObject getRequestContent() const; diff --git a/logic/lists/MojangAccountList.cpp b/logic/lists/MojangAccountList.cpp index 466cc934..defa5d8c 100644 --- a/logic/lists/MojangAccountList.cpp +++ b/logic/lists/MojangAccountList.cpp @@ -83,10 +83,7 @@ void MojangAccountList::removeAccount(QModelIndex index) MojangAccountPtr MojangAccountList::activeAccount() const { - if (m_activeAccount.isEmpty()) - return nullptr; - else - return findAccount(m_activeAccount); + return m_activeAccount; } void MojangAccountList::setActiveAccount(const QString &username) @@ -94,14 +91,14 @@ void MojangAccountList::setActiveAccount(const QString &username) beginResetModel(); if (username.isEmpty()) { - m_activeAccount = ""; + m_activeAccount = nullptr; } else { for (MojangAccountPtr account : m_accounts) { if (account->username() == username) - m_activeAccount = username; + m_activeAccount = account; } } endResetModel(); @@ -152,7 +149,7 @@ QVariant MojangAccountList::data(const QModelIndex &index, int role) const switch (index.column()) { case ActiveColumn: - return account->username() == m_activeAccount; + return account == m_activeAccount; case NameColumn: return account->username(); @@ -297,11 +294,9 @@ bool MojangAccountList::loadList(const QString &filePath) QLOG_WARN() << "Failed to load an account."; } } - endResetModel(); - // Load the active account. - m_activeAccount = root.value("activeAccount").toString(""); - + m_activeAccount = findAccount(root.value("activeAccount").toString("")); + endResetModel(); return true; } @@ -336,8 +331,11 @@ bool MojangAccountList::saveList(const QString &filePath) // Insert the account list into the root object. root.insert("accounts", accounts); - // Save the active account. - root.insert("activeAccount", m_activeAccount); + if(m_activeAccount) + { + // Save the active account. + root.insert("activeAccount", m_activeAccount->username()); + } // Create a JSON document object to convert our JSON to bytes. QJsonDocument doc(root); diff --git a/logic/lists/MojangAccountList.h b/logic/lists/MojangAccountList.h index 744f3c51..b3301bf6 100644 --- a/logic/lists/MojangAccountList.h +++ b/logic/lists/MojangAccountList.h @@ -161,10 +161,9 @@ protected: QList<MojangAccountPtr> m_accounts; /*! - * Username of the account that is currently active. - * Empty string if no account is active. + * Account that is currently active. */ - QString m_activeAccount; + MojangAccountPtr m_activeAccount; //! Path to the account list file. Empty string if there isn't one. QString m_listFilePath; diff --git a/logic/net/NetJob.h b/logic/net/NetJob.h index 021a1550..a7027e10 100644 --- a/logic/net/NetJob.h +++ b/logic/net/NetJob.h @@ -94,6 +94,8 @@ signals: public slots: virtual void start(); + // FIXME: implement + virtual void abort() {}; private slots: void partProgress(int index, qint64 bytesReceived, qint64 bytesTotal); diff --git a/logic/net/PasteUpload.cpp b/logic/net/PasteUpload.cpp new file mode 100644 index 00000000..acf09291 --- /dev/null +++ b/logic/net/PasteUpload.cpp @@ -0,0 +1,84 @@ +#include "PasteUpload.h" +#include "MultiMC.h" +#include "logger/QsLog.h" +#include <QJsonObject> +#include <QJsonDocument> +#include "gui/dialogs/CustomMessageBox.h" +#include <QDesktopServices> + +PasteUpload::PasteUpload(QWidget *window, QString text) : m_text(text), m_window(window) +{ +} + +void PasteUpload::executeTask() +{ + QNetworkRequest request(QUrl("http://paste.ee/api")); + request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Uncached)"); + QByteArray content( + "key=public&description=MultiMC5+Log+File&language=plain&format=json&paste=" + + m_text.toUtf8()); + request.setRawHeader("Content-Type", "application/x-www-form-urlencoded"); + request.setRawHeader("Content-Length", QByteArray::number(content.size())); + + auto worker = MMC->qnam(); + QNetworkReply *rep = worker->post(request, content); + + m_reply = std::shared_ptr<QNetworkReply>(rep); + connect(rep, &QNetworkReply::downloadProgress, [&](qint64 value, qint64 max) + { setProgress(value / max * 100); }); + connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, + SLOT(downloadError(QNetworkReply::NetworkError))); + connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished())); +} + +void PasteUpload::downloadError(QNetworkReply::NetworkError error) +{ + // error happened during download. + QLOG_ERROR() << "Network error: " << error; + emitFailed(m_reply->errorString()); +} + +void PasteUpload::downloadFinished() +{ + // if the download succeeded + if (m_reply->error() == QNetworkReply::NetworkError::NoError) + { + QByteArray data = m_reply->readAll(); + m_reply.reset(); + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if (jsonError.error != QJsonParseError::NoError) + { + emitFailed(jsonError.errorString()); + return; + } + QString error; + if (!parseResult(doc, &error)) + { + emitFailed(error); + return; + } + } + // else the download failed + else + { + emitFailed(QString("Network error: %1").arg(m_reply->errorString())); + m_reply.reset(); + return; + } + emitSucceeded(); +} + +bool PasteUpload::parseResult(QJsonDocument doc, QString *parseError) +{ + auto object = doc.object(); + auto status = object.value("status").toString("error"); + if (status == "error") + { + parseError = new QString(object.value("error").toString()); + return false; + } + QString pasteUrl = object.value("paste").toObject().value("link").toString(); + QDesktopServices::openUrl(pasteUrl); + return true; +} diff --git a/logic/net/PasteUpload.h b/logic/net/PasteUpload.h new file mode 100644 index 00000000..917a0016 --- /dev/null +++ b/logic/net/PasteUpload.h @@ -0,0 +1,26 @@ +#pragma once +#include "logic/tasks/Task.h" +#include <QMessageBox> +#include <QNetworkReply> +#include <memory> + +class PasteUpload : public Task +{ + Q_OBJECT +public: + PasteUpload(QWidget *window, QString text); + +protected: + virtual void executeTask(); + +private: + bool parseResult(QJsonDocument doc, QString *parseError); + QString m_text; + QString m_error; + QWidget *m_window; + std::shared_ptr<QNetworkReply> m_reply; +public +slots: + void downloadError(QNetworkReply::NetworkError); + void downloadFinished(); +}; diff --git a/logic/tasks/ProgressProvider.h b/logic/tasks/ProgressProvider.h index f6f2906a..15e453a3 100644 --- a/logic/tasks/ProgressProvider.h +++ b/logic/tasks/ProgressProvider.h @@ -38,4 +38,5 @@ public: public slots: virtual void start() = 0; + virtual void abort() = 0; }; diff --git a/logic/tasks/Task.h b/logic/tasks/Task.h index d08ef560..80d5e38b 100644 --- a/logic/tasks/Task.h +++ b/logic/tasks/Task.h @@ -44,6 +44,7 @@ public: public slots: virtual void start(); + virtual void abort() {}; protected: virtual void executeTask() = 0; |