Skip to content

Commit

Permalink
Merge pull request #13 from unmojang/evan-goode/curseforge
Browse files Browse the repository at this point in the history
Fetch CurseForge API key from official files
  • Loading branch information
evan-goode authored Jun 24, 2024
2 parents 51da756 + 810ab86 commit 5e9b3c2
Show file tree
Hide file tree
Showing 8 changed files with 76 additions and 36 deletions.
1 change: 0 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,6 @@ set(Launcher_MSA_CLIENT_ID "" CACHE STRING "Client ID you can get from Microsoft
# https://support.curseforge.com/en/support/solutions/articles/9000207405-curse-forge-3rd-party-api-terms-and-conditions
# NOTE: CurseForge requires you to change this if you make any kind of derivative work.
set(Launcher_CURSEFORGE_API_KEY "" CACHE STRING "API key for the CurseForge platform")
set(Launcher_CURSEFORGE_API_KEY_API_URL "https://cf.polymc.org/api" CACHE STRING "URL to fetch the Curseforge API key from.")

set(Launcher_COMPILER_NAME ${CMAKE_CXX_COMPILER_ID})
set(Launcher_COMPILER_VERSION ${CMAKE_CXX_COMPILER_VERSION})
Expand Down
1 change: 0 additions & 1 deletion buildconfig/BuildConfig.cpp.in
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ Config::Config()
IMGUR_CLIENT_ID = "@Launcher_IMGUR_CLIENT_ID@";
MSA_CLIENT_ID = "@Launcher_MSA_CLIENT_ID@";
FLAME_API_KEY = "@Launcher_CURSEFORGE_API_KEY@";
FLAME_API_KEY_API_URL = "@Launcher_CURSEFORGE_API_KEY_API_URL@";
META_URL = "@Launcher_META_URL@";

GLFW_LIBRARY_NAME = "@Launcher_GLFW_LIBRARY_NAME@";
Expand Down
5 changes: 0 additions & 5 deletions buildconfig/BuildConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,6 @@ class Config {
*/
QString FLAME_API_KEY;

/**
* URL to fetch the Client API key for CurseForge from
*/
QString FLAME_API_KEY_API_URL;

/**
* Metadata repository URL prefix
*/
Expand Down
26 changes: 20 additions & 6 deletions launcher/Application.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
#include <mutex>

#include <QAccessible>
#include <QCheckBox>
#include <QCommandLineParser>
#include <QDebug>
#include <QDir>
Expand Down Expand Up @@ -1204,12 +1205,25 @@ void Application::performMainStartupAction()
}
{
bool shouldFetch = m_settings->get("FlameKeyShouldBeFetchedOnStartup").toBool();
if (!BuildConfig.FLAME_API_KEY_API_URL.isEmpty() && shouldFetch && !(capabilities() & Capability::SupportsFlame)) {
// don't ask, just fetch
QString apiKey = GuiUtil::fetchFlameKey();
if (!apiKey.isEmpty()) {
m_settings->set("FlameKeyOverride", apiKey);
updateCapabilities();
if (shouldFetch && !(capabilities() & Capability::SupportsFlame)) {
QMessageBox msgBox{ m_mainWindow };
msgBox.setWindowTitle(tr("Fetch CurseForge Core API key?"));
msgBox.setText(tr("Would you like to fetch the official CurseForge app's API key now?"));
msgBox.setInformativeText(
tr("Using the official CurseForge app's API key may break CurseForge's terms of service but should allow Fjord Launcher "
"to download all mods in a modpack without you needing to download any of them manually."));
msgBox.setStandardButtons(QMessageBox::No | QMessageBox::Yes);
msgBox.setDefaultButton(QMessageBox::Yes);
msgBox.setModal(true);

const auto& result = msgBox.exec();

if (result == QMessageBox::Yes) {
const auto& apiKey = GuiUtil::fetchFlameKey();
if (!apiKey.isEmpty()) {
m_settings->set("FlameKeyOverride", apiKey);
updateCapabilities();
}
}
m_settings->set("FlameKeyShouldBeFetchedOnStartup", false);
}
Expand Down
71 changes: 55 additions & 16 deletions launcher/net/FetchFlameAPIKey.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,36 @@

FetchFlameAPIKey::FetchFlameAPIKey(QObject* parent) : Task{ parent } {}

// Here, we fetch the API key from the files of the official CurseForge app. We
// use the macOS disk image (as opposed to the zipped Linux AppImage) since
// there is only one layer of DEFLATE decompression to get through. We
// range-request the specific ~500KiB zlib block from the archive.org mirror
// that contains the API key.
// See also https://git.sakamoto.pl/domi/curseme/src/commit/388ac991eb57dedd5d1aca45f418deb221d757d1/getToken.sh

const QUrl CURSEFORGE_APP_URL{
"https://web.archive.org/web/20240520233008if_/https://curseforge.overwolf.com/downloads/curseforge-latest.dmg"
};

// To find these offsets:
// 1. Download the disk image from CURSEFORGE_APP_URL and run
// dmg2img -V ./curseforge-latest.dmg
// 2. Use a hex editor to find the address of the string "cfCoreApiKey" inside
// curseforge-latest.img.
// 3. In the output of `dmg2img -V`, find the `in_addr`, the `in_size`, and the
// ` out_size` of the block that contains this address.

const uint32_t IN_ADDR{ 4640617 };
const uint32_t IN_SIZE{ 511977 };
const uint32_t OUT_SIZE{ 1048576 };

void FetchFlameAPIKey::executeTask()
{
QNetworkRequest req(BuildConfig.FLAME_API_KEY_API_URL);
QNetworkRequest req{ CURSEFORGE_APP_URL };
// Request only a single block of the disk image file
const auto& rangeHeader = QString("bytes=%1-%2").arg(IN_ADDR).arg(IN_ADDR + IN_SIZE);
req.setRawHeader("Range", rangeHeader.toUtf8());

m_reply.reset(APPLICATION->network()->get(req));
connect(m_reply.get(), &QNetworkReply::downloadProgress, this, &Task::setProgress);
connect(m_reply.get(), &QNetworkReply::finished, this, &FetchFlameAPIKey::downloadFinished);
Expand All @@ -43,29 +70,41 @@ void FetchFlameAPIKey::executeTask()
emitFailed(m_reply->errorString());
});

setStatus(tr("Fetching Curseforge core API key"));
setStatus(tr("Fetching Curseforge core API key (may take a few seconds)..."));
}

void FetchFlameAPIKey::downloadFinished()
{
auto res = m_reply->readAll();
auto doc = QJsonDocument::fromJson(res);

qDebug() << doc;
// Prepend expected size header. See https://doc.qt.io/qt-6/qbytearray.html#qUncompress-1
QByteArray expectedSizeHeader;
QDataStream expectedSizeHeaderStream{ &expectedSizeHeader, QIODevice::WriteOnly };
expectedSizeHeaderStream.setByteOrder(QDataStream::BigEndian);
expectedSizeHeaderStream << OUT_SIZE;

try {
auto obj = Json::requireObject(doc);
res.prepend(expectedSizeHeader);

auto success = Json::requireBoolean(obj, "ok");
const auto& block = qUncompress(res);
if (block.isEmpty()) {
emitFailed("Couldn't decompress Curseforge app data.");
}

const char* precedingString = "\"cfCoreApiKey\":\"";
const QByteArray preceding{ precedingString };
const auto& precedingIndex = block.indexOf(preceding);
if (precedingIndex == -1) {
emitFailed(QString("Couldn't find string '%1'.").arg(precedingString));
}

if (success) {
m_result = Json::requireString(obj, "token");
emitSucceeded();
} else {
emitFailed("The API returned an output indicating failure.");
}
} catch (Json::JsonException&) {
qCritical() << "Output: " << res;
emitFailed("The API returned an unexpected JSON output.");
const auto& startIndex = precedingIndex + preceding.size();
const auto& finalIndex = block.indexOf(QByteArray{ "\"" }, startIndex);
if (finalIndex == -1) {
emitFailed("Couldn't find closing \" for cfCoreApiKey value.");
}

const auto& keyByteArray = block.mid(startIndex, finalIndex - startIndex);
m_result = QString{ keyByteArray };
qDebug() << "Fetched Flame API key: " << m_result;
emitSucceeded();
}
3 changes: 0 additions & 3 deletions launcher/ui/GuiUtil.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,6 @@

QString GuiUtil::fetchFlameKey(QWidget* parentWidget)
{
if (BuildConfig.FLAME_API_KEY_API_URL.isEmpty())
return "";

ProgressDialog prog(parentWidget);
auto flameKeyTask = std::make_unique<FetchFlameAPIKey>();
prog.execWithTask(flameKeyTask.get());
Expand Down
3 changes: 0 additions & 3 deletions launcher/ui/pages/global/APIPage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,6 @@ APIPage::APIPage(QWidget* parent) : QWidget(parent), ui(new Ui::APIPage)
ui->metaURL->setPlaceholderText(BuildConfig.META_URL);
ui->userAgentLineEdit->setPlaceholderText(BuildConfig.USER_AGENT);

if (BuildConfig.FLAME_API_KEY_API_URL.isEmpty())
ui->fetchKeyButton->hide();

loadSettings();

resetBaseURLNote();
Expand Down
2 changes: 1 addition & 1 deletion launcher/ui/pages/global/APIPage.ui
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@
</sizepolicy>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Note: you probably don't need to set this if CurseForge already works.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700;&quot;&gt;Using the Official Curseforge Launcher's key may break Curseforge's Terms of service, but should allow Fjord Launcher to download all mods in a modpack without you needing to download any of them manually.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Note: you probably don't need to set this if CurseForge already works.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700;&quot;&gt;Using the official CurseForge app's API key may break CurseForge's terms of service but should allow Fjord Launcher to download all mods in a modpack without you needing to download any of them manually.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
Expand Down

0 comments on commit 5e9b3c2

Please sign in to comment.