// SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Lenny McLennington * * 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 . */ #include "FetchFlameAPIKey.h" #include #include #include "Application.h" #include #include FetchFlameAPIKey::FetchFlameAPIKey() : Task{} {} // Here, we fetch the official CurseForge API key from the files of the // CurseForge app. We range-request the specific ~84KiB zlib block inside the // AppImage's SquashFS that contains the API key and extract only that. // Note: We need a direct link to the AppImage for this to work. The download // link for the Linux app on CurseForge's website is to a zipped AppImage, // which will not work here since (I think?) ZIP files typically have one // non-chunked DEFLATE stream for each file, and there's no way to fetch a // single, independent chunk to extract. // See also https://git.sakamoto.pl/domi/curseme/src/commit/388ac991eb57dedd5d1aca45f418deb221d757d1/getToken.sh const QUrl CURSEFORGE_APP_URL{ "https://curseforge.overwolf.com/electron/linux/CurseForge-0.198.1-21.AppImage" }; // Use https://github.com/unmojang/appimage-token-finder to find these offsets const uint32_t IN_ADDR{ 82926761 }; const uint32_t IN_SIZE{ 84196 }; const uint32_t OUT_SIZE{ 131072 }; void FetchFlameAPIKey::executeTask() { QNetworkRequest req{ CURSEFORGE_APP_URL }; // Request only a single zlib block from inside the AppImage 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); connect(m_reply.get(), #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) &QNetworkReply::errorOccurred, #else qOverload(&QNetworkReply::error), #endif this, [this](QNetworkReply::NetworkError error) { qCritical() << "Network error: " << error; emitFailed(m_reply->errorString()); }); setStatus(tr("Fetching Curseforge core API key (may take a few seconds)...")); } void FetchFlameAPIKey::downloadFinished() { auto res = m_reply->readAll(); // 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; res.prepend(expectedSizeHeader); 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(); }