Merge pull request #21 from unmojang/evan-goode/custom-authlib-injector

Allow using custom authlib-injector JAR
This commit is contained in:
Evan Goode 2024-08-11 21:39:46 -04:00 committed by GitHub
commit 37fb235492
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 472 additions and 6 deletions

View File

@ -100,3 +100,7 @@ AppImages are available in the [releases section](https://github.com/unmojang/Fj
## Building ## Building
To build the launcher yourself, follow [the instructions on the Prism Launcher website](https://prismlauncher.org/wiki/development/build-instructions) but clone this repo instead. To build the launcher yourself, follow [the instructions on the Prism Launcher website](https://prismlauncher.org/wiki/development/build-instructions) but clone this repo instead.
## Notes
- You can easily use a custom version of authlib-injector on an instance. Select the instance in the main window, click "Edit" (or Ctrl+I/Command+I), go to the Version tab, click "Add Agents", and select your authlib-injector JAR. If your JAR is not correctly identified as authlib-injector, make sure the `Agent-Class` field in the JAR's MANIFEST.MF is `moe.yushi.authlibinjector.Premain`.

View File

@ -95,6 +95,10 @@ set(CORE_SOURCES
MMCTime.cpp MMCTime.cpp
MTPixmapCache.h MTPixmapCache.h
# Manifest.mf parser
Manifest.h
Manifest.cpp
) )
if (UNIX AND NOT CYGWIN AND NOT APPLE) if (UNIX AND NOT CYGWIN AND NOT APPLE)
set(CORE_SOURCES set(CORE_SOURCES

View File

@ -146,9 +146,15 @@ void LaunchController::login()
if (m_accountToUse->usesCustomApiServers()) { if (m_accountToUse->usesCustomApiServers()) {
MinecraftInstancePtr inst = std::dynamic_pointer_cast<MinecraftInstance>(m_instance); MinecraftInstancePtr inst = std::dynamic_pointer_cast<MinecraftInstance>(m_instance);
const auto& authlibInjectorVersion = inst->getPackProfile()->getComponentVersion("moe.yushi.authlibinjector"); bool authlibInjectorInstalled = false;
const auto& agents = inst->getPackProfile()->getProfile()->getAgents();
for (const auto& agent : agents) {
if (agent->library()->artifactPrefix() == "moe.yushi:authlibinjector") {
authlibInjectorInstalled = true;
}
}
if (authlibInjectorVersion == "") { if (!authlibInjectorInstalled) {
// Account uses custom API servers, but authlib-injector is missing // Account uses custom API servers, but authlib-injector is missing
int globalMissingBehavior = APPLICATION->settings()->get("MissingAuthlibInjectorBehavior").toInt(); int globalMissingBehavior = APPLICATION->settings()->get("MissingAuthlibInjectorBehavior").toInt();

210
launcher/Manifest.cpp Normal file
View File

@ -0,0 +1,210 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Fjord Launcher - Minecraft Launcher
* Copyright (C) 2024 Evan Goode <mail@evangoo.de>
*
* 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 <https://www.gnu.org/licenses/>.
*
* This file incorporates work from OpenJDK, covered by the following copyright and
* permission notice:
*
* Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code 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
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*
* The GNU General Public License version 2 is available at https://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
*/
#include <string.h>
#include <iostream>
#include "Manifest.h"
// Manifest specification:
// https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Manifest_Specification
// The implementation of the Manifest class was ported to C++ from OpenJDK:
// https://github.com/openjdk/jdk/blob/0584af23255b6b8f49190eaf2618f3bcc299adfe/src/java.base/share/classes/java/util/jar/Manifest.java#L274
Manifest::Manifest(std::istream& is)
{
read(is, std::nullopt);
}
Manifest::Manifest(std::istream& is, const std::string& jar_filename)
{
read(is, jar_filename);
}
bool Manifest::isValidName(const std::string& name)
{
const auto len = name.length();
return 0 < len && len <= 70;
}
std::string Manifest::getErrorPosition(const std::optional<std::string>& filename, int line_number)
{
if (!filename.has_value()) {
return "line " + std::to_string(line_number);
}
return "manifest of " + *filename + ":" + std::to_string(line_number);
}
int Manifest::readAttributes(manifest_section_t& section,
std::istream& is,
lbuf_t lbuf,
const std::optional<std::string>& filename,
int line_number)
{
std::optional<std::string> name;
std::string value;
std::string full_line;
while (!is.eof() && is.getline(lbuf, MANIFEST_MAX_LINE_LENGTH)) {
std::size_t len = strlen(lbuf);
line_number += 1;
if (len > 0 && lbuf[len - 1] == '\r') {
len -= 1;
}
if (len == 0) {
break;
}
std::size_t i = 0;
if (lbuf[0] == ' ') {
// continuation of previous line
if (!name.has_value()) {
throw std::runtime_error("misplaced continuation line (" + getErrorPosition(filename, line_number) + ")");
}
full_line.append(lbuf + 1, len - 1);
if (is.peek() == ' ') {
continue;
}
value = full_line;
full_line.clear();
} else {
while (lbuf[i++] != ':') {
if (i >= len) {
throw std::runtime_error("invalid header field (" + getErrorPosition(filename, line_number) + ")");
}
}
if (lbuf[i++] != ' ') {
throw std::runtime_error("invalid header field (" + getErrorPosition(filename, line_number) + ")");
}
name = std::string{ lbuf, i - 2 };
if (is.peek() == ' ') {
full_line.clear();
full_line.append(lbuf + i, len - i);
continue;
}
value = std::string{ lbuf + i, len - i };
}
if (!isValidName(*name)) {
throw std::runtime_error("invalid header field name (" + getErrorPosition(filename, line_number) + ")");
}
section[*name] = value;
}
if (!is.eof() && is.fail()) {
throw std::length_error("line too long (" + getErrorPosition(filename, line_number) + ")");
}
return line_number;
}
std::optional<std::string> Manifest::parseName(lbuf_t lbuf, std::size_t len)
{
if (tolower(lbuf[0]) == 'n' && tolower(lbuf[1]) && tolower(lbuf[2]) == 'm' && tolower(lbuf[3]) && lbuf[4] == ':' && lbuf[5] == ' ') {
return std::string{ lbuf, 6, len - 6 };
}
return std::nullopt;
}
void Manifest::read(std::istream& is, const std::optional<std::string>& jar_filename)
{
// Line buffer
char lbuf[MANIFEST_MAX_LINE_LENGTH];
// Read the main attributes for the manifest
int line_number = readAttributes(m_main_section, is, lbuf, jar_filename, 0);
std::optional<std::string> name;
bool skip_empty_lines = true;
std::optional<std::string> lastline;
while (!is.eof() && is.getline(lbuf, MANIFEST_MAX_LINE_LENGTH)) {
std::size_t len = strlen(lbuf);
line_number += 1;
if (len > 0 && lbuf[len - 1] == '\r') {
len -= 1;
}
if (len == 0 && skip_empty_lines) {
continue;
}
skip_empty_lines = false;
if (!name.has_value()) {
name = parseName(lbuf, len);
if (!name.has_value()) {
throw std::runtime_error("invalid manifest format (" + getErrorPosition(jar_filename, line_number) + ")");
}
if (is.peek() == ' ') {
// name is wrapped
lastline = std::string{ lbuf + 6, len - 6 };
continue;
}
} else {
// continuation line
std::string buf{ *lastline };
buf.append(lbuf + 1, len - 1);
if (is.peek() == ' ') {
// name is wrapped
lastline = buf;
continue;
}
name = buf;
lastline = std::nullopt;
}
manifest_section_t& attr = m_individual_sections[*name];
line_number = readAttributes(attr, is, lbuf, jar_filename, line_number);
name = std::nullopt;
skip_empty_lines = true;
}
if (!is.eof() && is.fail()) {
throw std::length_error("manifest line too long (" + getErrorPosition(jar_filename, line_number) + ")");
}
}

67
launcher/Manifest.h Normal file
View File

@ -0,0 +1,67 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Fjord Launcher - Minecraft Launcher
* Copyright (C) 2024 Evan Goode <mail@evangoo.de>
*
* 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 <https://www.gnu.org/licenses/>.
*/
#include <map>
#include <optional>
#include <string>
constexpr const int MANIFEST_MAX_LINE_LENGTH = 512;
using lbuf_t = char[MANIFEST_MAX_LINE_LENGTH];
using manifest_section_t = std::map<std::string, std::string>;
using manifest_sections_t = std::map<std::string, manifest_section_t>;
class Manifest {
public:
Manifest(std::istream& is);
Manifest(std::istream& is, const std::string& jar_filename);
Manifest(const Manifest& other)
{
m_main_section = other.m_main_section;
m_individual_sections = other.m_individual_sections;
}
manifest_section_t& getMainAttributes() { return m_main_section; }
manifest_sections_t& getEntries() { return m_individual_sections; }
manifest_section_t& getAttributes(const std::string& name) { return m_individual_sections.at(name); }
Manifest& operator=(const Manifest& other)
{
if (this == &other) {
return *this;
}
m_main_section = other.m_main_section;
m_individual_sections = other.m_individual_sections;
return *this;
}
bool operator==(const Manifest& other) const
{
return m_main_section == other.m_main_section && m_individual_sections == other.m_individual_sections;
}
private:
static std::string getErrorPosition(const std::optional<std::string>& filename, int line_number);
static std::optional<std::string> parseName(lbuf_t lbuf, std::size_t len);
static bool isValidName(const std::string& name);
int readAttributes(manifest_section_t& section,
std::istream& is,
lbuf_t lbuf,
const std::optional<std::string>& jar_filename,
int line_number);
void read(std::istream& is, const std::optional<std::string>& jar_filename);
manifest_section_t m_main_section;
manifest_sections_t m_individual_sections;
};

View File

@ -1,9 +1,14 @@
#pragma once #pragma once
#include <QString> #include <QString>
#include <unordered_set>
#include "Library.h" #include "Library.h"
static const std::unordered_set<std::string> MANAGED_AGENTS = { "moe.yushi:authlibinjector" };
static const std::map<std::string, std::string> AGENT_CLASS_TO_MANAGED_AGENT = { { "moe.yushi.authlibinjector.Premain",
"moe.yushi:authlibinjector" } };
class Agent; class Agent;
using AgentPtr = std::shared_ptr<Agent>; using AgentPtr = std::shared_ptr<Agent>;

View File

@ -38,13 +38,10 @@
#include <java/JavaVersion.h> #include <java/JavaVersion.h>
#include <QDir> #include <QDir>
#include <QProcess> #include <QProcess>
#include <unordered_set>
#include "BaseInstance.h" #include "BaseInstance.h"
#include "minecraft/launch/MinecraftServerTarget.h" #include "minecraft/launch/MinecraftServerTarget.h"
#include "minecraft/mod/Mod.h" #include "minecraft/mod/Mod.h"
const std::unordered_set<std::string> MANAGED_AGENTS = { "moe.yushi:authlibinjector" };
class ModFolderModel; class ModFolderModel;
class ResourceFolderModel; class ResourceFolderModel;
class ResourcePackFolderModel; class ResourcePackFolderModel;

View File

@ -40,6 +40,9 @@
#include "Application.h" #include "Application.h"
#include <Version.h> #include <Version.h>
#include <quazip/quazip.h>
#include <quazip/quazipdir.h>
#include <quazip/quazipfile.h>
#include <QCryptographicHash> #include <QCryptographicHash>
#include <QDebug> #include <QDebug>
#include <QDir> #include <QDir>
@ -49,10 +52,12 @@
#include <QSaveFile> #include <QSaveFile>
#include <QTimer> #include <QTimer>
#include <QUuid> #include <QUuid>
#include <sstream>
#include "Exception.h" #include "Exception.h"
#include "FileSystem.h" #include "FileSystem.h"
#include "Json.h" #include "Json.h"
#include "Manifest.h"
#include "minecraft/MinecraftInstance.h" #include "minecraft/MinecraftInstance.h"
#include "minecraft/OneSixVersionFormat.h" #include "minecraft/OneSixVersionFormat.h"
#include "minecraft/ProfileUtils.h" #include "minecraft/ProfileUtils.h"
@ -874,6 +879,30 @@ bool PackProfile::installCustomJar_internal(QString filepath)
return true; return true;
} }
std::optional<Manifest> getJarManifest(const QFileInfo& fileinfo)
{
QuaZip zip(fileinfo.filePath());
if (!zip.open(QuaZip::mdUnzip)) {
return std::nullopt;
}
std::optional<Manifest> manifest;
QuaZipFile file(&zip);
if (zip.setCurrentFile("META-INF/MANIFEST.MF") && file.open(QIODevice::ReadOnly)) {
try {
const auto& file_bytes = file.readAll();
std::string file_contents(file_bytes.constData(), file_bytes.size());
std::istringstream iss{ file_contents };
manifest = Manifest(iss, fileinfo.fileName().toStdString());
} catch (std::exception& ex) {
qDebug() << "Error parsing META/MANIFEST.MF inside " << fileinfo.path() << ":" << ex.what();
}
file.close();
}
zip.close();
return manifest;
}
bool PackProfile::installAgents_internal(QStringList filepaths) bool PackProfile::installAgents_internal(QStringList filepaths)
{ {
// FIXME code duplication // FIXME code duplication
@ -903,7 +932,26 @@ bool PackProfile::installAgents_internal(QStringList filepaths)
auto agent = std::make_shared<Library>(); auto agent = std::make_shared<Library>();
agent->setRawName("custom.agents:" + id + ":1"); QString rawName = "custom.agents:" + id + ":1";
auto manifest = getJarManifest(sourceInfo);
if (manifest.has_value()) {
const auto& attrs = manifest->getMainAttributes();
if (auto ac = attrs.find("Agent-Class"); ac != attrs.end()) {
const auto& agentClass = ac->second;
if (auto ma = AGENT_CLASS_TO_MANAGED_AGENT.find(agentClass); ma != AGENT_CLASS_TO_MANAGED_AGENT.end()) {
const auto& artifactPrefix = QString::fromStdString(ma->second);
QString version = "1";
if (auto iv = attrs.find("Implementation-Version"); iv != attrs.end()) {
version = QString::fromStdString(iv->second);
}
rawName = artifactPrefix + ":" + version;
}
}
}
agent->setRawName(rawName);
agent->setFilename(targetBaseName); agent->setFilename(targetBaseName);
agent->setDisplayName(sourceInfo.completeBaseName()); agent->setDisplayName(sourceInfo.completeBaseName());
agent->setHint("local"); agent->setHint("local");

View File

@ -27,6 +27,9 @@ ecm_add_test(ResourceModel_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION
ecm_add_test(TexturePackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test ecm_add_test(TexturePackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
TEST_NAME TexturePackParse) TEST_NAME TexturePackParse)
ecm_add_test(Manifest_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
TEST_NAME Manifest)
ecm_add_test(DataPackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test ecm_add_test(DataPackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
TEST_NAME DataPackParse) TEST_NAME DataPackParse)

122
tests/Manifest_test.cpp Normal file
View File

@ -0,0 +1,122 @@
#include <QTest>
#include <QTimer>
#include <iostream>
#include "Manifest.h"
class ManifestTest : public QObject {
Q_OBJECT
private slots:
void test_emptyManifest()
{
std::istringstream iss;
const auto& manifest = new Manifest(iss);
QVERIFY(manifest->getMainAttributes().empty());
QVERIFY(manifest->getEntries().empty());
}
void test_parseManifest()
{
std::string manifest_text{ R"(Manifest-Version: 1.0
Created-By: 1.8.0 (Oracle Inc.)
Name: common/class1.class
SHA-256-Digest: D7fzW7bq0W+7YRCfxfZ4LY5LlCy+PXisjRgMCIiebS4=
Name: common/class2.class
SHA-256-Digest: TwUa/b/a2EQRsHWKupdFAR7S/BTeL52xTBvaB8C78Kc=
)" };
std::istringstream iss{ manifest_text };
const auto& manifest = new Manifest(iss);
QVERIFY(manifest->getEntries().size() == 2);
auto& main_attributes = manifest->getMainAttributes();
QVERIFY(main_attributes.size() == 2);
QVERIFY(main_attributes["Manifest-Version"] == "1.0");
QVERIFY(main_attributes["Created-By"] == "1.8.0 (Oracle Inc.)");
auto& class1_attributes = manifest->getAttributes("common/class1.class");
QVERIFY(class1_attributes.size() == 1);
QVERIFY(class1_attributes["SHA-256-Digest"] == "D7fzW7bq0W+7YRCfxfZ4LY5LlCy+PXisjRgMCIiebS4=");
auto& class2_attributes = manifest->getAttributes("common/class2.class");
QVERIFY(class2_attributes.size() == 1);
QVERIFY(class2_attributes["SHA-256-Digest"] == "TwUa/b/a2EQRsHWKupdFAR7S/BTeL52xTBvaB8C78Kc=");
// Manifest should parse even without the trailing newline
std::string manifest_text_no_newline{ manifest_text };
manifest_text_no_newline.pop_back();
std::istringstream iss_no_newline{ manifest_text_no_newline };
const auto& manifest_no_newline = new Manifest(iss_no_newline);
QVERIFY(*manifest_no_newline == *manifest);
}
void test_invalidName()
{
std::istringstream iss{ R"(Manifest-Version: 1.0
A-Name-That-Is-Way-Too-Loooooooooooooooooooooooooooooooooooooooooooooooonoooooooooong: 1
)" };
bool caught = false;
try {
new Manifest(iss);
} catch (const std::runtime_error&) {
caught = true;
}
QVERIFY(caught);
}
void test_lineTooLong()
{
std::string manifest_text{ "Manifest-Version: " };
manifest_text.append(std::string(MANIFEST_MAX_LINE_LENGTH, '1'));
std::istringstream iss{ manifest_text };
bool caught = false;
try {
new Manifest(iss);
} catch (const std::length_error&) {
caught = true;
}
QVERIFY(caught);
}
void test_misplacedContinuation()
{
std::istringstream iss{ " Manifest-Version: 1.0" };
bool caught = false;
try {
new Manifest(iss);
} catch (const std::runtime_error&) {
caught = true;
}
QVERIFY(caught);
}
void test_misingColon()
{
std::istringstream iss{ "Manifest-Version 1.0" };
bool caught = false;
try {
new Manifest(iss);
} catch (const std::runtime_error&) {
caught = true;
}
QVERIFY(caught);
}
void test_misingSpace()
{
std::istringstream iss{ "Manifest-Version:1.0" };
bool caught = false;
try {
new Manifest(iss);
} catch (const std::runtime_error&) {
caught = true;
}
QVERIFY(caught);
}
};
QTEST_GUILESS_MAIN(ManifestTest)
#include "Manifest_test.moc"