354 lines
13 KiB
C++
354 lines
13 KiB
C++
/* Copyright 2013-2021 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 "Yggdrasil.h"
|
|
#include "AccountData.h"
|
|
|
|
#include <QObject>
|
|
#include <QString>
|
|
#include <QJsonObject>
|
|
#include <QJsonDocument>
|
|
#include <QNetworkReply>
|
|
#include <QByteArray>
|
|
|
|
#include <QDebug>
|
|
|
|
#include "Application.h"
|
|
|
|
Yggdrasil::Yggdrasil(AccountData *data, QObject *parent)
|
|
: AccountTask(data, parent)
|
|
{
|
|
changeState(AccountTaskState::STATE_CREATED);
|
|
}
|
|
|
|
void Yggdrasil::sendRequest(QUrl endpoint, QByteArray content) {
|
|
changeState(AccountTaskState::STATE_WORKING);
|
|
|
|
QNetworkRequest netRequest(endpoint);
|
|
netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
|
m_netReply = APPLICATION->network()->post(netRequest, content);
|
|
connect(m_netReply, &QNetworkReply::finished, this, &Yggdrasil::processReply);
|
|
connect(m_netReply, &QNetworkReply::uploadProgress, this, &Yggdrasil::refreshTimers);
|
|
connect(m_netReply, &QNetworkReply::downloadProgress, this, &Yggdrasil::refreshTimers);
|
|
connect(m_netReply, &QNetworkReply::sslErrors, this, &Yggdrasil::sslErrors);
|
|
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, &Yggdrasil::abortByTimeout);
|
|
connect(&counter, &QTimer::timeout, this, &Yggdrasil::heartbeat);
|
|
}
|
|
|
|
void Yggdrasil::executeTask() {
|
|
}
|
|
|
|
void Yggdrasil::refresh() {
|
|
start();
|
|
/*
|
|
* {
|
|
* "clientToken": "client identifier"
|
|
* "accessToken": "current access token to be refreshed"
|
|
* "selectedProfile": // specifying this causes errors
|
|
* {
|
|
* "id": "profile ID"
|
|
* "name": "profile name"
|
|
* }
|
|
* "requestUser": true/false // request the user structure
|
|
* }
|
|
*/
|
|
QJsonObject req;
|
|
req.insert("clientToken", m_data->clientToken());
|
|
req.insert("accessToken", m_data->accessToken());
|
|
/*
|
|
{
|
|
auto currentProfile = m_account->currentProfile();
|
|
QJsonObject profile;
|
|
profile.insert("id", currentProfile->id());
|
|
profile.insert("name", currentProfile->name());
|
|
req.insert("selectedProfile", profile);
|
|
}
|
|
*/
|
|
req.insert("requestUser", false);
|
|
QJsonDocument doc(req);
|
|
|
|
QUrl reqUrl("https://authserver.mojang.com/refresh");
|
|
QByteArray requestData = doc.toJson();
|
|
|
|
sendRequest(reqUrl, requestData);
|
|
}
|
|
|
|
void Yggdrasil::login(QString password) {
|
|
start();
|
|
/*
|
|
* {
|
|
* "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
|
|
* "requestUser": true/false // request the user structure
|
|
* }
|
|
*/
|
|
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", m_data->userName());
|
|
req.insert("password", password);
|
|
req.insert("requestUser", false);
|
|
|
|
// If we already have a client token, give it to the server.
|
|
// Otherwise, let the server give us one.
|
|
|
|
m_data->generateClientTokenIfMissing();
|
|
req.insert("clientToken", m_data->clientToken());
|
|
|
|
QJsonDocument doc(req);
|
|
|
|
QUrl reqUrl("https://authserver.mojang.com/authenticate");
|
|
QNetworkRequest netRequest(reqUrl);
|
|
QByteArray requestData = doc.toJson();
|
|
|
|
sendRequest(reqUrl, requestData);
|
|
}
|
|
|
|
|
|
|
|
void Yggdrasil::refreshTimers(qint64, qint64) {
|
|
timeout_keeper.stop();
|
|
timeout_keeper.start(timeout_max);
|
|
progress(count = 0, timeout_max);
|
|
}
|
|
|
|
void Yggdrasil::heartbeat() {
|
|
count += time_step;
|
|
progress(count, timeout_max);
|
|
}
|
|
|
|
bool Yggdrasil::abort() {
|
|
progress(timeout_max, timeout_max);
|
|
// TODO: actually use this in a meaningful way
|
|
m_aborted = Yggdrasil::BY_USER;
|
|
m_netReply->abort();
|
|
return true;
|
|
}
|
|
|
|
void Yggdrasil::abortByTimeout() {
|
|
progress(timeout_max, timeout_max);
|
|
// TODO: actually use this in a meaningful way
|
|
m_aborted = Yggdrasil::BY_TIMEOUT;
|
|
m_netReply->abort();
|
|
}
|
|
|
|
void Yggdrasil::sslErrors(QList<QSslError> errors) {
|
|
int i = 1;
|
|
for (auto error : errors) {
|
|
qCritical() << "LOGIN SSL Error #" << i << " : " << error.errorString();
|
|
auto cert = error.certificate();
|
|
qCritical() << "Certificate in question:\n" << cert.toText();
|
|
i++;
|
|
}
|
|
}
|
|
|
|
void Yggdrasil::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("");
|
|
if (clientToken.isEmpty()) {
|
|
// Fail if the server gave us an empty client token
|
|
changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send a client token."));
|
|
return;
|
|
}
|
|
if(m_data->clientToken().isEmpty()) {
|
|
m_data->setClientToken(clientToken);
|
|
}
|
|
else if(clientToken != m_data->clientToken()) {
|
|
changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server attempted to change the client token. This isn't supported."));
|
|
return;
|
|
}
|
|
|
|
// Now, we set the access token.
|
|
qDebug() << "Getting access token.";
|
|
QString accessToken = responseData.value("accessToken").toString("");
|
|
if (accessToken.isEmpty()) {
|
|
// Fail if the server didn't give us an access token.
|
|
changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send an access token."));
|
|
return;
|
|
}
|
|
// Set the access token.
|
|
m_data->yggdrasilToken.token = accessToken;
|
|
m_data->yggdrasilToken.validity = Katabasis::Validity::Certain;
|
|
m_data->yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc();
|
|
|
|
// Get UUID here since we need it for later
|
|
auto profile = responseData.value("selectedProfile");
|
|
if (!profile.isObject()) {
|
|
changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send a selected profile."));
|
|
return;
|
|
}
|
|
|
|
auto profileObj = profile.toObject();
|
|
for (auto i = profileObj.constBegin(); i != profileObj.constEnd(); ++i) {
|
|
if (i.key() == "name" && i.value().isString()) {
|
|
m_data->minecraftProfile.name = i->toString();
|
|
}
|
|
else if (i.key() == "id" && i.value().isString()) {
|
|
m_data->minecraftProfile.id = i->toString();
|
|
}
|
|
}
|
|
|
|
if (m_data->minecraftProfile.id.isEmpty()) {
|
|
changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send a UUID in selected profile."));
|
|
return;
|
|
}
|
|
|
|
// We've made it through the minefield of possible errors. Return true to indicate that
|
|
// we've succeeded.
|
|
qDebug() << "Finished reading authentication response.";
|
|
changeState(AccountTaskState::STATE_SUCCEEDED);
|
|
}
|
|
|
|
void Yggdrasil::processReply() {
|
|
changeState(AccountTaskState::STATE_WORKING);
|
|
|
|
switch (m_netReply->error())
|
|
{
|
|
case QNetworkReply::NoError:
|
|
break;
|
|
case QNetworkReply::TimeoutError:
|
|
changeState(AccountTaskState::STATE_FAILED_SOFT, tr("Authentication operation timed out."));
|
|
return;
|
|
case QNetworkReply::OperationCanceledError:
|
|
changeState(AccountTaskState::STATE_FAILED_SOFT, tr("Authentication operation cancelled."));
|
|
return;
|
|
case QNetworkReply::SslHandshakeFailedError:
|
|
changeState(
|
|
AccountTaskState::STATE_FAILED_SOFT,
|
|
tr(
|
|
"<b>SSL Handshake failed.</b><br/>There might be a few causes for it:<br/>"
|
|
"<ul>"
|
|
"<li>You use Windows and need to update your root certificates, please install any outstanding updates.</li>"
|
|
"<li>Some device on your network is interfering with SSL traffic. In that case, "
|
|
"you have bigger worries than Minecraft not starting.</li>"
|
|
"<li>Possibly something else. Check the log file for details</li>"
|
|
"</ul>"
|
|
)
|
|
);
|
|
return;
|
|
// used for invalid credentials and similar errors. Fall through.
|
|
case QNetworkReply::ContentAccessDenied:
|
|
case QNetworkReply::ContentOperationNotPermittedError:
|
|
break;
|
|
case QNetworkReply::ContentGoneError: {
|
|
changeState(
|
|
AccountTaskState::STATE_FAILED_GONE,
|
|
tr("The Mojang account no longer exists. It may have been migrated to a Microsoft account.")
|
|
);
|
|
}
|
|
default:
|
|
changeState(
|
|
AccountTaskState::STATE_FAILED_SOFT,
|
|
tr("Authentication operation failed due to a network error: %1 (%2)").arg(m_netReply->errorString()).arg(m_netReply->error())
|
|
);
|
|
return;
|
|
}
|
|
|
|
// 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) {
|
|
// 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) {
|
|
processResponse(replyData.size() > 0 ? doc.object() : QJsonObject());
|
|
return;
|
|
}
|
|
else {
|
|
changeState(
|
|
AccountTaskState::STATE_FAILED_SOFT,
|
|
tr("Failed to parse authentication server response JSON response: %1 at offset %2.").arg(jsonError.errorString()).arg(jsonError.offset)
|
|
);
|
|
qCritical() << replyData;
|
|
}
|
|
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.";
|
|
changeState(
|
|
AccountTaskState::STATE_FAILED_SOFT,
|
|
tr("An unknown error occurred when trying to communicate with the authentication server: %1").arg(m_netReply->errorString())
|
|
);
|
|
}
|
|
}
|
|
|
|
void Yggdrasil::processError(QJsonObject responseData) {
|
|
QJsonValue errorVal = responseData.value("error");
|
|
QJsonValue errorMessageValue = responseData.value("errorMessage");
|
|
QJsonValue causeVal = responseData.value("cause");
|
|
|
|
if (errorVal.isString() && errorMessageValue.isString()) {
|
|
m_error = std::shared_ptr<Error>(
|
|
new Error {
|
|
errorVal.toString(""),
|
|
errorMessageValue.toString(""),
|
|
causeVal.toString("")
|
|
}
|
|
);
|
|
changeState(AccountTaskState::STATE_FAILED_HARD, m_error->m_errorMessageVerbose);
|
|
}
|
|
else {
|
|
// Error is not in standard format. Don't set m_error and return unknown error.
|
|
changeState(AccountTaskState::STATE_FAILED_HARD, tr("An unknown Yggdrasil error occurred."));
|
|
}
|
|
}
|