Merge branch 'feature_yggdrasil' into develop

Conflicts:
	gui/MainWindow.cpp
	logic/OneSixInstance.h

Fix missing session id functionality for legacy and old onesix.
This commit is contained in:
Petr Mrázek
2013-11-24 18:41:35 +01:00
24 changed files with 1849 additions and 132 deletions

View File

@ -0,0 +1,180 @@
/* 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 <logic/auth/AuthenticateTask.h>
#include <logic/auth/MojangAccount.h>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QVariant>
#include <QDebug>
#include "logger/QsLog.h"
AuthenticateTask::AuthenticateTask(MojangAccountPtr account, const QString& password, QObject* parent) :
YggdrasilTask(account, parent), m_password(password)
{
}
QJsonObject AuthenticateTask::getRequestContent() const
{
/*
* {
* "agent": { // optional
* "name": "Minecraft", // So far this is the only encountered value
* "version": 1 // This number might be increased
* // by the vanilla client in the future
* },
* "username": "mojang account name", // Can be an email address or player name for
// unmigrated accounts
* "password": "mojang account password",
* "clientToken": "client identifier" // optional
* }
*/
QJsonObject req;
{
QJsonObject agent;
// C++ makes string literals void* for some stupid reason, so we have to tell it QString... Thanks Obama.
agent.insert("name", QString("Minecraft"));
agent.insert("version", 1);
req.insert("agent", agent);
}
req.insert("username", getMojangAccount()->username());
req.insert("password", m_password);
// 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());
return req;
}
bool AuthenticateTask::processResponse(QJsonObject responseData)
{
// Read the response data. We need to get the client token, access token, and the selected profile.
QLOG_DEBUG() << "Processing authentication response.";
// If we already have a client token, make sure the one the server gave us matches our existing one.
QLOG_DEBUG() << "Getting client token.";
QString clientToken = responseData.value("clientToken").toString("");
if (clientToken.isEmpty())
{
// Fail if the server gave us an empty client token
// TODO: Set an error properly to display to the user.
QLOG_ERROR() << "Server didn't send a client token.";
return false;
}
if (!getMojangAccount()->clientToken().isEmpty() && clientToken != getMojangAccount()->clientToken())
{
// The server changed our client token! Obey its wishes, but complain. That's what I do for my parents, so...
QLOG_WARN() << "Server changed our client token to '" << clientToken
<< "'. This shouldn't happen, but it isn't really a big deal.";
}
// Set the client token.
getMojangAccount()->setClientToken(clientToken);
// Now, we set the access token.
QLOG_DEBUG() << "Getting access token.";
QString accessToken = responseData.value("accessToken").toString("");
if (accessToken.isEmpty())
{
// Fail if the server didn't give us an access token.
// TODO: Set an error properly to display to the user.
QLOG_ERROR() << "Server didn't send an access token.";
}
// Set the access token.
getMojangAccount()->setAccessToken(accessToken);
// Now we load the list of available profiles.
// Mojang hasn't yet implemented the profile system,
// but we might as well support what's there so we
// don't have trouble implementing it later.
QLOG_DEBUG() << "Loading profile list.";
QJsonArray availableProfiles = responseData.value("availableProfiles").toArray();
ProfileList loadedProfiles;
for (auto iter : availableProfiles)
{
QJsonObject profile = iter.toObject();
// Profiles are easy, we just need their ID and name.
QString id = profile.value("id").toString("");
QString name = profile.value("name").toString("");
if (id.isEmpty() || name.isEmpty())
{
// This should never happen, but we might as well
// warn about it if it does so we can debug it easily.
// You never know when Mojang might do something truly derpy.
QLOG_WARN() << "Found entry in available profiles list with missing ID or name field. Ignoring it.";
}
// Now, add a new AccountProfile entry to the list.
loadedProfiles.append(AccountProfile(id, name));
}
// Put the list of profiles we loaded into the MojangAccount object.
getMojangAccount()->loadProfiles(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
// is actually in the available profiles list.
// If it isn't, we'll just fail horribly (*shouldn't* ever happen, but you never know).
QLOG_DEBUG() << "Setting current profile.";
QJsonObject currentProfile = responseData.value("selectedProfile").toObject();
QString currentProfileId = currentProfile.value("id").toString("");
if (currentProfileId.isEmpty())
{
// TODO: Set an error to display to the user.
QLOG_ERROR() << "Server didn't specify a currently selected profile.";
return false;
}
if (!getMojangAccount()->setProfile(currentProfileId))
{
// TODO: Set an error to display to the user.
QLOG_ERROR() << "Server specified a selected profile that wasn't in the available profiles list.";
return false;
}
// We've made it through the minefield of possible errors. Return true to indicate that we've succeeded.
QLOG_DEBUG() << "Finished reading authentication response.";
return true;
}
QString AuthenticateTask::getEndpoint() const
{
return "authenticate";
}
QString AuthenticateTask::getStateMessage(const YggdrasilTask::State state) const
{
switch (state)
{
case STATE_SENDING_REQUEST:
return tr("Authenticating: Sending request.");
case STATE_PROCESSING_RESPONSE:
return tr("Authenticating: Processing response.");
default:
return YggdrasilTask::getStateMessage(state);
}
}

View File

@ -0,0 +1,46 @@
/* 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 <logic/auth/YggdrasilTask.h>
#include <QObject>
#include <QString>
#include <QJsonObject>
/**
* The authenticate task takes a MojangAccount with no access token and password and attempts to authenticate with Mojang's servers.
* If successful, it will set the MojangAccount's access token.
*/
class AuthenticateTask : public YggdrasilTask
{
Q_OBJECT
public:
AuthenticateTask(MojangAccountPtr account, const QString& password, 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:
QString m_password;
};

View File

@ -0,0 +1,220 @@
/* Copyright 2013 MultiMC Contributors
*
* Authors: Orochimarufan <orochimarufan.x3@gmail.com>
*
* 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 "MojangAccount.h"
#include <QUuid>
#include <QJsonObject>
#include <QJsonArray>
#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.
if (!object.value("username").isString())
{
QLOG_ERROR() << "Can't load Mojang account info from JSON object. Username field is missing or of the wrong type.";
return nullptr;
}
QString username = object.value("username").toString("");
QString clientToken = object.value("clientToken").toString("");
QString accessToken = object.value("accessToken").toString("");
QJsonArray profileArray = object.value("profiles").toArray();
if (profileArray.size() < 1)
{
QLOG_ERROR() << "Can't load Mojang account with username \"" << username << "\". No profiles found.";
return nullptr;
}
ProfileList profiles;
for (QJsonValue profileVal : profileArray)
{
QJsonObject profileObject = profileVal.toObject();
QString id = profileObject.value("id").toString("");
QString name = profileObject.value("name").toString("");
if (id.isEmpty() || name.isEmpty())
{
QLOG_WARN() << "Unable to load a profile because it was missing an ID or a name.";
continue;
}
profiles.append(AccountProfile(id, name));
}
MojangAccountPtr account(new MojangAccount(username, clientToken, accessToken));
account->loadProfiles(profiles);
// Get the currently selected profile.
QString currentProfile = object.value("activeProfile").toString("");
if (!currentProfile.isEmpty())
account->setProfile(currentProfile);
return account;
}
QJsonObject MojangAccount::saveToJson()
{
QJsonObject json;
json.insert("username", username());
json.insert("clientToken", clientToken());
json.insert("accessToken", accessToken());
QJsonArray profileArray;
for (AccountProfile profile : m_profiles)
{
QJsonObject profileObj;
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());
return json;
}
AccountProfile::AccountProfile(const QString& id, const QString& name)
{
m_id = id;
m_name = name;
}
AccountProfile::AccountProfile(const AccountProfile& other)
{
m_id = other.m_id;
m_name = other.m_name;
}
QString AccountProfile::id() const
{
return m_id;
}
QString AccountProfile::name() const
{
return m_name;
}

154
logic/auth/MojangAccount.h Normal file
View File

@ -0,0 +1,154 @@
/* 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 <QObject>
#include <QString>
#include <QList>
#include <QJsonObject>
#include <memory>
class MojangAccount;
typedef std::shared_ptr<MojangAccount> MojangAccountPtr;
Q_DECLARE_METATYPE(MojangAccountPtr)
/**
* Class that represents 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
{
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;
};
typedef QList<AccountProfile> ProfileList;
/**
* Object that stores information about a certain Mojang account.
*
* Said information may include things such as that account's username, client token, and access
* token if the user chose to stay logged in.
*/
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);
/**
* 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);
/**
* Constructs a new MojangAccount matching the given account.
*/
MojangAccount(const MojangAccount& other, QObject* parent);
/**
* 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();
/**
* This MojangAccount's username. May be an email address if the account is migrated.
*/
QString username() const;
/**
* 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.
*/
QString clientToken() const;
/**
* Sets the MojangAccount's client token to the given value.
*/
void setClientToken(const QString& token);
/**
* This MojangAccount's access token.
* If the user has not chosen to stay logged in, this will be an empty string.
*/
QString accessToken() const;
/**
* Changes this MojangAccount's access token to the given value.
*/
void setAccessToken(const QString& token);
/**
* Get full session ID
*/
QString sessionId() const;
/**
* Returns a list of the available account profiles.
*/
const ProfileList profiles() const;
/**
* 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.
*/
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);
protected:
QString m_username;
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.
};

View File

@ -0,0 +1,182 @@
/* 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 <logic/auth/YggdrasilTask.h>
#include <QObject>
#include <QString>
#include <QJsonObject>
#include <QJsonDocument>
#include <QNetworkReply>
#include <QByteArray>
#include <MultiMC.h>
#include <logic/auth/MojangAccount.h>
YggdrasilTask::YggdrasilTask(MojangAccountPtr account, QObject* parent) : Task(parent)
{
m_error = nullptr;
m_account = account;
}
YggdrasilTask::~YggdrasilTask()
{
if (m_error)
delete m_error;
}
void YggdrasilTask::executeTask()
{
setStatus(getStateMessage(STATE_SENDING_REQUEST));
// Get the content of the request we're going to send to the server.
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");
m_netReply = worker->post(netRequest, doc.toJson());
}
void YggdrasilTask::processReply(QNetworkReply* reply)
{
setStatus(getStateMessage(STATE_PROCESSING_RESPONSE));
if (m_netReply != reply)
// Wrong reply for some reason...
return;
// Check for errors.
switch (reply->error())
{
case QNetworkReply::NoError:
{
// 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)
{
case 200:
{
// 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;
}
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;
}
case QNetworkReply::OperationCanceledError:
emitFailed(tr("Login canceled."));
break;
default:
emitFailed(tr("An unknown error occurred when trying to communicate with the authentication server."));
break;
}
}
QString YggdrasilTask::processError(QJsonObject responseData)
{
QJsonValue errorVal = responseData.value("error");
QJsonValue msgVal = responseData.value("errorMessage");
QJsonValue causeVal = responseData.value("cause");
if (errorVal.isString() && msgVal.isString() && causeVal.isString())
{
m_error = new Error(errorVal.toString(""), msgVal.toString(""), causeVal.toString(""));
return m_error->getDisplayMessage();
}
else
{
// Error is not in standard format. Don't set m_error and return unknown error.
return tr("An unknown Yggdrasil error occurred.");
}
}
QString YggdrasilTask::getStateMessage(const YggdrasilTask::State state) const
{
switch (state)
{
case STATE_SENDING_REQUEST:
return tr("Sending request to auth servers.");
case STATE_PROCESSING_RESPONSE:
return tr("Processing response from servers.");
default:
return tr("Processing. Please wait.");
}
}
YggdrasilTask::Error *YggdrasilTask::getError() const
{
return this->m_error;
}
MojangAccountPtr YggdrasilTask::getMojangAccount() const
{
return this->m_account;
}

128
logic/auth/YggdrasilTask.h Normal file
View File

@ -0,0 +1,128 @@
/* 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 <logic/tasks/Task.h>
#include <QString>
#include <QJsonObject>
#include "logic/auth/MojangAccount.h"
class QNetworkReply;
/**
* A Yggdrasil task is a task that performs an operation on a given mojang account.
*/
class YggdrasilTask : public Task
{
Q_OBJECT
public:
explicit YggdrasilTask(MojangAccountPtr account, QObject* parent=0);
~YggdrasilTask();
/**
* Class describing a Yggdrasil error response.
*/
class 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_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.
* Used by the getStateMessage function to determine what the status message should be.
*/
enum State
{
STATE_SENDING_REQUEST,
STATE_PROCESSING_RESPONSE,
STATE_OTHER,
};
virtual void executeTask();
/**
* Gets the JSON object that will be sent to the authentication server.
* Should be overridden by subclasses.
*/
virtual QJsonObject getRequestContent() const = 0;
/**
* Gets the endpoint to POST to.
* No leading slash.
*/
virtual QString getEndpoint() const = 0;
/**
* Processes the response received from the server.
* 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.
*/
virtual bool processResponse(QJsonObject responseData) = 0;
/**
* Processes an error response received from the server.
* The default implementation will read data from Yggdrasil's standard error response format and set it as this task's Error.
* \returns a QString error message that will be passed to emitFailed.
*/
virtual QString processError(QJsonObject responseData);
/**
* Returns the state message for the given state.
* Used to set the status message for the task.
* Should be overridden by subclasses that want to change messages for a given state.
*/
virtual QString getStateMessage(const State state) const;
MojangAccountPtr m_account;
QNetworkReply* m_netReply;
Error* m_error;
protected slots:
void processReply(QNetworkReply* reply);
};