Authlib injector, local auth server and ely by accounts support (#31)

* Add injector

* Add uuid generation for profile

* Add auth server emulator

* Start auth server on random port and bypass it to injector

* Run injector only when account type is dummy

* Clean authlib injector

* Add ely by authentication

* Remove old comments

* Add response status text to auth server

* Fix json value access

as done by @maximmasterr
This commit is contained in:
Max
2021-06-10 17:00:54 +03:00
committed by GitHub
parent 4db5965f89
commit 6df1be94dd
21 changed files with 424 additions and 33 deletions

79
api/logic/AuthServer.cpp Normal file
View File

@@ -0,0 +1,79 @@
#include "AuthServer.h"
#include <QDebug>
#include <QTcpSocket>
AuthServer::AuthServer(QObject *parent) : QObject(parent)
{
m_tcpServer.reset(new QTcpServer(this));
connect(m_tcpServer.get(), &QTcpServer::newConnection, this, &AuthServer::newConnection);
if (!m_tcpServer->listen(QHostAddress::LocalHost))
{
// TODO: think about stop launching when server start fails
qCritical() << "Auth server start failed";
}
}
quint16 AuthServer::port()
{
return m_tcpServer->serverPort();
}
void AuthServer::newConnection()
{
QTcpSocket *tcpSocket = m_tcpServer->nextPendingConnection();
connect(tcpSocket, &QTcpSocket::readyRead, this, [tcpSocket]()
{
// Not the best way to process queries, but it just works
QString rawRequest = tcpSocket->readAll().data();
QStringList requestLines = rawRequest.split("\r\n");
QString requestPath = requestLines[0].split(" ")[1];
int responseStatusCode = 500;
QString responseBody = "";
QStringList responseHeaders;
responseHeaders << "Connection: keep-alive";
if (requestPath == "/")
{
responseBody = "{\"Status\":\"OK\",\"Runtime-Mode\":\"productionMode\",\"Application-Author\":\"Mojang Web Force\",\"Application-Description\":\"Mojang Public API.\",\"Specification-Version\":\"3.58.0\",\"Application-Name\":\"yggdrasil.accounts.restlet.server.public\",\"Implementation-Version\":\"3.58.0_build194\",\"Application-Owner\":\"Mojang\"}";
responseStatusCode = 200;
responseHeaders << "Content-Type: application/json; charset=utf-8";
}
else if (requestPath == "/sessionserver/session/minecraft/join" || requestPath == "/sessionserver/session/minecraft/hasJoined")
{
responseStatusCode = 204;
}
else
{
responseBody = "Not found";
responseStatusCode = 404;
}
QString responseStatusText = "Internal Server Error";
if (responseStatusCode == 200)
responseStatusText = "OK";
else if (responseStatusCode == 204)
responseStatusText = "No Content";
else if (responseStatusCode == 404)
responseStatusText = "Not Found";
if (responseBody.length() != 0)
{
responseHeaders << ((QString) "Content-Length: %1").arg(responseBody.length());
}
tcpSocket->write(((QString) "HTTP/1.1 %1 %2\r\nConnection: keep-alive\r\n").arg(responseStatusCode).arg(responseStatusText).toUtf8());
tcpSocket->write(responseHeaders.join("\r\n").toUtf8());
tcpSocket->write("\r\n\r\n");
tcpSocket->write(responseBody.toUtf8());
});
connect(tcpSocket, &QTcpSocket::disconnected, this, [tcpSocket]()
{
tcpSocket->close();
});
}

20
api/logic/AuthServer.h Normal file
View File

@@ -0,0 +1,20 @@
#pragma once
#include <QString>
#include <QTcpServer>
#include "settings/SettingsObject.h"
#include "multimc_logic_export.h"
class MULTIMC_LOGIC_EXPORT AuthServer: public QObject
{
public:
explicit AuthServer(QObject *parent = 0);
quint16 port();
private:
void newConnection();
private:
std::shared_ptr<QTcpServer> m_tcpServer;
};

View File

@@ -148,7 +148,7 @@ public:
/// returns a valid launcher (task container)
virtual shared_qobject_ptr<LaunchTask> createLaunchTask(
AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) = 0;
AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin, quint16 localAuthServerPort) = 0;
/// returns the current launch task (if any)
shared_qobject_ptr<LaunchTask> getLaunchTask();

View File

@@ -4,6 +4,8 @@ include (UnitTest)
set(CORE_SOURCES
# LOGIC - Base classes and infrastructure
AuthServer.h
AuthServer.cpp
BaseInstaller.h
BaseInstaller.cpp
BaseVersionList.h
@@ -246,6 +248,8 @@ set(MINECRAFT_SOURCES
minecraft/launch/ReconstructAssets.h
minecraft/launch/ScanModFolders.cpp
minecraft/launch/ScanModFolders.h
minecraft/launch/InjectAuthlib.cpp
minecraft/launch/InjectAuthlib.h
minecraft/legacy/LegacyModList.h
minecraft/legacy/LegacyModList.cpp

View File

@@ -92,6 +92,7 @@ void Env::initHttpMetaCache()
m_metacache->addBase("asset_objects", QDir("assets/objects").absolutePath());
m_metacache->addBase("versions", QDir("versions").absolutePath());
m_metacache->addBase("libraries", QDir("libraries").absolutePath());
m_metacache->addBase("injectors", QDir("injectors").absolutePath());
m_metacache->addBase("minecraftforge", QDir("mods/minecraftforge").absolutePath());
m_metacache->addBase("fmllibs", QDir("mods/minecraftforge/libs").absolutePath());
m_metacache->addBase("liteloader", QDir("mods/liteloader").absolutePath());

View File

@@ -27,7 +27,7 @@ public:
{
return instanceRoot();
};
shared_qobject_ptr<LaunchTask> createLaunchTask(AuthSessionPtr, MinecraftServerTargetPtr) override
shared_qobject_ptr<LaunchTask> createLaunchTask(AuthSessionPtr, MinecraftServerTargetPtr, quint16) override
{
return nullptr;
}

View File

@@ -23,6 +23,7 @@
#include "minecraft/launch/ClaimAccount.h"
#include "minecraft/launch/ReconstructAssets.h"
#include "minecraft/launch/ScanModFolders.h"
#include "minecraft/launch/InjectAuthlib.h"
#include "java/launch/CheckJava.h"
#include "java/JavaUtils.h"
#include "meta/Index.h"
@@ -50,7 +51,7 @@ class OrSetting : public Setting
Q_OBJECT
public:
OrSetting(QString id, std::shared_ptr<Setting> a, std::shared_ptr<Setting> b)
:Setting({id}, false), m_a(a), m_b(b)
:Setting({id}, false), m_a(a), m_b(b)
{
}
virtual QVariant get() const
@@ -297,6 +298,8 @@ QStringList MinecraftInstance::javaArguments() const
{
QStringList args;
args.append(m_injector->javaArg);
// custom args go first. we want to override them if we have our own here.
args.append(extraArguments());
@@ -401,7 +404,7 @@ static QString replaceTokensIn(QString text, QMap<QString, QString> with)
}
QStringList MinecraftInstance::processMinecraftArgs(
AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) const
AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) const
{
auto profile = m_components->getProfile();
QString args_pattern = profile->getMinecraftArguments();
@@ -481,9 +484,9 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftS
// generic minecraft params
for (auto param : processMinecraftArgs(
session,
nullptr /* When using a launch script, the server parameters are handled by it*/
))
session,
nullptr /* When using a launch script, the server parameters are handled by it*/
))
{
launchScript += "param " + param + "\n";
}
@@ -601,10 +604,10 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr
out << QString("%1:").arg(label);
auto modList = model.allMods();
std::sort(modList.begin(), modList.end(), [](Mod &a, Mod &b) {
auto aName = a.filename().completeBaseName();
auto bName = b.filename().completeBaseName();
return aName.localeAwareCompare(bName) < 0;
});
auto aName = a.filename().completeBaseName();
auto bName = b.filename().completeBaseName();
return aName.localeAwareCompare(bName) < 0;
});
for(auto & mod: modList)
{
if(mod.type() == Mod::MOD_FOLDER)
@@ -741,12 +744,12 @@ MessageLevel::Enum MinecraftInstance::guessLevel(const QString &line, MessageLev
return MessageLevel::Fatal;
//NOTE: this diverges from the real regexp. no unicode, the first section is + instead of *
static const QString javaSymbol = "([a-zA-Z_$][a-zA-Z\\d_$]*\\.)+[a-zA-Z_$][a-zA-Z\\d_$]*";
if (line.contains("Exception in thread")
if (line.contains("Exception in thread")
|| line.contains(QRegularExpression("\\s+at " + javaSymbol))
|| line.contains(QRegularExpression("Caused by: " + javaSymbol))
|| line.contains(QRegularExpression("([a-zA-Z_$][a-zA-Z\\d_$]*\\.)+[a-zA-Z_$]?[a-zA-Z\\d_$]*(Exception|Error|Throwable)"))
|| line.contains(QRegularExpression("... \\d+ more$"))
)
)
return MessageLevel::Error;
return level;
}
@@ -810,19 +813,19 @@ shared_qobject_ptr<Task> MinecraftInstance::createUpdateTask(Net::Mode mode)
{
switch (mode)
{
case Net::Mode::Offline:
{
return shared_qobject_ptr<Task>(new MinecraftLoadAndCheck(this));
}
case Net::Mode::Online:
{
return shared_qobject_ptr<Task>(new MinecraftUpdate(this));
}
case Net::Mode::Offline:
{
return shared_qobject_ptr<Task>(new MinecraftLoadAndCheck(this));
}
case Net::Mode::Online:
{
return shared_qobject_ptr<Task>(new MinecraftUpdate(this));
}
}
return nullptr;
}
shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin)
shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin, quint16 localAuthServerPort)
{
// FIXME: get rid of shared_from_this ...
auto process = LaunchTask::create(std::dynamic_pointer_cast<MinecraftInstance>(shared_from_this()));
@@ -914,6 +917,17 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
process->appendStep(new ReconstructAssets(pptr));
}
// authlib patch
if (session->m_accountPtr->loginType() != "mojang")
{
auto step = new InjectAuthlib(pptr, &m_injector);
if(session->m_accountPtr->loginType() == "dummy")
step->setAuthServer(((QString)"http://localhost:%1").arg(localAuthServerPort));
else if(session->m_accountPtr->loginType() == "elyby")
step->setAuthServer(((QString) "ely.by").arg(localAuthServerPort));
process->appendStep(step);
}
{
// actually launch the game
auto method = launchMethod();

View File

@@ -6,6 +6,7 @@
#include <QDir>
#include "multimc_logic_export.h"
#include "minecraft/launch/MinecraftServerTarget.h"
#include "minecraft/launch/InjectAuthlib.h"
class ModFolderModel;
class WorldList;
@@ -77,7 +78,7 @@ public:
////// Launch stuff //////
shared_qobject_ptr<Task> createUpdateTask(Net::Mode mode) override;
shared_qobject_ptr<LaunchTask> createLaunchTask(AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) override;
shared_qobject_ptr<LaunchTask> createLaunchTask(AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin, quint16 localAuthServerPort) override;
QStringList extraArguments() const override;
QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) override;
QList<Mod> getJarMods() const;
@@ -128,6 +129,7 @@ protected: // data
mutable std::shared_ptr<ModFolderModel> m_texture_pack_list;
mutable std::shared_ptr<WorldList> m_world_list;
mutable std::shared_ptr<GameOptions> m_game_options;
mutable std::shared_ptr<AuthlibInjector> m_injector;
};
typedef std::shared_ptr<MinecraftInstance> MinecraftInstancePtr;

View File

@@ -28,6 +28,8 @@
#include <QDebug>
#include <BuildConfig.h>
MojangAccountPtr MojangAccount::loadFromJson(const QJsonObject &object)
{
// The JSON object must at least have a username for it to be valid.
@@ -148,7 +150,7 @@ QJsonObject MojangAccount::saveToJson() const
bool MojangAccount::setLoginType(const QString &loginType)
{
// TODO: Implement a cleaner validity check
if (loginType == "mojang" or loginType == "dummy")
if (loginType == "mojang" || loginType == "dummy" || loginType == "elyby")
{
m_loginType = loginType;
return true;
@@ -184,6 +186,14 @@ AccountStatus MojangAccount::accountStatus() const
return Verified;
}
QString MojangAccount::authEndpoint() const
{
if(m_loginType == "elyby")
return BuildConfig.AUTH_BASE_ELYBY;
return BuildConfig.AUTH_BASE_MOJANG;
}
std::shared_ptr<YggdrasilTask> MojangAccount::login(AuthSessionPtr session, QString password)
{
Q_ASSERT(m_currentTask.get() == nullptr);
@@ -202,7 +212,7 @@ std::shared_ptr<YggdrasilTask> MojangAccount::login(AuthSessionPtr session, QStr
// TODO: Proper profile support (idk how)
auto dummyProfile = AccountProfile();
dummyProfile.name = m_username;
dummyProfile.id = "-";
dummyProfile.id = QUuid::createUuid().toString().remove(QRegExp("[{}]"));
m_profiles.append(dummyProfile);
m_currentProfile = 0;
}

View File

@@ -141,6 +141,8 @@ public: /* queries */
//! Returns whether the account is NotVerified, Verified or Online
AccountStatus accountStatus() const;
QString authEndpoint() const;
signals:
/**
* This signal is emitted when the account changes
@@ -151,7 +153,7 @@ signals:
protected: /* variables */
// Authentication system used.
// Usable values: "mojang", "dummy"
// Usable values: "mojang", "dummy", "elyby"
QString m_loginType;
// Username taken by account.

View File

@@ -25,8 +25,6 @@
#include <Env.h>
#include <BuildConfig.h>
#include <QDebug>
YggdrasilTask::YggdrasilTask(MojangAccount *account, QObject *parent)
@@ -42,8 +40,9 @@ void YggdrasilTask::executeTask()
// Get the content of the request we're going to send to the server.
QJsonDocument doc(getRequestContent());
QUrl reqUrl(BuildConfig.AUTH_BASE + getEndpoint());
QNetworkRequest netRequest(reqUrl);
QUrl reqUrl(m_account->authEndpoint() + getEndpoint());
qDebug() << m_account->authEndpoint() + getEndpoint();
QNetworkRequest netRequest(reqUrl);
netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QByteArray requestData = doc.toJson();

View File

@@ -0,0 +1,161 @@
/* 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 <launch/LaunchTask.h>
#include <minecraft/MinecraftInstance.h>
#include <FileSystem.h>
#include <Env.h>
#include <Json.h>
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");
MetaEntryPtr entry = ENV.metacache()->resolveEntry("injectors", "version.json");
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();
}
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;
auto netJob = new NetJob("Injector download");
MetaEntryPtr entry = ENV.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();
}
void InjectAuthlib::onDownloadSucceeded()
{
QString injector = QString("-javaagent:%1=%2").arg(QDir("injectors").absoluteFilePath(m_versionName)).arg(m_authServer);
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;
}

View File

@@ -0,0 +1,70 @@
/* 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 <launch/LaunchStep.h>
#include <QObjectPtr.h>
#include <LoggedProcess.h>
#include <java/JavaChecker.h>
#include <net/Mode.h>
#include <net/NetJob.h>
struct AuthlibInjector
{
QString javaArg;
AuthlibInjector(const QString arg)
{
javaArg = std::move(arg);
qDebug() << "NEW INJECTOR" << javaArg;
}
};
typedef std::shared_ptr<AuthlibInjector> 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;
};
public slots:
bool abort() override;
private slots:
void onVersionDownloadSucceeded();
void onDownloadSucceeded();
void onDownloadFailed(QString reason);
private:
shared_qobject_ptr<Task> jobPtr;
bool m_aborted = false;
QString m_versionName;
QString m_authServer;
AuthlibInjectorPtr *m_injector;
};

View File

@@ -112,7 +112,7 @@ public:
return false;
}
shared_qobject_ptr<LaunchTask> createLaunchTask(
AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) override
AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin, quint16 localAuthServerPort) override
{
return nullptr;
}