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."));
 | 
						|
    }
 | 
						|
}
 |