Merge pull request #13 from unmojang/evan-goode/curseforge
Fetch CurseForge API key from official files
This commit is contained in:
commit
5e9b3c2baa
@ -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})
|
||||
|
@ -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@";
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -84,6 +84,7 @@
|
||||
#include <mutex>
|
||||
|
||||
#include <QAccessible>
|
||||
#include <QCheckBox>
|
||||
#include <QCommandLineParser>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
@ -1204,13 +1205,26 @@ 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 (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);
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
const auto& block = qUncompress(res);
|
||||
if (block.isEmpty()) {
|
||||
emitFailed("Couldn't decompress Curseforge app data.");
|
||||
}
|
||||
|
||||
if (success) {
|
||||
m_result = Json::requireString(obj, "token");
|
||||
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();
|
||||
} else {
|
||||
emitFailed("The API returned an output indicating failure.");
|
||||
}
|
||||
} catch (Json::JsonException&) {
|
||||
qCritical() << "Output: " << res;
|
||||
emitFailed("The API returned an unexpected JSON output.");
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -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();
|
||||
|
@ -294,7 +294,7 @@
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string><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></string>
|
||||
<string><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></string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
|
Loading…
Reference in New Issue
Block a user