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
|
# 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.
|
# 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 "" 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_NAME ${CMAKE_CXX_COMPILER_ID})
|
||||||
set(Launcher_COMPILER_VERSION ${CMAKE_CXX_COMPILER_VERSION})
|
set(Launcher_COMPILER_VERSION ${CMAKE_CXX_COMPILER_VERSION})
|
||||||
|
@ -117,7 +117,6 @@ Config::Config()
|
|||||||
IMGUR_CLIENT_ID = "@Launcher_IMGUR_CLIENT_ID@";
|
IMGUR_CLIENT_ID = "@Launcher_IMGUR_CLIENT_ID@";
|
||||||
MSA_CLIENT_ID = "@Launcher_MSA_CLIENT_ID@";
|
MSA_CLIENT_ID = "@Launcher_MSA_CLIENT_ID@";
|
||||||
FLAME_API_KEY = "@Launcher_CURSEFORGE_API_KEY@";
|
FLAME_API_KEY = "@Launcher_CURSEFORGE_API_KEY@";
|
||||||
FLAME_API_KEY_API_URL = "@Launcher_CURSEFORGE_API_KEY_API_URL@";
|
|
||||||
META_URL = "@Launcher_META_URL@";
|
META_URL = "@Launcher_META_URL@";
|
||||||
|
|
||||||
GLFW_LIBRARY_NAME = "@Launcher_GLFW_LIBRARY_NAME@";
|
GLFW_LIBRARY_NAME = "@Launcher_GLFW_LIBRARY_NAME@";
|
||||||
|
@ -148,11 +148,6 @@ class Config {
|
|||||||
*/
|
*/
|
||||||
QString FLAME_API_KEY;
|
QString FLAME_API_KEY;
|
||||||
|
|
||||||
/**
|
|
||||||
* URL to fetch the Client API key for CurseForge from
|
|
||||||
*/
|
|
||||||
QString FLAME_API_KEY_API_URL;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Metadata repository URL prefix
|
* Metadata repository URL prefix
|
||||||
*/
|
*/
|
||||||
|
@ -84,6 +84,7 @@
|
|||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
|
||||||
#include <QAccessible>
|
#include <QAccessible>
|
||||||
|
#include <QCheckBox>
|
||||||
#include <QCommandLineParser>
|
#include <QCommandLineParser>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
@ -1204,12 +1205,25 @@ void Application::performMainStartupAction()
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
bool shouldFetch = m_settings->get("FlameKeyShouldBeFetchedOnStartup").toBool();
|
bool shouldFetch = m_settings->get("FlameKeyShouldBeFetchedOnStartup").toBool();
|
||||||
if (!BuildConfig.FLAME_API_KEY_API_URL.isEmpty() && shouldFetch && !(capabilities() & Capability::SupportsFlame)) {
|
if (shouldFetch && !(capabilities() & Capability::SupportsFlame)) {
|
||||||
// don't ask, just fetch
|
QMessageBox msgBox{ m_mainWindow };
|
||||||
QString apiKey = GuiUtil::fetchFlameKey();
|
msgBox.setWindowTitle(tr("Fetch CurseForge Core API key?"));
|
||||||
if (!apiKey.isEmpty()) {
|
msgBox.setText(tr("Would you like to fetch the official CurseForge app's API key now?"));
|
||||||
m_settings->set("FlameKeyOverride", apiKey);
|
msgBox.setInformativeText(
|
||||||
updateCapabilities();
|
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);
|
m_settings->set("FlameKeyShouldBeFetchedOnStartup", false);
|
||||||
}
|
}
|
||||||
|
@ -26,9 +26,36 @@
|
|||||||
|
|
||||||
FetchFlameAPIKey::FetchFlameAPIKey(QObject* parent) : Task{ parent } {}
|
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()
|
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));
|
m_reply.reset(APPLICATION->network()->get(req));
|
||||||
connect(m_reply.get(), &QNetworkReply::downloadProgress, this, &Task::setProgress);
|
connect(m_reply.get(), &QNetworkReply::downloadProgress, this, &Task::setProgress);
|
||||||
connect(m_reply.get(), &QNetworkReply::finished, this, &FetchFlameAPIKey::downloadFinished);
|
connect(m_reply.get(), &QNetworkReply::finished, this, &FetchFlameAPIKey::downloadFinished);
|
||||||
@ -43,29 +70,41 @@ void FetchFlameAPIKey::executeTask()
|
|||||||
emitFailed(m_reply->errorString());
|
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()
|
void FetchFlameAPIKey::downloadFinished()
|
||||||
{
|
{
|
||||||
auto res = m_reply->readAll();
|
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 {
|
res.prepend(expectedSizeHeader);
|
||||||
auto obj = Json::requireObject(doc);
|
|
||||||
|
|
||||||
auto success = Json::requireBoolean(obj, "ok");
|
const auto& block = qUncompress(res);
|
||||||
|
if (block.isEmpty()) {
|
||||||
if (success) {
|
emitFailed("Couldn't decompress Curseforge app data.");
|
||||||
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 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();
|
||||||
}
|
}
|
||||||
|
@ -54,9 +54,6 @@
|
|||||||
|
|
||||||
QString GuiUtil::fetchFlameKey(QWidget* parentWidget)
|
QString GuiUtil::fetchFlameKey(QWidget* parentWidget)
|
||||||
{
|
{
|
||||||
if (BuildConfig.FLAME_API_KEY_API_URL.isEmpty())
|
|
||||||
return "";
|
|
||||||
|
|
||||||
ProgressDialog prog(parentWidget);
|
ProgressDialog prog(parentWidget);
|
||||||
auto flameKeyTask = std::make_unique<FetchFlameAPIKey>();
|
auto flameKeyTask = std::make_unique<FetchFlameAPIKey>();
|
||||||
prog.execWithTask(flameKeyTask.get());
|
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->metaURL->setPlaceholderText(BuildConfig.META_URL);
|
||||||
ui->userAgentLineEdit->setPlaceholderText(BuildConfig.USER_AGENT);
|
ui->userAgentLineEdit->setPlaceholderText(BuildConfig.USER_AGENT);
|
||||||
|
|
||||||
if (BuildConfig.FLAME_API_KEY_API_URL.isEmpty())
|
|
||||||
ui->fetchKeyButton->hide();
|
|
||||||
|
|
||||||
loadSettings();
|
loadSettings();
|
||||||
|
|
||||||
resetBaseURLNote();
|
resetBaseURLNote();
|
||||||
|
@ -294,7 +294,7 @@
|
|||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<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>
|
||||||
<property name="wordWrap">
|
<property name="wordWrap">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
|
Loading…
Reference in New Issue
Block a user