diff --git a/CMakeLists.txt b/CMakeLists.txt index 33d281772..89cd71763 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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}) diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in index 5013d3b2c..f8af22551 100644 --- a/buildconfig/BuildConfig.cpp.in +++ b/buildconfig/BuildConfig.cpp.in @@ -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@"; diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index 71f597857..d62d08efc 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -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 */ diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 7140f859c..d6cf2b6c6 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -84,6 +84,7 @@ #include #include +#include #include #include #include @@ -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); } diff --git a/launcher/net/FetchFlameAPIKey.cpp b/launcher/net/FetchFlameAPIKey.cpp index a5e0c3fca..f40138929 100644 --- a/launcher/net/FetchFlameAPIKey.cpp +++ b/launcher/net/FetchFlameAPIKey.cpp @@ -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); @@ -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"); - - 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& 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)); + } + + 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(); } diff --git a/launcher/ui/GuiUtil.cpp b/launcher/ui/GuiUtil.cpp index 620ec5d94..ca80fcaa7 100644 --- a/launcher/ui/GuiUtil.cpp +++ b/launcher/ui/GuiUtil.cpp @@ -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(); prog.execWithTask(flameKeyTask.get()); diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp index da39a1b63..baaa83228 100644 --- a/launcher/ui/pages/global/APIPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -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(); diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index bd18050ef..51ad739b2 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -294,7 +294,7 @@ - <html><head/><body><p>Note: you probably don't need to set this if CurseForge already works.</p><p><span style=" font-weight:700;">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.</span></p></body></html> + <html><head/><body><p>Note: you probably don't need to set this if CurseForge already works.</p><p><span style=" font-weight:700;">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.</span></p></body></html> true