diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 8ee653c2f..758da3896 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -271,6 +271,8 @@ set(MINECRAFT_SOURCES minecraft/auth/custom/steps/CustomAuthStep.cpp minecraft/auth/custom/steps/CustomAuthStep.h + minecraft/auth/custom/steps/CustomRefreshStep.cpp + minecraft/auth/custom/steps/CustomRefreshStep.h minecraft/auth/custom/steps/CustomGetSkinStep.cpp minecraft/auth/custom/steps/CustomGetSkinStep.h diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h index 30ed478b2..2e411ac2c 100644 --- a/launcher/minecraft/auth/AccountData.h +++ b/launcher/minecraft/auth/AccountData.h @@ -130,4 +130,5 @@ struct AccountData { QString errorString; AccountState accountState = AccountState::Unchecked; QString accountLogin; + bool profileSelectedExplicitly = false; }; diff --git a/launcher/minecraft/auth/AuthFlow.cpp b/launcher/minecraft/auth/AuthFlow.cpp index a7f32b6c9..964d53a5d 100644 --- a/launcher/minecraft/auth/AuthFlow.cpp +++ b/launcher/minecraft/auth/AuthFlow.cpp @@ -1,9 +1,12 @@ +#include "AuthFlow.h" + #include #include #include #include #include "minecraft/auth/AccountData.h" +#include "tasks/Task.h" // MSA #include "minecraft/auth/msa/steps/EntitlementsStep.h" @@ -22,16 +25,9 @@ #include "elyby/steps/MinecraftProfileStepEly.h" // Custom -#include "AuthFlow.h" - #include "custom/steps/CustomAuthStep.h" #include "custom/steps/CustomGetSkinStep.h" - -#include "tasks/Task.h" - -#include "AuthFlow.h" - -#include +#include "custom/steps/CustomRefreshStep.h" AuthFlow::AuthFlow(AccountData* data, Action action, QString password) : Task(), m_data(data) { @@ -73,10 +69,10 @@ AuthFlow::AuthFlow(AccountData* data, Action action, QString password) : Task(), } break; case AccountType::Custom: { if (action == Action::Login) { - m_steps.append(makeShared(m_data, Action::Login, std::move(password))); - m_steps.append(makeShared(m_data, Action::Refresh, QString())); + m_steps.append(makeShared(m_data, std::move(password))); + m_steps.append(makeShared(m_data)); } else { - m_steps.append(makeShared(m_data, action, std::move(password))); + m_steps.append(makeShared(m_data)); } m_steps.append(makeShared(m_data)); } break; diff --git a/launcher/minecraft/auth/custom/steps/CustomAuthStep.cpp b/launcher/minecraft/auth/custom/steps/CustomAuthStep.cpp index 435b0dcf4..539d16454 100644 --- a/launcher/minecraft/auth/custom/steps/CustomAuthStep.cpp +++ b/launcher/minecraft/auth/custom/steps/CustomAuthStep.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Freesm Launcher - Minecraft Launcher - * Copyright (C) 2025 so5iso4ka + * Copyright (C) 2026 so5iso4ka * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,109 +18,105 @@ #include "CustomAuthStep.h" +#include #include +#include +#include #include "Application.h" #include "Logging.h" #include "net/NetUtils.h" #include "net/RawHeaderProxy.h" -#include +CustomAuthStep::CustomAuthStep(AccountData* data, QString password) : AuthStep(data), m_password(std::move(password)) {} -CustomAuthStep::CustomAuthStep(AccountData* data, AuthFlow::Action action, QString password) - : AuthStep(data), m_password(std::move(password)), m_action(action) -{} +CustomAuthStep::~CustomAuthStep() = default; void CustomAuthStep::perform() { - const QUrl url(authUrl() + requestUrl()); - const QString requestData = fillRequest(); + if (m_data == nullptr) { + emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Account data is a null pointer")); + return; + } + + const QUrl url(m_data->authUrl + m_data->loginUrl); + const QJsonDocument request(fillRequest()); - m_response.reset(new QByteArray()); - m_request = Net::Upload::makeByteArray(url, m_response, requestData.toUtf8()); + m_response = std::make_shared(); + m_request = Net::Upload::makeByteArray(url, m_response, request.toJson()); - const auto headerProxy = - new Net::RawHeaderProxy(QList{ { "Content-Type", "application/json" }, { "Accept", "application/json" } }); - m_request->addHeaderProxy(headerProxy); // RawHeaderProxy::addHeaderProxy takes ownership of the proxy, so no cleanup is required + m_request->addHeaderProxy(new Net::RawHeaderProxy( + QList{ { "Content-Type", "application/json; charset=utf-8" }, { "Accept", "application/json" } })); - m_task.reset(new NetJob(authType() + "AuthStep", APPLICATION->network())); + m_task.reset(new NetJob("CustomAuthStep", APPLICATION->network())); m_task->setAskRetry(false); m_task->addNetAction(m_request); connect(m_task.get(), &Task::finished, this, &CustomAuthStep::onRequestDone); m_task->start(); - qDebug() << "Getting authorization token for " + authType() + " account"; + qDebug() << "Getting authorization token for custom account"; } -QString CustomAuthStep::requestUrl() +QJsonObject CustomAuthStep::fillRequest() const { - return m_action == AuthFlow::Action::Login ? m_data->loginUrl : m_data->refreshUrl; -} + QJsonObject root; + root.insert("username", m_data->accountLogin); + root.insert("password", m_password); -QString CustomAuthStep::requestTemplate() -{ - if (m_action == AuthFlow::Action::Login) { - return R"XXX( -{ - "username": "%1", - "password": "%2", - "clientToken": "%3", - "requestUser": false, - "agent": { - "name":"Minecraft", - "version":1 - } -} -)XXX"; - } else { - return R"XXX( -{ - "accessToken": "%1", - "clientToken": "%2", - "requestUser": false, - "selectedProfile": { - "id": "%3", - "name": "%4" - } -} -)XXX"; - } -} + QJsonObject agent; + agent.insert("name", "Minecraft"); + agent.insert("version", 1); -QString CustomAuthStep::fillRequest() -{ - if (m_action == AuthFlow::Action::Login) { - return requestTemplate().arg(m_data->accountLogin, m_password, clientID()); - } else { - return requestTemplate().arg(m_data->yggdrasilToken.token, m_data->clientID, m_data->minecraftProfile.id, - m_data->minecraftProfile.name); - } + root.insert("agent", agent); + + return root; } -bool CustomAuthStep::parseResponse() +void CustomAuthStep::onRequestDone() { qCDebug(authCredentials()) << *m_response; - if (m_request->error() != QNetworkReply::NoError) { - qWarning() << "Reply error:" << m_request->error(); - return false; + + if (m_request->error() != QNetworkReply::NoError && m_request->error() != QNetworkReply::ContentAccessDenied) { + emit finished(AccountTaskState::STATE_OFFLINE, m_request->errorString()); + return; } - auto jsonResponse = QJsonDocument::fromJson(*m_response); + QJsonParseError err; + auto jsonResponse = QJsonDocument::fromJson(*m_response, &err); + + if (err.error != QJsonParseError::NoError) { + emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Error while parsing JSON response: %1").arg(err.errorString())); + return; + } + + if (m_request->error() == QNetworkReply::ContentAccessDenied) { + const QString msg = jsonResponse["errorMessage"].toString() == "Invalid credentials. Invalid username or password." + ? tr("Invalid credentials. Invalid username or password.") + : m_request->errorString(); + + emit finished(AccountTaskState::STATE_FAILED_HARD, msg); + return; + } m_data->yggdrasilToken.token = jsonResponse["accessToken"].toString(); + m_data->yggdrasilToken.validity = Validity::Certain; + m_data->yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc(); m_data->clientID = jsonResponse["clientToken"].toString(); - if (!jsonResponse["selectedProfile"].isNull()) { - auto profile = jsonResponse["selectedProfile"].toObject(); - m_data->minecraftProfile.id = profile["id"].toString(); - m_data->minecraftProfile.name = profile["name"].toString(); + QJsonObject selectedProfile = jsonResponse["selectedProfile"].toObject(); + if (!selectedProfile.isEmpty()) { + m_data->minecraftProfile.id = selectedProfile["id"].toString(); + m_data->minecraftProfile.name = selectedProfile["name"].toString(); + + emit finished(AccountTaskState::STATE_WORKING, tr("Got authorization for custom account")); + return; } const QJsonArray profiles = jsonResponse["availableProfiles"].toArray(); - if (profiles.size() > 1 && m_data->minecraftProfile.id.isEmpty()) { + if (profiles.size() > 1) { const auto profileName = [](const auto& profile) { auto obj = profile.toObject(); return obj["name"].toString(); @@ -134,7 +130,8 @@ bool CustomAuthStep::parseResponse() QInputDialog::getItem(nullptr, tr("Select profile"), tr("Select profile for this account"), list, 0, false, &ok); if (!ok) { - return false; + emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Profile selection cancelled")); + return; } const auto it = std::ranges::find(profiles, selectedProfileName, profileName); @@ -142,24 +139,16 @@ bool CustomAuthStep::parseResponse() auto profileObj = it->toObject(); m_data->minecraftProfile = MinecraftProfile{ .id = profileObj["id"].toString(), .name = profileObj["name"].toString() }; } else { - return false; + // assuming that this will never happen + emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Something went wrong")); + return; } - } - - if (profiles.size() == 1 && m_data->minecraftProfile.id.isEmpty()) { + } else if (profiles.size() == 1) { auto profileObj = profiles.first().toObject(); m_data->minecraftProfile = MinecraftProfile{ .id = profileObj["id"].toString(), .name = profileObj["name"].toString() }; } - return true; -} + m_data->profileSelectedExplicitly = true; -void CustomAuthStep::onRequestDone() -{ - if (!parseResponse()) { - emit finished(AccountTaskState::STATE_OFFLINE, - tr("Failed to get authorization for %1 account: %2").arg(authType(), m_request->errorString())); - return; - } - emit finished(AccountTaskState::STATE_WORKING, tr("Got authorization for %1 account").arg(authType())); + emit finished(AccountTaskState::STATE_WORKING, tr("Got authorization for custom account")); } diff --git a/launcher/minecraft/auth/custom/steps/CustomAuthStep.h b/launcher/minecraft/auth/custom/steps/CustomAuthStep.h index c40811670..be35b2fe3 100644 --- a/launcher/minecraft/auth/custom/steps/CustomAuthStep.h +++ b/launcher/minecraft/auth/custom/steps/CustomAuthStep.h @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Freesm Launcher - Minecraft Launcher - * Copyright (C) 2025 so5iso4ka + * Copyright (C) 2026 so5iso4ka * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,46 +18,38 @@ #pragma once -#include "BuildConfig.h" -#include "minecraft/auth/AuthFlow.h" +#include +#include +#include +#include + #include "minecraft/auth/AuthStep.h" #include "net/NetJob.h" #include "net/Upload.h" +struct AccountData; + class CustomAuthStep : public AuthStep { Q_OBJECT public: - CustomAuthStep(AccountData* data, AuthFlow::Action action, QString password); - virtual ~CustomAuthStep() noexcept = default; + CustomAuthStep(AccountData* data, QString password); + ~CustomAuthStep() override; void perform() override; QString describe() override { return tr("Custom account authentication"); } - protected: - virtual QString authType() { return "Custom"; } - - virtual QString authUrl() { return m_data->authUrl; } - - virtual QString clientID() { return BuildConfig.LAUNCHER_NAME; } - - virtual QString requestUrl(); - - QString requestTemplate(); - - QString fillRequest(); - - bool parseResponse(); + private: + QJsonObject fillRequest() const; - protected slots: + private slots: virtual void onRequestDone(); - protected: + private: std::shared_ptr m_response; Net::Upload::Ptr m_request; NetJob::Ptr m_task; - const QString m_password; - const AuthFlow::Action m_action; + QString m_password; }; diff --git a/launcher/minecraft/auth/custom/steps/CustomRefreshStep.cpp b/launcher/minecraft/auth/custom/steps/CustomRefreshStep.cpp new file mode 100644 index 000000000..066f5ab4e --- /dev/null +++ b/launcher/minecraft/auth/custom/steps/CustomRefreshStep.cpp @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Freesm Launcher - Minecraft Launcher + * Copyright (C) 2026 so5iso4ka + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "CustomRefreshStep.h" + +#include + +#include "Application.h" +#include "Logging.h" +#include "net/RawHeaderProxy.h" + +CustomRefreshStep::CustomRefreshStep(AccountData* data) : AuthStep(data) {} + +CustomRefreshStep::~CustomRefreshStep() = default; + +void CustomRefreshStep::perform() +{ + if (m_data == nullptr) { + emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Account data is a null pointer")); + return; + } + + const QUrl url(m_data->authUrl + m_data->refreshUrl); + const QJsonDocument request(fillRequest()); + + m_response = std::make_shared(); + m_request = Net::Upload::makeByteArray(url, m_response, request.toJson()); + + // RawHeaderProxy::addHeaderProxy takes ownership of the proxy, so no cleanup is required + m_request->addHeaderProxy(new Net::RawHeaderProxy( + QList{ { "Content-Type", "application/json; charset=utf-8" }, { "Accept", "application/json" } })); + + m_task.reset(new NetJob("CustomRefreshStep", APPLICATION->network())); + m_task->setAskRetry(false); + m_task->addNetAction(m_request); + + connect(m_task.get(), &Task::finished, this, &CustomRefreshStep::onRequestDone); + + m_task->start(); + qDebug() << "Getting authorization token for custom account"; +} + +QString CustomRefreshStep::describe() +{ + return tr("Refreshing custom account"); +} + +QJsonObject CustomRefreshStep::fillRequest() const +{ + QJsonObject root; + root.insert("accessToken", m_data->yggdrasilToken.token); + root.insert("clientToken", m_data->clientID); + + if (m_data->profileSelectedExplicitly) { + QJsonObject selectedProfile; + selectedProfile.insert("id", m_data->minecraftProfile.id); + selectedProfile.insert("name", m_data->minecraftProfile.name); + + root.insert("selectedProfile", selectedProfile); + } + + return root; +} + +void CustomRefreshStep::onRequestDone() +{ + qCDebug(authCredentials()) << *m_response; + + if (m_request->error() != QNetworkReply::NoError && m_request->error() != QNetworkReply::ContentAccessDenied) { + emit finished(AccountTaskState::STATE_OFFLINE, m_request->errorString()); + return; + } + + QJsonParseError err; + auto jsonResponse = QJsonDocument::fromJson(*m_response, &err); + + if (err.error != QJsonParseError::NoError) { + emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Error while parsing JSON response: %1").arg(err.errorString())); + return; + } + + if (m_request->error() == QNetworkReply::ContentAccessDenied) { + const QString msg = jsonResponse["errorMessage"].toString() == "Invalid credentials. Invalid username or password." + ? tr("Invalid credentials. Invalid username or password.") + : m_request->errorString(); + + emit finished(AccountTaskState::STATE_FAILED_HARD, msg); + return; + } + + m_data->yggdrasilToken.token = jsonResponse["accessToken"].toString(); + m_data->yggdrasilToken.validity = Validity::Certain; + m_data->yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc(); + + m_data->clientID = jsonResponse["clientToken"].toString(); + + QJsonObject selectedProfile = jsonResponse["selectedProfile"].toObject(); + if (selectedProfile.isEmpty()) { + emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("No profile selected")); + return; + } + + m_data->minecraftProfile.id = selectedProfile["id"].toString(); + m_data->minecraftProfile.name = selectedProfile["name"].toString(); + + m_data->profileSelectedExplicitly = false; + + emit finished(AccountTaskState::STATE_WORKING, tr("Refreshed custom account")); +} diff --git a/launcher/minecraft/auth/custom/steps/CustomRefreshStep.h b/launcher/minecraft/auth/custom/steps/CustomRefreshStep.h new file mode 100644 index 000000000..ca9f57b87 --- /dev/null +++ b/launcher/minecraft/auth/custom/steps/CustomRefreshStep.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Freesm Launcher - Minecraft Launcher + * Copyright (C) 2026 so5iso4ka + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include +#include + +#include "minecraft/auth/AuthStep.h" +#include "net/NetJob.h" +#include "net/Upload.h" + +class CustomRefreshStep : public AuthStep { + Q_OBJECT + + public: + explicit CustomRefreshStep(AccountData* data); + ~CustomRefreshStep() override; + + void perform() override; + + QString describe() override; + + private: + QJsonObject fillRequest() const; + + private slots: + virtual void onRequestDone(); + + private: + std::shared_ptr m_response; + Net::Upload::Ptr m_request; + NetJob::Ptr m_task; +}; \ No newline at end of file