From 0170e8b1770cacecaca2be249f35243893d6af92 Mon Sep 17 00:00:00 2001 From: fn2006 <92369097+fn2006@users.noreply.github.com> Date: Sun, 7 Aug 2022 18:38:53 +0100 Subject: [PATCH] Add Ely.by accounts (#17) * Initial Ely.by support * Fix profile pictures for Ely.by * Disable upload and delete skin buttons for Ely.by accounts * Port UltimMC's authlib injector to PollyMC --- launcher/Application.cpp | 1 + launcher/CMakeLists.txt | 15 +- launcher/minecraft/MinecraftInstance.cpp | 15 ++ launcher/minecraft/MinecraftInstance.h | 2 + launcher/minecraft/auth/AccountData.cpp | 14 +- launcher/minecraft/auth/AccountData.h | 3 +- launcher/minecraft/auth/AccountList.cpp | 2 +- launcher/minecraft/auth/MinecraftAccount.cpp | 26 +++ launcher/minecraft/auth/MinecraftAccount.h | 15 ++ launcher/minecraft/auth/Yggdrasil.cpp | 8 +- launcher/minecraft/auth/Yggdrasil.h | 4 +- launcher/minecraft/auth/flows/Elyby.cpp | 24 +++ launcher/minecraft/auth/flows/Elyby.h | 26 +++ .../minecraft/auth/steps/ElybyProfileStep.cpp | 93 ++++++++++ .../minecraft/auth/steps/ElybyProfileStep.h | 22 +++ launcher/minecraft/auth/steps/ElybyStep.cpp | 52 ++++++ launcher/minecraft/auth/steps/ElybyStep.h | 28 +++ .../minecraft/auth/steps/YggdrasilStep.cpp | 4 +- launcher/minecraft/launch/InjectAuthlib.cpp | 173 ++++++++++++++++++ launcher/minecraft/launch/InjectAuthlib.h | 75 ++++++++ launcher/ui/dialogs/ElybyLoginDialog.cpp | 119 ++++++++++++ launcher/ui/dialogs/ElybyLoginDialog.h | 59 ++++++ launcher/ui/dialogs/ElybyLoginDialog.ui | 77 ++++++++ launcher/ui/pages/global/AccountListPage.cpp | 24 ++- launcher/ui/pages/global/AccountListPage.h | 1 + launcher/ui/pages/global/AccountListPage.ui | 6 + 26 files changed, 871 insertions(+), 17 deletions(-) create mode 100644 launcher/minecraft/auth/flows/Elyby.cpp create mode 100644 launcher/minecraft/auth/flows/Elyby.h create mode 100644 launcher/minecraft/auth/steps/ElybyProfileStep.cpp create mode 100644 launcher/minecraft/auth/steps/ElybyProfileStep.h create mode 100644 launcher/minecraft/auth/steps/ElybyStep.cpp create mode 100644 launcher/minecraft/auth/steps/ElybyStep.h create mode 100644 launcher/minecraft/launch/InjectAuthlib.cpp create mode 100644 launcher/minecraft/launch/InjectAuthlib.h create mode 100644 launcher/ui/dialogs/ElybyLoginDialog.cpp create mode 100644 launcher/ui/dialogs/ElybyLoginDialog.h create mode 100644 launcher/ui/dialogs/ElybyLoginDialog.ui diff --git a/launcher/Application.cpp b/launcher/Application.cpp index cb8088be..11ce59ac 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -870,6 +870,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_metacache->addBase("translations", QDir("translations").absolutePath()); m_metacache->addBase("icons", QDir("cache/icons").absolutePath()); m_metacache->addBase("meta", QDir("meta").absolutePath()); + m_metacache->addBase("injectors", QDir("injectors").absolutePath()); m_metacache->Load(); qDebug() << "<> Cache initialized."; } diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 4ce033f9..5a53f9bf 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -212,11 +212,15 @@ set(MINECRAFT_SOURCES minecraft/auth/flows/MSA.h minecraft/auth/flows/Offline.cpp minecraft/auth/flows/Offline.h + minecraft/auth/flows/Elyby.cpp + minecraft/auth/flows/Elyby.h - minecraft/auth/steps/OfflineStep.cpp - minecraft/auth/steps/OfflineStep.h minecraft/auth/steps/EntitlementsStep.cpp minecraft/auth/steps/EntitlementsStep.h + minecraft/auth/steps/ElybyProfileStep.cpp + minecraft/auth/steps/ElybyProfileStep.h + minecraft/auth/steps/ElybyStep.cpp + minecraft/auth/steps/ElybyStep.h minecraft/auth/steps/GetSkinStep.cpp minecraft/auth/steps/GetSkinStep.h minecraft/auth/steps/LauncherLoginStep.cpp @@ -229,6 +233,8 @@ set(MINECRAFT_SOURCES minecraft/auth/steps/MinecraftProfileStepMojang.h minecraft/auth/steps/MSAStep.cpp minecraft/auth/steps/MSAStep.h + minecraft/auth/steps/OfflineStep.cpp + minecraft/auth/steps/OfflineStep.h minecraft/auth/steps/XboxAuthorizationStep.cpp minecraft/auth/steps/XboxAuthorizationStep.h minecraft/auth/steps/XboxProfileStep.cpp @@ -264,6 +270,8 @@ set(MINECRAFT_SOURCES minecraft/launch/LauncherPartLaunch.h minecraft/launch/MinecraftServerTarget.cpp minecraft/launch/MinecraftServerTarget.h + minecraft/launch/InjectAuthlib.cpp + minecraft/launch/InjectAuthlib.h minecraft/launch/PrintInstanceInfo.cpp minecraft/launch/PrintInstanceInfo.h minecraft/launch/ReconstructAssets.cpp @@ -819,6 +827,8 @@ SET(LAUNCHER_SOURCES ui/dialogs/CustomMessageBox.h ui/dialogs/EditAccountDialog.cpp ui/dialogs/EditAccountDialog.h + ui/dialogs/ElybyLoginDialog.cpp + ui/dialogs/ElybyLoginDialog.h ui/dialogs/ExportInstanceDialog.cpp ui/dialogs/ExportInstanceDialog.h ui/dialogs/IconPickerDialog.cpp @@ -955,6 +965,7 @@ qt_wrap_ui(LAUNCHER_UI ui/dialogs/IconPickerDialog.ui ui/dialogs/MSALoginDialog.ui ui/dialogs/OfflineLoginDialog.ui + ui/dialogs/ElybyLoginDialog.ui ui/dialogs/AboutDialog.ui ui/dialogs/LoginDialog.ui ui/dialogs/EditAccountDialog.ui diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 5a6f8de0..a8f8a82a 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -348,6 +348,7 @@ QStringList MinecraftInstance::extraArguments() const if (!addn.isEmpty()) { list.append(addn); } + auto agents = m_components->getProfile()->getAgents(); for (auto agent : agents) { @@ -355,6 +356,13 @@ QStringList MinecraftInstance::extraArguments() const agent->library()->getApplicableFiles(currentSystem, jar, temp1, temp2, temp3, getLocalLibraryPath()); list.append("-javaagent:"+jar[0]+(agent->argument().isEmpty() ? "" : "="+agent->argument())); } + + // TODO: figure out how polymc's javaagent system works and use it instead of this hack + if (m_injector) { + list.append("-javaagent:"+m_injector->javaArg); + list.append("-Dauthlibinjector.noShowServerName"); + } + return list; } @@ -972,7 +980,14 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt if(!session->demo) { process->appendStep(new ClaimAccount(pptr, session)); } + + // authlib patch + if (session->user_type == "elyby") + { + process->appendStep(new InjectAuthlib(pptr, &m_injector)); + } process->appendStep(new Update(pptr, Net::Mode::Online)); + } else { diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h index 8e1c67f2..fb5c6bff 100644 --- a/launcher/minecraft/MinecraftInstance.h +++ b/launcher/minecraft/MinecraftInstance.h @@ -5,6 +5,7 @@ #include #include #include "minecraft/launch/MinecraftServerTarget.h" +#include "minecraft/launch/InjectAuthlib.h" class ModFolderModel; class WorldList; @@ -128,6 +129,7 @@ protected: // data mutable std::shared_ptr m_texture_pack_list; mutable std::shared_ptr m_world_list; mutable std::shared_ptr m_game_options; + mutable std::shared_ptr m_injector; }; typedef std::shared_ptr MinecraftInstancePtr; diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp index 44f7e256..50ca155f 100644 --- a/launcher/minecraft/auth/AccountData.cpp +++ b/launcher/minecraft/auth/AccountData.cpp @@ -352,6 +352,8 @@ bool AccountData::resumeStateFromV3(QJsonObject data) { type = AccountType::Mojang; } else if (typeS == "Offline") { type = AccountType::Offline; + } else if (typeS == "Elyby") { + type = AccountType::Elyby; } else { qWarning() << "Failed to parse account data: type is not recognized."; return false; @@ -409,6 +411,9 @@ QJsonObject AccountData::saveState() const { else if (type == AccountType::Offline) { output["type"] = "Offline"; } + else if (type == AccountType::Elyby) { + output["type"] = "Elyby"; + } tokenToJSONV3(output, yggdrasilToken, "ygg"); profileToJSONV3(output, minecraftProfile, "profile"); @@ -428,14 +433,14 @@ QString AccountData::accessToken() const { } QString AccountData::clientToken() const { - if(type != AccountType::Mojang) { + if(type != AccountType::Mojang && type != AccountType::Elyby) { return QString(); } return yggdrasilToken.extra["clientToken"].toString(); } void AccountData::setClientToken(QString clientToken) { - if(type != AccountType::Mojang) { + if(type != AccountType::Mojang && type != AccountType::Elyby) { return; } yggdrasilToken.extra["clientToken"] = clientToken; @@ -449,7 +454,7 @@ void AccountData::generateClientTokenIfMissing() { } void AccountData::invalidateClientToken() { - if(type != AccountType::Mojang) { + if(type != AccountType::Mojang && type != AccountType::Elyby) { return; } yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{-}]")); @@ -473,6 +478,9 @@ QString AccountData::accountDisplayString() const { case AccountType::Mojang: { return userName(); } + case AccountType::Elyby: { + return userName(); + } case AccountType::Offline: { return QObject::tr(""); } diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h index 092e1691..c11aa146 100644 --- a/launcher/minecraft/auth/AccountData.h +++ b/launcher/minecraft/auth/AccountData.h @@ -74,7 +74,8 @@ struct MinecraftProfile { enum class AccountType { MSA, Mojang, - Offline + Offline, + Elyby }; enum class AccountState { diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp index b3b57c74..4f7f0a12 100644 --- a/launcher/minecraft/auth/AccountList.cpp +++ b/launcher/minecraft/auth/AccountList.cpp @@ -332,7 +332,7 @@ QVariant AccountList::data(const QModelIndex &index, int role) const } case MigrationColumn: { - if(account->isMSA() || account->isOffline()) { + if(!account->isMojang()) { return tr("N/A", "Can Migrate?"); } if (account->canMigrate()) { diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp index a5c6f542..0b11f7d6 100644 --- a/launcher/minecraft/auth/MinecraftAccount.cpp +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -51,6 +51,7 @@ #include "flows/MSA.h" #include "flows/Mojang.h" #include "flows/Offline.h" +#include "flows/Elyby.h" MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) { data.internalId = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); @@ -106,6 +107,17 @@ MinecraftAccountPtr MinecraftAccount::createOffline(const QString &username) return account; } +MinecraftAccountPtr MinecraftAccount::createElyby(const QString &username) +{ + MinecraftAccountPtr account = new MinecraftAccount(); + account->data.type = AccountType::Elyby; + account->data.yggdrasilToken.extra["userName"] = username; + account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); + account->data.minecraftEntitlement.ownsMinecraft = true; + account->data.minecraftEntitlement.canPlayMinecraft = true; + return account; +} + QJsonObject MinecraftAccount::saveToJson() const { @@ -162,6 +174,17 @@ shared_qobject_ptr MinecraftAccount::loginOffline() { return m_currentTask; } +shared_qobject_ptr MinecraftAccount::loginElyby(QString password) { + Q_ASSERT(m_currentTask.get() == nullptr); + + m_currentTask.reset(new ElybyLogin(&data, password)); + connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); + connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); + connect(m_currentTask.get(), &Task::aborted, this, [this]{ authFailed(tr("Aborted")); }); + emit activityChanged(true); + return m_currentTask; +} + shared_qobject_ptr MinecraftAccount::refresh() { if(m_currentTask) { return m_currentTask; @@ -173,6 +196,9 @@ shared_qobject_ptr MinecraftAccount::refresh() { else if(data.type == AccountType::Offline) { m_currentTask.reset(new OfflineRefresh(&data)); } + else if(data.type == AccountType::Elyby) { + m_currentTask.reset(new ElybyRefresh(&data)); + } else { m_currentTask.reset(new MojangRefresh(&data)); } diff --git a/launcher/minecraft/auth/MinecraftAccount.h b/launcher/minecraft/auth/MinecraftAccount.h index 7777f846..923df0f5 100644 --- a/launcher/minecraft/auth/MinecraftAccount.h +++ b/launcher/minecraft/auth/MinecraftAccount.h @@ -95,6 +95,8 @@ public: /* construction */ static MinecraftAccountPtr createOffline(const QString &username); + static MinecraftAccountPtr createElyby(const QString &username); + static MinecraftAccountPtr loadFromJsonV2(const QJsonObject &json); static MinecraftAccountPtr loadFromJsonV3(const QJsonObject &json); @@ -113,6 +115,8 @@ public: /* manipulation */ shared_qobject_ptr loginOffline(); + shared_qobject_ptr loginElyby(QString password); + shared_qobject_ptr refresh(); shared_qobject_ptr currentTask(); @@ -152,10 +156,18 @@ public: /* queries */ return data.type == AccountType::MSA; } + bool isMojang() const { + return data.type == AccountType::Mojang; + } + bool isOffline() const { return data.type == AccountType::Offline; } + bool isElyby() const { + return data.type == AccountType::Elyby; + } + bool ownsMinecraft() const { return data.minecraftEntitlement.ownsMinecraft; } @@ -180,6 +192,9 @@ public: /* queries */ case AccountType::Offline: { return "offline"; } + case AccountType::Elyby: { + return "elyby"; + } break; default: { return "unknown"; diff --git a/launcher/minecraft/auth/Yggdrasil.cpp b/launcher/minecraft/auth/Yggdrasil.cpp index 29978411..acc026be 100644 --- a/launcher/minecraft/auth/Yggdrasil.cpp +++ b/launcher/minecraft/auth/Yggdrasil.cpp @@ -55,7 +55,7 @@ void Yggdrasil::sendRequest(QUrl endpoint, QByteArray content) { void Yggdrasil::executeTask() { } -void Yggdrasil::refresh() { +void Yggdrasil::refresh(QString baseUrl) { start(); /* * { @@ -84,13 +84,13 @@ void Yggdrasil::refresh() { req.insert("requestUser", false); QJsonDocument doc(req); - QUrl reqUrl("https://authserver.mojang.com/refresh"); + QUrl reqUrl(baseUrl + "refresh"); QByteArray requestData = doc.toJson(); sendRequest(reqUrl, requestData); } -void Yggdrasil::login(QString password) { +void Yggdrasil::login(QString password, QString baseUrl) { start(); /* * { @@ -129,7 +129,7 @@ void Yggdrasil::login(QString password) { QJsonDocument doc(req); - QUrl reqUrl("https://authserver.mojang.com/authenticate"); + QUrl reqUrl(baseUrl + "authenticate"); QNetworkRequest netRequest(reqUrl); QByteArray requestData = doc.toJson(); diff --git a/launcher/minecraft/auth/Yggdrasil.h b/launcher/minecraft/auth/Yggdrasil.h index 4f52a04c..34eb18b2 100644 --- a/launcher/minecraft/auth/Yggdrasil.h +++ b/launcher/minecraft/auth/Yggdrasil.h @@ -40,8 +40,8 @@ public: ); virtual ~Yggdrasil() = default; - void refresh(); - void login(QString password); + void refresh(QString baseUrl); + void login(QString password, QString baseUrl); struct Error { diff --git a/launcher/minecraft/auth/flows/Elyby.cpp b/launcher/minecraft/auth/flows/Elyby.cpp new file mode 100644 index 00000000..72c10472 --- /dev/null +++ b/launcher/minecraft/auth/flows/Elyby.cpp @@ -0,0 +1,24 @@ +#include "Elyby.h" + +#include "minecraft/auth/steps/ElybyStep.h" +#include "minecraft/auth/steps/ElybyProfileStep.h" +#include "minecraft/auth/steps/GetSkinStep.h" + +ElybyRefresh::ElybyRefresh( + AccountData *data, + QObject *parent +) : AuthFlow(data, parent) { + m_steps.append(new ElybyStep(m_data, QString())); + m_steps.append(new ElybyProfileStep(m_data)); + m_steps.append(new GetSkinStep(m_data)); +} + +ElybyLogin::ElybyLogin( + AccountData *data, + QString password, + QObject *parent +): AuthFlow(data, parent), m_password(password) { + m_steps.append(new ElybyStep(m_data, m_password)); + m_steps.append(new ElybyProfileStep(m_data)); + m_steps.append(new GetSkinStep(m_data)); +} diff --git a/launcher/minecraft/auth/flows/Elyby.h b/launcher/minecraft/auth/flows/Elyby.h new file mode 100644 index 00000000..beec3e62 --- /dev/null +++ b/launcher/minecraft/auth/flows/Elyby.h @@ -0,0 +1,26 @@ +#pragma once +#include "AuthFlow.h" + +class ElybyRefresh : public AuthFlow +{ + Q_OBJECT +public: + explicit ElybyRefresh( + AccountData *data, + QObject *parent = 0 + ); +}; + +class ElybyLogin : public AuthFlow +{ + Q_OBJECT +public: + explicit ElybyLogin( + AccountData *data, + QString password, + QObject *parent = 0 + ); + +private: + QString m_password; +}; diff --git a/launcher/minecraft/auth/steps/ElybyProfileStep.cpp b/launcher/minecraft/auth/steps/ElybyProfileStep.cpp new file mode 100644 index 00000000..8cd34ffa --- /dev/null +++ b/launcher/minecraft/auth/steps/ElybyProfileStep.cpp @@ -0,0 +1,93 @@ +#include "ElybyProfileStep.h" + +#include + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" +#include "net/NetUtils.h" + +ElybyProfileStep::ElybyProfileStep(AccountData* data) : AuthStep(data) { + +} + +ElybyProfileStep::~ElybyProfileStep() noexcept = default; + +QString ElybyProfileStep::describe() { + return tr("Fetching the Minecraft profile."); +} + + +void ElybyProfileStep::perform() { + if (m_data->minecraftProfile.id.isEmpty()) { + emit finished(AccountTaskState::STATE_FAILED_HARD, tr("A UUID is required to get the profile.")); + return; + } + + QUrl url = QUrl("https://authserver.ely.by/session/profile/" + m_data->minecraftProfile.id); + QNetworkRequest req = QNetworkRequest(url); + AuthRequest *request = new AuthRequest(this); + connect(request, &AuthRequest::finished, this, &ElybyProfileStep::onRequestDone); + request->get(req); +} + +void ElybyProfileStep::rehydrate() { + // NOOP, for now. We only save bools and there's nothing to check. +} + +void ElybyProfileStep::onRequestDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers +) { + auto requestor = qobject_cast(QObject::sender()); + requestor->deleteLater(); + +#ifndef NDEBUG + qDebug() << data; +#endif + if (error == QNetworkReply::ContentNotFoundError) { + // NOTE: Succeed even if we do not have a profile. This is a valid account state. + m_data->minecraftProfile = MinecraftProfile(); + emit finished( + AccountTaskState::STATE_SUCCEEDED, + tr("Account has no Minecraft profile.") + ); + return; + } + if (error != QNetworkReply::NoError) { + qWarning() << "Error getting profile:"; + qWarning() << " HTTP Status: " << requestor->httpStatus_; + qWarning() << " Internal error no.: " << error; + qWarning() << " Error string: " << requestor->errorString_; + + qWarning() << " Response:"; + qWarning() << QString::fromUtf8(data); + + if (Net::isApplicationError(error)) { + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_) + ); + } + else { + emit finished( + AccountTaskState::STATE_OFFLINE, + tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_) + ); + } + return; + } + if(!Parsers::parseMinecraftProfileMojang(data, m_data->minecraftProfile)) { + m_data->minecraftProfile = MinecraftProfile(); + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Minecraft Java profile response could not be parsed") + ); + return; + } + + emit finished( + AccountTaskState::STATE_WORKING, + tr("Minecraft Java profile acquisition succeeded.") + ); +} diff --git a/launcher/minecraft/auth/steps/ElybyProfileStep.h b/launcher/minecraft/auth/steps/ElybyProfileStep.h new file mode 100644 index 00000000..765d79e9 --- /dev/null +++ b/launcher/minecraft/auth/steps/ElybyProfileStep.h @@ -0,0 +1,22 @@ +#pragma once +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class ElybyProfileStep : public AuthStep { + Q_OBJECT + +public: + explicit ElybyProfileStep(AccountData *data); + virtual ~ElybyProfileStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); +}; diff --git a/launcher/minecraft/auth/steps/ElybyStep.cpp b/launcher/minecraft/auth/steps/ElybyStep.cpp new file mode 100644 index 00000000..e81ebb09 --- /dev/null +++ b/launcher/minecraft/auth/steps/ElybyStep.cpp @@ -0,0 +1,52 @@ +#include "ElybyStep.h" + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" +#include "minecraft/auth/Yggdrasil.h" + +ElybyStep::ElybyStep(AccountData* data, QString password) : AuthStep(data), m_password(password) { + m_yggdrasil = new Yggdrasil(m_data, this); + + connect(m_yggdrasil, &Task::failed, this, &ElybyStep::onAuthFailed); + connect(m_yggdrasil, &Task::succeeded, this, &ElybyStep::onAuthSucceeded); + connect(m_yggdrasil, &Task::aborted, this, &ElybyStep::onAuthFailed); +} + +ElybyStep::~ElybyStep() noexcept = default; + +QString ElybyStep::describe() { + return tr("Logging in with Ely.by account."); +} + +void ElybyStep::rehydrate() { + // NOOP, for now. +} + +void ElybyStep::perform() { + if(m_password.size()) { + m_yggdrasil->login(m_password, "https://authserver.ely.by/auth/"); + } + else { + m_yggdrasil->refresh("https://authserver.ely.by/auth/"); + } +} + +void ElybyStep::onAuthSucceeded() { + emit finished(AccountTaskState::STATE_WORKING, tr("Logged in with Ely.by")); +} + +void ElybyStep::onAuthFailed() { + // TODO: hook these in again, expand to MSA + // m_error = m_yggdrasil->m_error; + // m_aborted = m_yggdrasil->m_aborted; + + auto state = m_yggdrasil->taskState(); + QString errorMessage = tr("Ely.by user authentication failed."); + + // NOTE: soft error in the first step means 'offline' + if(state == AccountTaskState::STATE_FAILED_SOFT) { + state = AccountTaskState::STATE_OFFLINE; + errorMessage = tr("Ely.by user authentication ended with a network error."); + } + emit finished(state, errorMessage); +} diff --git a/launcher/minecraft/auth/steps/ElybyStep.h b/launcher/minecraft/auth/steps/ElybyStep.h new file mode 100644 index 00000000..5bf8f52c --- /dev/null +++ b/launcher/minecraft/auth/steps/ElybyStep.h @@ -0,0 +1,28 @@ +#pragma once +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + +class Yggdrasil; + +class ElybyStep : public AuthStep { + Q_OBJECT + +public: + explicit ElybyStep(AccountData *data, QString password); + virtual ~ElybyStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onAuthSucceeded(); + void onAuthFailed(); + +private: + Yggdrasil *m_yggdrasil = nullptr; + QString m_password; +}; diff --git a/launcher/minecraft/auth/steps/YggdrasilStep.cpp b/launcher/minecraft/auth/steps/YggdrasilStep.cpp index e1d33172..d46dce9b 100644 --- a/launcher/minecraft/auth/steps/YggdrasilStep.cpp +++ b/launcher/minecraft/auth/steps/YggdrasilStep.cpp @@ -24,10 +24,10 @@ void YggdrasilStep::rehydrate() { void YggdrasilStep::perform() { if(m_password.size()) { - m_yggdrasil->login(m_password); + m_yggdrasil->login(m_password, "https://authserver.mojang.com/"); } else { - m_yggdrasil->refresh(); + m_yggdrasil->refresh("https://authserver.mojang.com/"); } } diff --git a/launcher/minecraft/launch/InjectAuthlib.cpp b/launcher/minecraft/launch/InjectAuthlib.cpp new file mode 100644 index 00000000..51bc3834 --- /dev/null +++ b/launcher/minecraft/launch/InjectAuthlib.cpp @@ -0,0 +1,173 @@ +/* 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 "InjectAuthlib.h" +#include +#include +#include +#include +#include + +InjectAuthlib::InjectAuthlib(LaunchTask *parent, AuthlibInjectorPtr* injector) : LaunchStep(parent) +{ + m_injector = injector; +} + +void InjectAuthlib::executeTask() +{ + if (m_aborted) + { + emitFailed(tr("Task aborted.")); + return; + } + + auto latestVersionInfo = QString("https://authlib-injector.yushi.moe/artifact/latest.json"); + auto netJob = new NetJob("Injector versions info download", APPLICATION->network()); + MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("injectors", "version.json"); + if (!m_offlineMode) + { + entry->setStale(true); + auto task = Net::Download::makeCached(QUrl(latestVersionInfo), entry); + netJob->addNetAction(task); + + jobPtr.reset(netJob); + QObject::connect(netJob, &NetJob::succeeded, this, &InjectAuthlib::onVersionDownloadSucceeded); + QObject::connect(netJob, &NetJob::failed, this, &InjectAuthlib::onDownloadFailed); + jobPtr->start(); + } + else + { + onVersionDownloadSucceeded(); + } +} + +void InjectAuthlib::onVersionDownloadSucceeded() +{ + + QByteArray data; + try + { + data = FS::read(QDir("injectors").absoluteFilePath("version.json")); + } + catch (const Exception &e) + { + qCritical() << "Translations Download Failed: index file not readable"; + jobPtr.reset(); + emitFailed("Error while parsing JSON response from InjectorEndpoint"); + return; + } + + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(data, &parse_error); + if (parse_error.error != QJsonParseError::NoError) + { + qCritical() << "Error while parsing JSON response from InjectorEndpoint at " << parse_error.offset << " reason: " << parse_error.errorString(); + qCritical() << data; + jobPtr.reset(); + emitFailed("Error while parsing JSON response from InjectorEndpoint"); + return; + } + + if (!doc.isObject()) + { + qCritical() << "Error while parsing JSON response from InjectorEndpoint root is not object"; + qCritical() << data; + jobPtr.reset(); + emitFailed("Error while parsing JSON response from InjectorEndpoint"); + return; + } + + QString downloadUrl; + try + { + downloadUrl = Json::requireString(doc.object(), "download_url"); + } + catch (const JSONValidationError &e) + { + qCritical() << "Error while parsing JSON response from InjectorEndpoint download url is not string"; + qCritical() << e.cause(); + qCritical() << data; + jobPtr.reset(); + emitFailed("Error while parsing JSON response from InjectorEndpoint"); + return; + } + + QFileInfo fi(downloadUrl); + m_versionName = fi.fileName(); + + qDebug() << "Authlib injector version:" << m_versionName; + if (!m_offlineMode) + { + auto netJob = new NetJob("Injector download", APPLICATION->network()); + MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("injectors", m_versionName); + entry->setStale(true); + auto task = Net::Download::makeCached(QUrl(downloadUrl), entry); + netJob->addNetAction(task); + + jobPtr.reset(netJob); + QObject::connect(netJob, &NetJob::succeeded, this, &InjectAuthlib::onDownloadSucceeded); + QObject::connect(netJob, &NetJob::failed, this, &InjectAuthlib::onDownloadFailed); + jobPtr->start(); + } + else + { + onDownloadSucceeded(); + } +} + +void InjectAuthlib::onDownloadSucceeded() +{ + QString injector = QString("%1=%2").arg(QDir("injectors").absoluteFilePath(m_versionName)).arg("ely.by"); + + qDebug() + << "Injecting " << injector; + auto inj = new AuthlibInjector(injector); + m_injector->reset(inj); + + jobPtr.reset(); + emitSucceeded(); +} + +void InjectAuthlib::onDownloadFailed(QString reason) +{ + jobPtr.reset(); + emitFailed(reason); +} + +void InjectAuthlib::proceed() +{ +} + +bool InjectAuthlib::canAbort() const +{ + if (jobPtr) + { + return jobPtr->canAbort(); + } + return true; +} + +bool InjectAuthlib::abort() +{ + m_aborted = true; + if (jobPtr) + { + if (jobPtr->canAbort()) + { + return jobPtr->abort(); + } + } + return true; +} diff --git a/launcher/minecraft/launch/InjectAuthlib.h b/launcher/minecraft/launch/InjectAuthlib.h new file mode 100644 index 00000000..5274f55d --- /dev/null +++ b/launcher/minecraft/launch/InjectAuthlib.h @@ -0,0 +1,75 @@ +/* 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. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +struct AuthlibInjector +{ + QString javaArg; + + AuthlibInjector(const QString arg) + { + javaArg = std::move(arg); + qDebug() << "NEW INJECTOR" << javaArg; + } +}; + +typedef std::shared_ptr AuthlibInjectorPtr; + +// FIXME: stupid. should be defined by the instance type? or even completely abstracted away... +class InjectAuthlib : public LaunchStep +{ + Q_OBJECT +public: + InjectAuthlib(LaunchTask *parent, AuthlibInjectorPtr *injector); + virtual ~InjectAuthlib(){}; + + void executeTask() override; + bool canAbort() const override; + void proceed() override; + + void setAuthServer(QString server) + { + m_authServer = server; + }; + + void setOfflineMode(bool offline) { + m_offlineMode = offline; + } + +public slots: + bool abort() override; + +private slots: + void onVersionDownloadSucceeded(); + void onDownloadSucceeded(); + void onDownloadFailed(QString reason); + +private: + shared_qobject_ptr jobPtr; + bool m_aborted = false; + + bool m_offlineMode; + QString m_versionName; + QString m_authServer; + AuthlibInjectorPtr *m_injector; +}; diff --git a/launcher/ui/dialogs/ElybyLoginDialog.cpp b/launcher/ui/dialogs/ElybyLoginDialog.cpp new file mode 100644 index 00000000..b2a0520d --- /dev/null +++ b/launcher/ui/dialogs/ElybyLoginDialog.cpp @@ -0,0 +1,119 @@ +/* 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 "ElybyLoginDialog.h" +#include "ui_ElybyLoginDialog.h" + +#include "minecraft/auth/AccountTask.h" + +#include + +ElybyLoginDialog::ElybyLoginDialog(QWidget *parent) : QDialog(parent), ui(new Ui::ElybyLoginDialog) +{ + ui->setupUi(this); + ui->progressBar->setVisible(false); + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); +} + +ElybyLoginDialog::~ElybyLoginDialog() +{ + delete ui; +} + +// Stage 1: User interaction +void ElybyLoginDialog::accept() +{ + setUserInputsEnabled(false); + ui->progressBar->setVisible(true); + + // Setup the login task and start it + m_account = MinecraftAccount::createElyby(ui->userTextBox->text()); + m_loginTask = m_account->loginElyby(ui->passTextBox->text()); + connect(m_loginTask.get(), &Task::failed, this, &ElybyLoginDialog::onTaskFailed); + connect(m_loginTask.get(), &Task::succeeded, this, &ElybyLoginDialog::onTaskSucceeded); + connect(m_loginTask.get(), &Task::status, this, &ElybyLoginDialog::onTaskStatus); + connect(m_loginTask.get(), &Task::progress, this, &ElybyLoginDialog::onTaskProgress); + m_loginTask->start(); +} + +void ElybyLoginDialog::setUserInputsEnabled(bool enable) +{ + ui->userTextBox->setEnabled(enable); + ui->passTextBox->setEnabled(enable); + ui->buttonBox->setEnabled(enable); +} + +// Enable the OK button only when both textboxes contain something. +void ElybyLoginDialog::on_userTextBox_textEdited(const QString &newText) +{ + ui->buttonBox->button(QDialogButtonBox::Ok) + ->setEnabled(!newText.isEmpty() && !ui->passTextBox->text().isEmpty()); +} +void ElybyLoginDialog::on_passTextBox_textEdited(const QString &newText) +{ + ui->buttonBox->button(QDialogButtonBox::Ok) + ->setEnabled(!newText.isEmpty() && !ui->userTextBox->text().isEmpty()); +} + +void ElybyLoginDialog::onTaskFailed(const QString &reason) +{ + // Set message + auto lines = reason.split('\n'); + QString processed; + for(auto line: lines) { + if(line.size()) { + processed += "" + line + "
"; + } + else { + processed += "
"; + } + } + ui->label->setText(processed); + + // Re-enable user-interaction + setUserInputsEnabled(true); + ui->progressBar->setVisible(false); +} + +void ElybyLoginDialog::onTaskSucceeded() +{ + QDialog::accept(); +} + +void ElybyLoginDialog::onTaskStatus(const QString &status) +{ + ui->label->setText(status); +} + +void ElybyLoginDialog::onTaskProgress(qint64 current, qint64 total) +{ + ui->progressBar->setMaximum(total); + ui->progressBar->setValue(current); +} + +// Public interface +MinecraftAccountPtr ElybyLoginDialog::newAccount(QWidget *parent, QString msg) +{ + ElybyLoginDialog dlg(parent); + dlg.ui->label->setText(msg); + if (dlg.exec() == QDialog::Accepted) + { + return dlg.m_account; + } + return 0; +} diff --git a/launcher/ui/dialogs/ElybyLoginDialog.h b/launcher/ui/dialogs/ElybyLoginDialog.h new file mode 100644 index 00000000..4b81c0b8 --- /dev/null +++ b/launcher/ui/dialogs/ElybyLoginDialog.h @@ -0,0 +1,59 @@ +/* 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. + */ + +#pragma once + +#include +#include + +#include "minecraft/auth/MinecraftAccount.h" +#include "tasks/Task.h" + +namespace Ui +{ +class ElybyLoginDialog; +} + +class ElybyLoginDialog : public QDialog +{ + Q_OBJECT + +public: + ~ElybyLoginDialog(); + + static MinecraftAccountPtr newAccount(QWidget *parent, QString message); + +private: + explicit ElybyLoginDialog(QWidget *parent = 0); + + void setUserInputsEnabled(bool enable); + +protected +slots: + void accept(); + + void onTaskFailed(const QString &reason); + void onTaskSucceeded(); + void onTaskStatus(const QString &status); + void onTaskProgress(qint64 current, qint64 total); + + void on_userTextBox_textEdited(const QString &newText); + void on_passTextBox_textEdited(const QString &newText); + +private: + Ui::ElybyLoginDialog *ui; + MinecraftAccountPtr m_account; + Task::Ptr m_loginTask; +}; diff --git a/launcher/ui/dialogs/ElybyLoginDialog.ui b/launcher/ui/dialogs/ElybyLoginDialog.ui new file mode 100644 index 00000000..4b03ebf9 --- /dev/null +++ b/launcher/ui/dialogs/ElybyLoginDialog.ui @@ -0,0 +1,77 @@ + + + ElybyLoginDialog + + + + 0 + 0 + 421 + 198 + + + + + 0 + 0 + + + + Add Account + + + + + + Message label placeholder. + + + Qt::RichText + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Email + + + + + + + QLineEdit::Password + + + Password + + + + + + + 24 + + + false + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp index 1e1f70b4..9fb1c22a 100644 --- a/launcher/ui/pages/global/AccountListPage.cpp +++ b/launcher/ui/pages/global/AccountListPage.cpp @@ -48,6 +48,7 @@ #include "ui/dialogs/OfflineLoginDialog.h" #include "ui/dialogs/LoginDialog.h" #include "ui/dialogs/MSALoginDialog.h" +#include "ui/dialogs/ElybyLoginDialog.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/SkinUploadDialog.h" @@ -202,6 +203,22 @@ void AccountListPage::on_actionAddOffline_triggered() } } +void AccountListPage::on_actionAddElyby_triggered() +{ + MinecraftAccountPtr account = ElybyLoginDialog::newAccount( + this, + tr("Please enter your Ely.by account email and password to add your account.") + ); + + if (account) + { + m_accounts->addAccount(account); + if (m_accounts->count() == 1) { + m_accounts->setDefaultAccount(account); + } + } +} + void AccountListPage::on_actionRemove_triggered() { QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); @@ -245,17 +262,20 @@ void AccountListPage::updateButtonStates() bool hasSelection = !selection.empty(); bool accountIsReady = false; bool accountIsOnline = false; + bool accountIsElyby = false; if (hasSelection) { QModelIndex selected = selection.first(); MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value(); accountIsReady = !account->isActive(); accountIsOnline = !account->isOffline(); + accountIsElyby = account->isElyby(); + } ui->actionRemove->setEnabled(accountIsReady); ui->actionSetDefault->setEnabled(accountIsReady); - ui->actionUploadSkin->setEnabled(accountIsReady && accountIsOnline); - ui->actionDeleteSkin->setEnabled(accountIsReady && accountIsOnline); + ui->actionUploadSkin->setEnabled(accountIsReady && accountIsOnline && !accountIsElyby); + ui->actionDeleteSkin->setEnabled(accountIsReady && accountIsOnline && !accountIsElyby); ui->actionRefresh->setEnabled(accountIsReady && accountIsOnline); if(m_accounts->defaultAccount().get() == nullptr) { diff --git a/launcher/ui/pages/global/AccountListPage.h b/launcher/ui/pages/global/AccountListPage.h index 9395e92b..1812c5e5 100644 --- a/launcher/ui/pages/global/AccountListPage.h +++ b/launcher/ui/pages/global/AccountListPage.h @@ -85,6 +85,7 @@ public slots: void on_actionAddMojang_triggered(); void on_actionAddMicrosoft_triggered(); void on_actionAddOffline_triggered(); + void on_actionAddElyby_triggered(); void on_actionRemove_triggered(); void on_actionRefresh_triggered(); void on_actionSetDefault_triggered(); diff --git a/launcher/ui/pages/global/AccountListPage.ui b/launcher/ui/pages/global/AccountListPage.ui index 469955b5..e5d76ec4 100644 --- a/launcher/ui/pages/global/AccountListPage.ui +++ b/launcher/ui/pages/global/AccountListPage.ui @@ -55,6 +55,7 @@ + @@ -109,6 +110,11 @@ Add &Offline + + + Add &Ely.by + + &Refresh