mirror of
https://github.com/PrismLauncher/PrismLauncher.git
synced 2025-06-12 21:27:44 +02:00
Merge branch 'develop' of https://github.com/PrismLauncher/PrismLauncher into change_version
This commit is contained in:
@ -48,6 +48,7 @@
|
||||
#include "pathmatcher/MultiMatcher.h"
|
||||
#include "pathmatcher/SimplePrefixMatcher.h"
|
||||
#include "settings/INIFile.h"
|
||||
#include "tools/GenericProfiler.h"
|
||||
#include "ui/InstanceWindow.h"
|
||||
#include "ui/MainWindow.h"
|
||||
|
||||
@ -132,6 +133,15 @@
|
||||
#include "gamemode_client.h"
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_LINUX)
|
||||
#include <sys/statvfs.h>
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
|
||||
#include <sys/mount.h>
|
||||
#include <sys/types.h>
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_MAC)
|
||||
#if defined(SPARKLE_ENABLED)
|
||||
#include "updater/MacSparkleUpdater.h"
|
||||
@ -216,6 +226,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
|
||||
|
||||
// Don't quit on hiding the last window
|
||||
this->setQuitOnLastWindowClosed(false);
|
||||
this->setQuitLockEnabled(false);
|
||||
|
||||
// Commandline parsing
|
||||
QCommandLineParser parser;
|
||||
@ -299,7 +310,11 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
|
||||
adjustedBy = "Persistent data path";
|
||||
|
||||
#ifndef Q_OS_MACOS
|
||||
if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) {
|
||||
if (auto portableUserData = FS::PathCombine(m_rootPath, "UserData"); QDir(portableUserData).exists()) {
|
||||
dataPath = portableUserData;
|
||||
adjustedBy = "Portable user data path";
|
||||
m_portable = true;
|
||||
} else if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) {
|
||||
dataPath = m_rootPath;
|
||||
adjustedBy = "Portable data path";
|
||||
m_portable = true;
|
||||
@ -485,8 +500,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
|
||||
}
|
||||
|
||||
{
|
||||
qDebug() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME) << ", (c) 2022-2023 "
|
||||
<< qPrintable(QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", "));
|
||||
qDebug() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME + ", " + QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", "));
|
||||
qDebug() << "Version : " << BuildConfig.printableVersionString();
|
||||
qDebug() << "Platform : " << BuildConfig.BUILD_PLATFORM;
|
||||
qDebug() << "Git commit : " << BuildConfig.GIT_COMMIT;
|
||||
@ -631,10 +645,11 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
|
||||
m_settings->registerSetting("UseNativeGLFW", false);
|
||||
m_settings->registerSetting("CustomGLFWPath", "");
|
||||
|
||||
// Peformance related options
|
||||
// Performance related options
|
||||
m_settings->registerSetting("EnableFeralGamemode", false);
|
||||
m_settings->registerSetting("EnableMangoHud", false);
|
||||
m_settings->registerSetting("UseDiscreteGpu", false);
|
||||
m_settings->registerSetting("UseZink", false);
|
||||
|
||||
// Game time
|
||||
m_settings->registerSetting("ShowGameTime", true);
|
||||
@ -658,6 +673,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
|
||||
|
||||
// The cat
|
||||
m_settings->registerSetting("TheCat", false);
|
||||
m_settings->registerSetting("CatOpacity", 100);
|
||||
|
||||
m_settings->registerSetting("StatusBarVisible", true);
|
||||
|
||||
m_settings->registerSetting("ToolbarsLocked", false);
|
||||
|
||||
@ -740,6 +758,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
|
||||
m_settings->registerSetting("ModrinthToken", "");
|
||||
m_settings->registerSetting("UserAgentOverride", "");
|
||||
|
||||
// FTBApp instances
|
||||
m_settings->registerSetting("FTBAppInstancesPath", "");
|
||||
|
||||
// Init page provider
|
||||
{
|
||||
m_globalSettingsProvider = std::make_shared<GenericPageProvider>(tr("Settings"));
|
||||
@ -859,6 +880,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
|
||||
// FIXME: what to do with these?
|
||||
m_profilers.insert("jprofiler", std::shared_ptr<BaseProfilerFactory>(new JProfilerFactory()));
|
||||
m_profilers.insert("jvisualvm", std::shared_ptr<BaseProfilerFactory>(new JVisualVMFactory()));
|
||||
m_profilers.insert("generic", std::shared_ptr<BaseProfilerFactory>(new GenericProfilerFactory()));
|
||||
for (auto profiler : m_profilers.values()) {
|
||||
profiler->registerSettings(m_settings);
|
||||
}
|
||||
@ -988,6 +1010,37 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
|
||||
}
|
||||
}
|
||||
|
||||
// notify user if /tmp is mounted with `noexec` (#1693)
|
||||
{
|
||||
bool is_tmp_noexec = false;
|
||||
|
||||
#if defined(Q_OS_LINUX)
|
||||
|
||||
struct statvfs tmp_stat;
|
||||
statvfs("/tmp", &tmp_stat);
|
||||
is_tmp_noexec = tmp_stat.f_flag & ST_NOEXEC;
|
||||
|
||||
#elif defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
|
||||
|
||||
struct statfs tmp_stat;
|
||||
statfs("/tmp", &tmp_stat);
|
||||
is_tmp_noexec = tmp_stat.f_flags & MNT_NOEXEC;
|
||||
|
||||
#endif
|
||||
|
||||
if (is_tmp_noexec) {
|
||||
auto infoMsg =
|
||||
tr("Your /tmp directory is currently mounted with the 'noexec' flag enabled.\n"
|
||||
"Some versions of Minecraft may not launch.\n");
|
||||
auto msgBox = new QMessageBox(QMessageBox::Information, tr("Incompatible system configuration"), infoMsg, QMessageBox::Ok);
|
||||
msgBox->setDefaultButton(QMessageBox::Ok);
|
||||
msgBox->setAttribute(Qt::WA_DeleteOnClose);
|
||||
msgBox->setMinimumWidth(460);
|
||||
msgBox->adjustSize();
|
||||
msgBox->open();
|
||||
}
|
||||
}
|
||||
|
||||
if (createSetupWizard()) {
|
||||
return;
|
||||
}
|
||||
@ -1471,6 +1524,17 @@ InstanceWindow* Application::showInstanceWindow(InstancePtr instance, QString pa
|
||||
auto& window = extras.window;
|
||||
|
||||
if (window) {
|
||||
// If the window is minimized on macOS or Windows, activate and bring it up
|
||||
#ifdef Q_OS_MACOS
|
||||
if (window->isMinimized()) {
|
||||
window->setWindowState(window->windowState() & ~Qt::WindowMinimized);
|
||||
}
|
||||
#elif defined(Q_OS_WIN)
|
||||
if (window->isMinimized()) {
|
||||
window->showNormal();
|
||||
}
|
||||
#endif
|
||||
|
||||
window->raise();
|
||||
window->activateWindow();
|
||||
} else {
|
||||
@ -1478,6 +1542,7 @@ InstanceWindow* Application::showInstanceWindow(InstancePtr instance, QString pa
|
||||
m_openWindows++;
|
||||
connect(window, &InstanceWindow::isClosing, this, &Application::on_windowClose);
|
||||
}
|
||||
|
||||
if (!page.isEmpty()) {
|
||||
window->selectPage(page);
|
||||
}
|
||||
|
@ -64,6 +64,8 @@ BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr s
|
||||
|
||||
m_settings->registerSetting("lastLaunchTime", 0);
|
||||
m_settings->registerSetting("totalTimePlayed", 0);
|
||||
if (m_settings->get("totalTimePlayed").toLongLong() < 0)
|
||||
m_settings->reset("totalTimePlayed");
|
||||
m_settings->registerSetting("lastTimePlayed", 0);
|
||||
|
||||
m_settings->registerSetting("linkedInstances", "[]");
|
||||
|
@ -126,7 +126,6 @@ set(NET_SOURCES
|
||||
net/MetaCacheSink.h
|
||||
net/Logging.h
|
||||
net/Logging.cpp
|
||||
net/NetAction.h
|
||||
net/NetJob.cpp
|
||||
net/NetJob.h
|
||||
net/NetUtils.h
|
||||
@ -210,28 +209,17 @@ set(MINECRAFT_SOURCES
|
||||
minecraft/auth/AccountData.h
|
||||
minecraft/auth/AccountList.cpp
|
||||
minecraft/auth/AccountList.h
|
||||
minecraft/auth/AccountTask.cpp
|
||||
minecraft/auth/AccountTask.h
|
||||
minecraft/auth/AuthRequest.cpp
|
||||
minecraft/auth/AuthRequest.h
|
||||
minecraft/auth/AuthSession.cpp
|
||||
minecraft/auth/AuthSession.h
|
||||
minecraft/auth/AuthStep.cpp
|
||||
minecraft/auth/AuthStep.h
|
||||
minecraft/auth/MinecraftAccount.cpp
|
||||
minecraft/auth/MinecraftAccount.h
|
||||
minecraft/auth/Parsers.cpp
|
||||
minecraft/auth/Parsers.h
|
||||
|
||||
minecraft/auth/flows/AuthFlow.cpp
|
||||
minecraft/auth/flows/AuthFlow.h
|
||||
minecraft/auth/flows/MSA.cpp
|
||||
minecraft/auth/flows/MSA.h
|
||||
minecraft/auth/flows/Offline.cpp
|
||||
minecraft/auth/flows/Offline.h
|
||||
minecraft/auth/AuthFlow.cpp
|
||||
minecraft/auth/AuthFlow.h
|
||||
|
||||
minecraft/auth/steps/OfflineStep.cpp
|
||||
minecraft/auth/steps/OfflineStep.h
|
||||
minecraft/auth/steps/EntitlementsStep.cpp
|
||||
minecraft/auth/steps/EntitlementsStep.h
|
||||
minecraft/auth/steps/GetSkinStep.cpp
|
||||
@ -240,6 +228,8 @@ set(MINECRAFT_SOURCES
|
||||
minecraft/auth/steps/LauncherLoginStep.h
|
||||
minecraft/auth/steps/MinecraftProfileStep.cpp
|
||||
minecraft/auth/steps/MinecraftProfileStep.h
|
||||
minecraft/auth/steps/MSADeviceCodeStep.cpp
|
||||
minecraft/auth/steps/MSADeviceCodeStep.h
|
||||
minecraft/auth/steps/MSAStep.cpp
|
||||
minecraft/auth/steps/MSAStep.h
|
||||
minecraft/auth/steps/XboxAuthorizationStep.cpp
|
||||
@ -453,6 +443,8 @@ set(TOOLS_SOURCES
|
||||
tools/JVisualVM.h
|
||||
tools/MCEditTool.cpp
|
||||
tools/MCEditTool.h
|
||||
tools/GenericProfiler.cpp
|
||||
tools/GenericProfiler.h
|
||||
)
|
||||
|
||||
set(META_SOURCES
|
||||
@ -624,7 +616,6 @@ set(PRISMUPDATER_SOURCES
|
||||
net/HttpMetaCache.h
|
||||
net/Logging.h
|
||||
net/Logging.cpp
|
||||
net/NetAction.h
|
||||
net/NetRequest.cpp
|
||||
net/NetRequest.h
|
||||
net/NetJob.cpp
|
||||
@ -827,6 +818,8 @@ SET(LAUNCHER_SOURCES
|
||||
ui/themes/DarkTheme.h
|
||||
ui/themes/ITheme.cpp
|
||||
ui/themes/ITheme.h
|
||||
ui/themes/HintOverrideProxyStyle.cpp
|
||||
ui/themes/HintOverrideProxyStyle.h
|
||||
ui/themes/SystemTheme.cpp
|
||||
ui/themes/SystemTheme.h
|
||||
ui/themes/IconTheme.cpp
|
||||
@ -1239,7 +1232,6 @@ target_link_libraries(Launcher_logic
|
||||
tomlplusplus::tomlplusplus
|
||||
qdcss
|
||||
BuildConfig
|
||||
Katabasis
|
||||
Qt${QT_VERSION_MAJOR}::Widgets
|
||||
ghcFilesystem::ghc_filesystem
|
||||
)
|
||||
@ -1257,6 +1249,7 @@ target_link_libraries(Launcher_logic
|
||||
Qt${QT_VERSION_MAJOR}::Concurrent
|
||||
Qt${QT_VERSION_MAJOR}::Gui
|
||||
Qt${QT_VERSION_MAJOR}::Widgets
|
||||
Qt${QT_VERSION_MAJOR}::NetworkAuth
|
||||
${Launcher_QT_LIBS}
|
||||
)
|
||||
target_link_libraries(Launcher_logic
|
||||
@ -1327,7 +1320,6 @@ if(Launcher_BUILD_UPDATER)
|
||||
Qt${QT_VERSION_MAJOR}::Network
|
||||
${Launcher_QT_LIBS}
|
||||
cmark::cmark
|
||||
Katabasis
|
||||
)
|
||||
|
||||
add_executable("${Launcher_Name}_updater" WIN32 updater/prismupdater/updater_main.cpp)
|
||||
@ -1492,7 +1484,6 @@ if(INSTALL_BUNDLE STREQUAL "full")
|
||||
CONFIGURATIONS Debug RelWithDebInfo ""
|
||||
DESTINATION ${PLUGIN_DEST_DIR}
|
||||
COMPONENT Runtime
|
||||
PATTERN "*qopensslbackend*" EXCLUDE
|
||||
PATTERN "*qcertonlybackend*" EXCLUDE
|
||||
)
|
||||
install(
|
||||
@ -1503,10 +1494,78 @@ if(INSTALL_BUNDLE STREQUAL "full")
|
||||
REGEX "dd\\." EXCLUDE
|
||||
REGEX "_debug\\." EXCLUDE
|
||||
REGEX "\\.dSYM" EXCLUDE
|
||||
PATTERN "*qopensslbackend*" EXCLUDE
|
||||
PATTERN "*qcertonlybackend*" EXCLUDE
|
||||
)
|
||||
endif()
|
||||
# Wayland support
|
||||
if(EXISTS "${QT_PLUGINS_DIR}/wayland-graphics-integration-client")
|
||||
install(
|
||||
DIRECTORY "${QT_PLUGINS_DIR}/wayland-graphics-integration-client"
|
||||
CONFIGURATIONS Debug RelWithDebInfo ""
|
||||
DESTINATION ${PLUGIN_DEST_DIR}
|
||||
COMPONENT Runtime
|
||||
)
|
||||
install(
|
||||
DIRECTORY "${QT_PLUGINS_DIR}/wayland-graphics-integration-client"
|
||||
CONFIGURATIONS Release MinSizeRel
|
||||
DESTINATION ${PLUGIN_DEST_DIR}
|
||||
COMPONENT Runtime
|
||||
REGEX "dd\\." EXCLUDE
|
||||
REGEX "_debug\\." EXCLUDE
|
||||
REGEX "\\.dSYM" EXCLUDE
|
||||
)
|
||||
endif()
|
||||
if(EXISTS "${QT_PLUGINS_DIR}/wayland-graphics-integration-server")
|
||||
install(
|
||||
DIRECTORY "${QT_PLUGINS_DIR}/wayland-graphics-integration-server"
|
||||
CONFIGURATIONS Debug RelWithDebInfo ""
|
||||
DESTINATION ${PLUGIN_DEST_DIR}
|
||||
COMPONENT Runtime
|
||||
)
|
||||
install(
|
||||
DIRECTORY "${QT_PLUGINS_DIR}/wayland-graphics-integration-server"
|
||||
CONFIGURATIONS Release MinSizeRel
|
||||
DESTINATION ${PLUGIN_DEST_DIR}
|
||||
COMPONENT Runtime
|
||||
REGEX "dd\\." EXCLUDE
|
||||
REGEX "_debug\\." EXCLUDE
|
||||
REGEX "\\.dSYM" EXCLUDE
|
||||
)
|
||||
endif()
|
||||
if(EXISTS "${QT_PLUGINS_DIR}/wayland-decoration-client")
|
||||
install(
|
||||
DIRECTORY "${QT_PLUGINS_DIR}/wayland-decoration-client"
|
||||
CONFIGURATIONS Debug RelWithDebInfo ""
|
||||
DESTINATION ${PLUGIN_DEST_DIR}
|
||||
COMPONENT Runtime
|
||||
)
|
||||
install(
|
||||
DIRECTORY "${QT_PLUGINS_DIR}/wayland-decoration-client"
|
||||
CONFIGURATIONS Release MinSizeRel
|
||||
DESTINATION ${PLUGIN_DEST_DIR}
|
||||
COMPONENT Runtime
|
||||
REGEX "dd\\." EXCLUDE
|
||||
REGEX "_debug\\." EXCLUDE
|
||||
REGEX "\\.dSYM" EXCLUDE
|
||||
)
|
||||
endif()
|
||||
if(EXISTS "${QT_PLUGINS_DIR}/wayland-shell-integration")
|
||||
install(
|
||||
DIRECTORY "${QT_PLUGINS_DIR}/wayland-shell-integration"
|
||||
CONFIGURATIONS Debug RelWithDebInfo ""
|
||||
DESTINATION ${PLUGIN_DEST_DIR}
|
||||
COMPONENT Runtime
|
||||
)
|
||||
install(
|
||||
DIRECTORY "${QT_PLUGINS_DIR}/wayland-shell-integration"
|
||||
CONFIGURATIONS Release MinSizeRel
|
||||
DESTINATION ${PLUGIN_DEST_DIR}
|
||||
COMPONENT Runtime
|
||||
REGEX "dd\\." EXCLUDE
|
||||
REGEX "_debug\\." EXCLUDE
|
||||
REGEX "\\.dSYM" EXCLUDE
|
||||
)
|
||||
endif()
|
||||
configure_file(
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/install_prereqs.cmake.in"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/install_prereqs.cmake"
|
||||
|
@ -37,140 +37,33 @@
|
||||
#include <QDesktopServices>
|
||||
#include <QDir>
|
||||
#include <QProcess>
|
||||
|
||||
/**
|
||||
* This shouldn't exist, but until QTBUG-9328 and other unreported bugs are fixed, it needs to be a thing.
|
||||
*/
|
||||
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
|
||||
|
||||
#include <errno.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
template <typename T>
|
||||
bool IndirectOpen(T callable, qint64* pid_forked = nullptr)
|
||||
{
|
||||
auto pid = fork();
|
||||
if (pid_forked) {
|
||||
if (pid > 0)
|
||||
*pid_forked = pid;
|
||||
else
|
||||
*pid_forked = 0;
|
||||
}
|
||||
if (pid == -1) {
|
||||
qWarning() << "IndirectOpen failed to fork: " << errno;
|
||||
return false;
|
||||
}
|
||||
// child - do the stuff
|
||||
if (pid == 0) {
|
||||
// unset all this garbage so it doesn't get passed to the child process
|
||||
qunsetenv("LD_PRELOAD");
|
||||
qunsetenv("LD_LIBRARY_PATH");
|
||||
qunsetenv("LD_DEBUG");
|
||||
qunsetenv("QT_PLUGIN_PATH");
|
||||
qunsetenv("QT_FONTPATH");
|
||||
|
||||
// open the URL
|
||||
auto status = callable();
|
||||
|
||||
// detach from the parent process group.
|
||||
setsid();
|
||||
|
||||
// die. now. do not clean up anything, it would just hang forever.
|
||||
_exit(status ? 0 : 1);
|
||||
} else {
|
||||
// parent - assume it worked.
|
||||
int status;
|
||||
while (waitpid(pid, &status, 0)) {
|
||||
if (WIFEXITED(status)) {
|
||||
return WEXITSTATUS(status) == 0;
|
||||
}
|
||||
if (WIFSIGNALED(status)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#include "FileSystem.h"
|
||||
|
||||
namespace DesktopServices {
|
||||
bool openDirectory(const QString& path, [[maybe_unused]] bool ensureExists)
|
||||
bool openPath(const QFileInfo& path, bool ensureFolderPathExists)
|
||||
{
|
||||
qDebug() << "Opening directory" << path;
|
||||
QDir parentPath;
|
||||
QDir dir(path);
|
||||
if (ensureExists && !dir.exists()) {
|
||||
parentPath.mkpath(dir.absolutePath());
|
||||
qDebug() << "Opening path" << path;
|
||||
if (ensureFolderPathExists) {
|
||||
FS::ensureFolderPathExists(path);
|
||||
}
|
||||
auto f = [&]() { return QDesktopServices::openUrl(QUrl::fromLocalFile(dir.absolutePath())); };
|
||||
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
|
||||
if (!isSandbox()) {
|
||||
return IndirectOpen(f);
|
||||
}
|
||||
#endif
|
||||
return f();
|
||||
return openUrl(QUrl::fromLocalFile(QFileInfo(path).absoluteFilePath()));
|
||||
}
|
||||
|
||||
bool openFile(const QString& path)
|
||||
bool openPath(const QString& path, bool ensureFolderPathExists)
|
||||
{
|
||||
qDebug() << "Opening file" << path;
|
||||
auto f = [&]() { return QDesktopServices::openUrl(QUrl::fromLocalFile(path)); };
|
||||
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
|
||||
if (!isSandbox()) {
|
||||
return IndirectOpen(f);
|
||||
} else {
|
||||
return f();
|
||||
}
|
||||
#else
|
||||
return f();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool openFile(const QString& application, const QString& path, const QString& workingDirectory, qint64* pid)
|
||||
{
|
||||
qDebug() << "Opening file" << path << "using" << application;
|
||||
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
|
||||
// FIXME: the pid here is fake. So if something depends on it, it will likely misbehave
|
||||
if (!isSandbox()) {
|
||||
return IndirectOpen([&]() { return QProcess::startDetached(application, QStringList() << path, workingDirectory); }, pid);
|
||||
} else {
|
||||
return QProcess::startDetached(application, QStringList() << path, workingDirectory, pid);
|
||||
}
|
||||
#else
|
||||
return QProcess::startDetached(application, QStringList() << path, workingDirectory, pid);
|
||||
#endif
|
||||
return openPath(QFileInfo(path), ensureFolderPathExists);
|
||||
}
|
||||
|
||||
bool run(const QString& application, const QStringList& args, const QString& workingDirectory, qint64* pid)
|
||||
{
|
||||
qDebug() << "Running" << application << "with args" << args.join(' ');
|
||||
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
|
||||
if (!isSandbox()) {
|
||||
// FIXME: the pid here is fake. So if something depends on it, it will likely misbehave
|
||||
return IndirectOpen([&]() { return QProcess::startDetached(application, args, workingDirectory); }, pid);
|
||||
} else {
|
||||
return QProcess::startDetached(application, args, workingDirectory, pid);
|
||||
}
|
||||
#else
|
||||
return QProcess::startDetached(application, args, workingDirectory, pid);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool openUrl(const QUrl& url)
|
||||
{
|
||||
qDebug() << "Opening URL" << url.toString();
|
||||
auto f = [&]() { return QDesktopServices::openUrl(url); };
|
||||
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
|
||||
if (!isSandbox()) {
|
||||
return IndirectOpen(f);
|
||||
} else {
|
||||
return f();
|
||||
}
|
||||
#else
|
||||
return f();
|
||||
#endif
|
||||
return QDesktopServices::openUrl(url);
|
||||
}
|
||||
|
||||
bool isFlatpak()
|
||||
@ -191,9 +84,4 @@ bool isSnap()
|
||||
#endif
|
||||
}
|
||||
|
||||
bool isSandbox()
|
||||
{
|
||||
return isSnap() || isFlatpak();
|
||||
}
|
||||
|
||||
} // namespace DesktopServices
|
||||
|
@ -3,31 +3,30 @@
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
class QFileInfo;
|
||||
|
||||
/**
|
||||
* This wraps around QDesktopServices and adds workarounds where needed
|
||||
* Use this instead of QDesktopServices!
|
||||
*/
|
||||
namespace DesktopServices {
|
||||
/**
|
||||
* Open a file in whatever application is applicable
|
||||
* Open a path in whatever application is applicable.
|
||||
* @param ensureFolderPathExists Make sure the path exists
|
||||
*/
|
||||
bool openFile(const QString& path);
|
||||
bool openPath(const QFileInfo& path, bool ensureFolderPathExists = false);
|
||||
|
||||
/**
|
||||
* Open a file in the specified application
|
||||
* Open a path in whatever application is applicable.
|
||||
* @param ensureFolderPathExists Make sure the path exists
|
||||
*/
|
||||
bool openFile(const QString& application, const QString& path, const QString& workingDirectory = QString(), qint64* pid = 0);
|
||||
bool openPath(const QString& path, bool ensureFolderPathExists = false);
|
||||
|
||||
/**
|
||||
* Run an application
|
||||
*/
|
||||
bool run(const QString& application, const QStringList& args, const QString& workingDirectory = QString(), qint64* pid = 0);
|
||||
|
||||
/**
|
||||
* Open a directory
|
||||
*/
|
||||
bool openDirectory(const QString& path, bool ensureExists = false);
|
||||
|
||||
/**
|
||||
* Open the URL, most likely in a browser. Maybe.
|
||||
*/
|
||||
@ -42,9 +41,4 @@ bool isFlatpak();
|
||||
* Determine whether the launcher is running in a Snap environment
|
||||
*/
|
||||
bool isSnap();
|
||||
|
||||
/**
|
||||
* Determine whether the launcher is running in a sandboxed (Flatpak or Snap) environment
|
||||
*/
|
||||
bool isSandbox();
|
||||
} // namespace DesktopServices
|
||||
|
@ -1,4 +1,37 @@
|
||||
// Licensed under the Apache-2.0 license. See README.md for details.
|
||||
// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (c) 2024 TheKodeToad <TheKodeToad@proton.me>
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This file incorporates work covered by the following copyright and
|
||||
* permission notice:
|
||||
*
|
||||
* Copyright 2013-2021 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
@ -8,12 +41,12 @@
|
||||
|
||||
class Exception : public std::exception {
|
||||
public:
|
||||
Exception(const QString& message) : std::exception(), m_message(message) { qCritical() << "Exception:" << message; }
|
||||
Exception(const Exception& other) : std::exception(), m_message(other.cause()) {}
|
||||
Exception(const QString& message) : std::exception(), m_message(message.toUtf8()) { qCritical() << "Exception:" << message; }
|
||||
Exception(const Exception& other) : std::exception(), m_message(other.m_message) {}
|
||||
virtual ~Exception() noexcept {}
|
||||
const char* what() const noexcept { return m_message.toLatin1().constData(); }
|
||||
QString cause() const { return m_message; }
|
||||
const char* what() const noexcept { return m_message.constData(); }
|
||||
QString cause() const { return QString::fromUtf8(m_message); }
|
||||
|
||||
private:
|
||||
QString m_message;
|
||||
QByteArray m_message;
|
||||
};
|
||||
|
@ -272,15 +272,19 @@ bool ensureFilePathExists(QString filenamepath)
|
||||
return success;
|
||||
}
|
||||
|
||||
bool ensureFolderPathExists(QString foldernamepath)
|
||||
bool ensureFolderPathExists(const QFileInfo folderPath)
|
||||
{
|
||||
QFileInfo a(foldernamepath);
|
||||
QDir dir;
|
||||
QString ensuredPath = a.filePath();
|
||||
QString ensuredPath = folderPath.filePath();
|
||||
bool success = dir.mkpath(ensuredPath);
|
||||
return success;
|
||||
}
|
||||
|
||||
bool ensureFolderPathExists(const QString folderPathName)
|
||||
{
|
||||
return ensureFolderPathExists(QFileInfo(folderPathName));
|
||||
}
|
||||
|
||||
bool copyFileAttributes(QString src, QString dst)
|
||||
{
|
||||
#ifdef Q_OS_WIN32
|
||||
@ -797,15 +801,24 @@ QString NormalizePath(QString path)
|
||||
}
|
||||
}
|
||||
|
||||
QString badFilenameChars = "\"\\/?<>:;*|!+\r\n";
|
||||
static const QString BAD_PATH_CHARS = "\"?<>:;*|!+\r\n";
|
||||
static const QString BAD_FILENAME_CHARS = BAD_PATH_CHARS + "\\/";
|
||||
|
||||
QString RemoveInvalidFilenameChars(QString string, QChar replaceWith)
|
||||
{
|
||||
for (int i = 0; i < string.length(); i++) {
|
||||
if (badFilenameChars.contains(string[i])) {
|
||||
for (int i = 0; i < string.length(); i++)
|
||||
if (string.at(i) < ' ' || BAD_FILENAME_CHARS.contains(string.at(i)))
|
||||
string[i] = replaceWith;
|
||||
}
|
||||
}
|
||||
|
||||
return string;
|
||||
}
|
||||
|
||||
QString RemoveInvalidPathChars(QString string, QChar replaceWith)
|
||||
{
|
||||
for (int i = 0; i < string.length(); i++)
|
||||
if (string.at(i) < ' ' || BAD_PATH_CHARS.contains(string.at(i)))
|
||||
string[i] = replaceWith;
|
||||
|
||||
return string;
|
||||
}
|
||||
|
||||
@ -1581,4 +1594,44 @@ uintmax_t hardLinkCount(const QString& path)
|
||||
return count;
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
// returns 8.3 file format from long path
|
||||
QString shortPathName(const QString& file)
|
||||
{
|
||||
auto input = file.toStdWString();
|
||||
std::wstring output;
|
||||
long length = GetShortPathNameW(input.c_str(), NULL, 0);
|
||||
if (length == 0)
|
||||
return {};
|
||||
// NOTE: this resizing might seem weird...
|
||||
// when GetShortPathNameW fails, it returns length including null character
|
||||
// when it succeeds, it returns length excluding null character
|
||||
// See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa364989(v=vs.85).aspx
|
||||
output.resize(length);
|
||||
if (GetShortPathNameW(input.c_str(), (LPWSTR)output.c_str(), length) == 0)
|
||||
return {};
|
||||
output.resize(length - 1);
|
||||
QString ret = QString::fromStdWString(output);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// if the string survives roundtrip through local 8bit encoding...
|
||||
bool fitsInLocal8bit(const QString& string)
|
||||
{
|
||||
return string == QString::fromLocal8Bit(string.toLocal8Bit());
|
||||
}
|
||||
|
||||
QString getPathNameInLocal8bit(const QString& file)
|
||||
{
|
||||
if (!fitsInLocal8bit(file)) {
|
||||
auto path = shortPathName(file);
|
||||
if (!path.isEmpty()) {
|
||||
return path;
|
||||
}
|
||||
// in case shortPathName fails just return the path as is
|
||||
}
|
||||
return file;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace FS
|
||||
|
@ -91,7 +91,13 @@ bool ensureFilePathExists(QString filenamepath);
|
||||
* Creates all the folders in a path for the specified path
|
||||
* last segment of the path is treated as a folder name and is created!
|
||||
*/
|
||||
bool ensureFolderPathExists(QString filenamepath);
|
||||
bool ensureFolderPathExists(const QFileInfo folderPath);
|
||||
|
||||
/**
|
||||
* Creates all the folders in a path for the specified path
|
||||
* last segment of the path is treated as a folder name and is created!
|
||||
*/
|
||||
bool ensureFolderPathExists(const QString folderPathName);
|
||||
|
||||
/**
|
||||
* @brief Copies a directory and it's contents from src to dest
|
||||
@ -336,6 +342,8 @@ QString NormalizePath(QString path);
|
||||
|
||||
QString RemoveInvalidFilenameChars(QString string, QChar replaceWith = '-');
|
||||
|
||||
QString RemoveInvalidPathChars(QString string, QChar replaceWith = '-');
|
||||
|
||||
QString DirNameFromString(QString string, QString inDir = ".");
|
||||
|
||||
/// Checks if the a given Path contains "!"
|
||||
@ -545,4 +553,8 @@ bool canLink(const QString& src, const QString& dst);
|
||||
|
||||
uintmax_t hardLinkCount(const QString& path);
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
QString getPathNameInLocal8bit(const QString& file);
|
||||
#endif
|
||||
|
||||
} // namespace FS
|
||||
|
@ -43,10 +43,10 @@ void InstanceCopyTask::executeTask()
|
||||
QFileInfo dotMCDir(FS::PathCombine(m_stagingPath, ".minecraft"));
|
||||
|
||||
QString staging_mc_dir;
|
||||
if (mcDir.exists() && !dotMCDir.exists())
|
||||
staging_mc_dir = mcDir.filePath();
|
||||
else
|
||||
if (dotMCDir.exists() && !mcDir.exists())
|
||||
staging_mc_dir = dotMCDir.filePath();
|
||||
else
|
||||
staging_mc_dir = mcDir.filePath();
|
||||
|
||||
FS::copy savesCopy(FS::PathCombine(m_origInstance->gameRoot(), "saves"), FS::PathCombine(staging_mc_dir, "saves"));
|
||||
savesCopy.followSymlinks(true);
|
||||
@ -142,9 +142,8 @@ void InstanceCopyTask::copyFinished()
|
||||
if (!m_keepPlaytime) {
|
||||
inst->resetTimePlayed();
|
||||
}
|
||||
if (m_useLinks)
|
||||
inst->addLinkedInstanceId(m_origInstance->id());
|
||||
if (m_useLinks) {
|
||||
inst->addLinkedInstanceId(m_origInstance->id());
|
||||
auto allowed_symlinks_file = QFileInfo(FS::PathCombine(inst->gameRoot(), "allowed_symlinks.txt"));
|
||||
|
||||
QByteArray allowed_symlinks;
|
||||
|
@ -3,8 +3,6 @@
|
||||
#include <QDebug>
|
||||
#include <QFile>
|
||||
|
||||
InstanceCreationTask::InstanceCreationTask() = default;
|
||||
|
||||
void InstanceCreationTask::executeTask()
|
||||
{
|
||||
setAbortable(true);
|
||||
|
@ -6,7 +6,7 @@
|
||||
class InstanceCreationTask : public InstanceTask {
|
||||
Q_OBJECT
|
||||
public:
|
||||
InstanceCreationTask();
|
||||
InstanceCreationTask() = default;
|
||||
virtual ~InstanceCreationTask() = default;
|
||||
|
||||
protected:
|
||||
|
@ -164,8 +164,8 @@ void InstanceImportTask::processZipPack()
|
||||
} else if (technicFound) {
|
||||
// process as Technic pack
|
||||
qDebug() << "Technic:" << technicFound;
|
||||
extractDir.mkpath(".minecraft");
|
||||
extractDir.cd(".minecraft");
|
||||
extractDir.mkpath("minecraft");
|
||||
extractDir.cd("minecraft");
|
||||
m_modpackType = ModpackType::Technic;
|
||||
} else {
|
||||
QStringList paths_to_ignore{ "overrides/" };
|
||||
|
@ -47,9 +47,6 @@
|
||||
#include <optional>
|
||||
|
||||
class QuaZip;
|
||||
namespace Flame {
|
||||
class FileResolvingTask;
|
||||
}
|
||||
|
||||
class InstanceImportTask : public InstanceTask {
|
||||
Q_OBJECT
|
||||
@ -79,7 +76,6 @@ class InstanceImportTask : public InstanceTask {
|
||||
|
||||
private: /* data */
|
||||
NetJob::Ptr m_filesNetJob;
|
||||
shared_qobject_ptr<Flame::FileResolvingTask> m_modIdResolver;
|
||||
QUrl m_sourceUrl;
|
||||
QString m_archivePath;
|
||||
bool m_downloadRequired = false;
|
||||
|
@ -38,6 +38,7 @@
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QFileSystemWatcher>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
@ -847,14 +848,16 @@ class InstanceStaging : public Task {
|
||||
const unsigned maxBackoff = 16;
|
||||
|
||||
public:
|
||||
InstanceStaging(InstanceList* parent, InstanceTask* child, QString stagingPath, InstanceName const& instanceName, QString groupName)
|
||||
: m_parent(parent)
|
||||
, backoff(minBackoff, maxBackoff)
|
||||
, m_stagingPath(std::move(stagingPath))
|
||||
, m_instance_name(std::move(instanceName))
|
||||
, m_groupName(std::move(groupName))
|
||||
InstanceStaging(InstanceList* parent, InstanceTask* child, SettingsObjectPtr settings)
|
||||
: m_parent(parent), backoff(minBackoff, maxBackoff)
|
||||
{
|
||||
m_stagingPath = parent->getStagedInstancePath();
|
||||
|
||||
m_child.reset(child);
|
||||
|
||||
m_child->setStagingPath(m_stagingPath);
|
||||
m_child->setParentSettings(std::move(settings));
|
||||
|
||||
connect(child, &Task::succeeded, this, &InstanceStaging::childSucceeded);
|
||||
connect(child, &Task::failed, this, &InstanceStaging::childFailed);
|
||||
connect(child, &Task::aborted, this, &InstanceStaging::childAborted);
|
||||
@ -866,7 +869,7 @@ class InstanceStaging : public Task {
|
||||
connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceeded);
|
||||
}
|
||||
|
||||
virtual ~InstanceStaging(){};
|
||||
virtual ~InstanceStaging() {}
|
||||
|
||||
// FIXME/TODO: add ability to abort during instance commit retries
|
||||
bool abort() override
|
||||
@ -881,14 +884,22 @@ class InstanceStaging : public Task {
|
||||
bool canAbort() const override { return (m_child && m_child->canAbort()); }
|
||||
|
||||
protected:
|
||||
virtual void executeTask() override { m_child->start(); }
|
||||
virtual void executeTask() override
|
||||
{
|
||||
if (m_stagingPath.isNull()) {
|
||||
emitFailed(tr("Could not create staging folder"));
|
||||
return;
|
||||
}
|
||||
|
||||
m_child->start();
|
||||
}
|
||||
QStringList warnings() const override { return m_child->warnings(); }
|
||||
|
||||
private slots:
|
||||
void childSucceeded()
|
||||
{
|
||||
unsigned sleepTime = backoff();
|
||||
if (m_parent->commitStagedInstance(m_stagingPath, m_instance_name, m_groupName, *m_child.get())) {
|
||||
if (m_parent->commitStagedInstance(m_stagingPath, *m_child.get(), m_child->group(), *m_child.get())) {
|
||||
emitSucceeded();
|
||||
return;
|
||||
}
|
||||
@ -897,7 +908,7 @@ class InstanceStaging : public Task {
|
||||
emitFailed(tr("Failed to commit instance, even after multiple retries. It is being blocked by something."));
|
||||
return;
|
||||
}
|
||||
qDebug() << "Failed to commit instance" << m_instance_name.name() << "Initiating backoff:" << sleepTime;
|
||||
qDebug() << "Failed to commit instance" << m_child->name() << "Initiating backoff:" << sleepTime;
|
||||
m_backoffTimer.start(sleepTime * 500);
|
||||
}
|
||||
void childFailed(const QString& reason)
|
||||
@ -906,7 +917,11 @@ class InstanceStaging : public Task {
|
||||
emitFailed(reason);
|
||||
}
|
||||
|
||||
void childAborted() { emitAborted(); }
|
||||
void childAborted()
|
||||
{
|
||||
m_parent->destroyStagingPath(m_stagingPath);
|
||||
emitAborted();
|
||||
}
|
||||
|
||||
private:
|
||||
InstanceList* m_parent;
|
||||
@ -918,34 +933,35 @@ class InstanceStaging : public Task {
|
||||
ExponentialSeries backoff;
|
||||
QString m_stagingPath;
|
||||
unique_qobject_ptr<InstanceTask> m_child;
|
||||
InstanceName m_instance_name;
|
||||
QString m_groupName;
|
||||
QTimer m_backoffTimer;
|
||||
};
|
||||
|
||||
Task* InstanceList::wrapInstanceTask(InstanceTask* task)
|
||||
{
|
||||
auto stagingPath = getStagedInstancePath();
|
||||
task->setStagingPath(stagingPath);
|
||||
task->setParentSettings(m_globalSettings);
|
||||
return new InstanceStaging(this, task, stagingPath, *task, task->group());
|
||||
return new InstanceStaging(this, task, m_globalSettings);
|
||||
}
|
||||
|
||||
QString InstanceList::getStagedInstancePath()
|
||||
{
|
||||
QString key = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
QString tempDir = ".LAUNCHER_TEMP/";
|
||||
QString relPath = FS::PathCombine(tempDir, key);
|
||||
QDir rootPath(m_instDir);
|
||||
auto path = FS::PathCombine(m_instDir, relPath);
|
||||
if (!rootPath.mkpath(relPath)) {
|
||||
return QString();
|
||||
}
|
||||
const QString tempRoot = FS::PathCombine(m_instDir, ".tmp");
|
||||
|
||||
QString result;
|
||||
int tries = 0;
|
||||
|
||||
do {
|
||||
if (++tries > 256)
|
||||
return {};
|
||||
|
||||
const QString key = QUuid::createUuid().toString(QUuid::Id128).left(6);
|
||||
result = FS::PathCombine(tempRoot, key);
|
||||
} while (QFileInfo::exists(result));
|
||||
|
||||
if (!QDir::current().mkpath(result))
|
||||
return {};
|
||||
#ifdef Q_OS_WIN32
|
||||
auto tempPath = FS::PathCombine(m_instDir, tempDir);
|
||||
SetFileAttributesA(tempPath.toStdString().c_str(), FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED);
|
||||
SetFileAttributesA(tempRoot.toStdString().c_str(), FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED);
|
||||
#endif
|
||||
return path;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool InstanceList::commitStagedInstance(const QString& path,
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
#include "ui/dialogs/CustomMessageBox.h"
|
||||
|
||||
#include <QPushButton>
|
||||
|
||||
InstanceNameChange askForChangingInstanceName(QWidget* parent, const QString& old_name, const QString& new_name)
|
||||
{
|
||||
auto dialog =
|
||||
@ -27,16 +29,15 @@ ShouldUpdate askIfShouldUpdate(QWidget* parent, QString original_version_name)
|
||||
"separate instance, or update the existing one?\n\nNOTE: Make sure you made a backup of your important instance data before "
|
||||
"updating, as worlds can be corrupted and some configuration may be lost (due to pack overrides).")
|
||||
.arg(original_version_name),
|
||||
QMessageBox::Information, QMessageBox::Ok | QMessageBox::Reset | QMessageBox::Abort);
|
||||
info->setButtonText(QMessageBox::Ok, QObject::tr("Update existing instance"));
|
||||
info->setButtonText(QMessageBox::Abort, QObject::tr("Create new instance"));
|
||||
info->setButtonText(QMessageBox::Reset, QObject::tr("Cancel"));
|
||||
QMessageBox::Information, QMessageBox::Cancel);
|
||||
QAbstractButton* update = info->addButton(QObject::tr("Update existing instance"), QMessageBox::AcceptRole);
|
||||
QAbstractButton* skip = info->addButton(QObject::tr("Create new instance"), QMessageBox::ResetRole);
|
||||
|
||||
info->exec();
|
||||
|
||||
if (info->clickedButton() == info->button(QMessageBox::Ok))
|
||||
if (info->clickedButton() == update)
|
||||
return ShouldUpdate::Update;
|
||||
if (info->clickedButton() == info->button(QMessageBox::Abort))
|
||||
if (info->clickedButton() == skip)
|
||||
return ShouldUpdate::SkipUpdating;
|
||||
return ShouldUpdate::Cancel;
|
||||
}
|
||||
|
@ -42,7 +42,6 @@
|
||||
#include "ui/InstanceWindow.h"
|
||||
#include "ui/MainWindow.h"
|
||||
#include "ui/dialogs/CustomMessageBox.h"
|
||||
#include "ui/dialogs/EditAccountDialog.h"
|
||||
#include "ui/dialogs/ProfileSelectDialog.h"
|
||||
#include "ui/dialogs/ProfileSetupDialog.h"
|
||||
#include "ui/dialogs/ProgressDialog.h"
|
||||
@ -58,7 +57,6 @@
|
||||
#include "BuildConfig.h"
|
||||
#include "JavaCommon.h"
|
||||
#include "launch/steps/TextPrint.h"
|
||||
#include "minecraft/auth/AccountTask.h"
|
||||
#include "tasks/Task.h"
|
||||
|
||||
LaunchController::LaunchController(QObject* parent) : Task(parent) {}
|
||||
@ -144,6 +142,12 @@ void LaunchController::login()
|
||||
bool tryagain = true;
|
||||
unsigned int tries = 0;
|
||||
|
||||
if (m_accountToUse->accountType() != AccountType::Offline && m_accountToUse->accountState() == AccountState::Offline) {
|
||||
// Force account refresh on the account used to launch the instance updating the AccountState
|
||||
// only on first try and if it is not meant to be offline
|
||||
auto accounts = APPLICATION->accounts();
|
||||
accounts->requestRefresh(m_accountToUse->internalId());
|
||||
}
|
||||
while (tryagain) {
|
||||
if (tries > 0 && tries % 3 == 0) {
|
||||
auto result =
|
||||
@ -250,12 +254,6 @@ void LaunchController::login()
|
||||
progDialog.execWithTask(task.get());
|
||||
continue;
|
||||
}
|
||||
// FIXME: this is missing - the meaning is that the account is queued for refresh and we should wait for that
|
||||
/*
|
||||
case AccountState::Queued: {
|
||||
return;
|
||||
}
|
||||
*/
|
||||
case AccountState::Expired: {
|
||||
auto errorString = tr("The account has expired and needs to be logged into manually again.");
|
||||
QMessageBox::warning(m_parentWidget, tr("Account refresh failed"), errorString, QMessageBox::StandardButton::Ok,
|
||||
|
@ -119,6 +119,7 @@ bool compressDirFiles(QuaZip* zip, QString dir, QFileInfoList files, bool follow
|
||||
bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks)
|
||||
{
|
||||
QuaZip zip(fileCompressed);
|
||||
zip.setUtf8Enabled(true);
|
||||
QDir().mkpath(QFileInfo(fileCompressed).absolutePath());
|
||||
if (!zip.open(QuaZip::mdCreate)) {
|
||||
QFile::remove(fileCompressed);
|
||||
@ -141,6 +142,7 @@ bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files,
|
||||
bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod*>& mods)
|
||||
{
|
||||
QuaZip zipOut(targetJarPath);
|
||||
zipOut.setUtf8Enabled(true);
|
||||
if (!zipOut.open(QuaZip::mdCreate)) {
|
||||
QFile::remove(targetJarPath);
|
||||
qCritical() << "Failed to open the minecraft.jar for modding";
|
||||
@ -286,10 +288,13 @@ std::optional<QStringList> extractSubDir(QuaZip* zip, const QString& subdir, con
|
||||
|
||||
do {
|
||||
QString file_name = zip->getCurrentFileName();
|
||||
#ifdef Q_OS_WIN
|
||||
file_name = FS::RemoveInvalidPathChars(file_name);
|
||||
#endif
|
||||
if (!file_name.startsWith(subdir))
|
||||
continue;
|
||||
|
||||
auto relative_file_name = QDir::fromNativeSeparators(file_name.remove(0, subdir.size()));
|
||||
auto relative_file_name = QDir::fromNativeSeparators(file_name.mid(subdir.size()));
|
||||
auto original_name = relative_file_name;
|
||||
|
||||
// Fix subdirs/files ending with a / getting transformed into absolute paths
|
||||
@ -463,7 +468,7 @@ auto ExportToZipTask::exportZip() -> ZipResult
|
||||
|
||||
auto absolute = file.absoluteFilePath();
|
||||
auto relative = m_dir.relativeFilePath(absolute);
|
||||
setStatus("Compresing: " + relative);
|
||||
setStatus("Compressing: " + relative);
|
||||
setProgress(m_progress + 1, m_progressTotal);
|
||||
if (m_follow_symlinks) {
|
||||
if (file.isSymLink())
|
||||
|
@ -154,7 +154,12 @@ bool collectFileListRecursively(const QString& rootDir, const QString& subDir, Q
|
||||
#if defined(LAUNCHER_APPLICATION)
|
||||
class ExportToZipTask : public Task {
|
||||
public:
|
||||
ExportToZipTask(QString outputPath, QDir dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false)
|
||||
ExportToZipTask(QString outputPath,
|
||||
QDir dir,
|
||||
QFileInfoList files,
|
||||
QString destinationPrefix = "",
|
||||
bool followSymlinks = false,
|
||||
bool utf8Enabled = false)
|
||||
: m_output_path(outputPath)
|
||||
, m_output(outputPath)
|
||||
, m_dir(dir)
|
||||
@ -163,9 +168,15 @@ class ExportToZipTask : public Task {
|
||||
, m_follow_symlinks(followSymlinks)
|
||||
{
|
||||
setAbortable(true);
|
||||
m_output.setUtf8Enabled(utf8Enabled);
|
||||
};
|
||||
ExportToZipTask(QString outputPath, QString dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false)
|
||||
: ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks){};
|
||||
ExportToZipTask(QString outputPath,
|
||||
QString dir,
|
||||
QFileInfoList files,
|
||||
QString destinationPrefix = "",
|
||||
bool followSymlinks = false,
|
||||
bool utf8Enabled = false)
|
||||
: ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks, utf8Enabled){};
|
||||
|
||||
virtual ~ExportToZipTask() = default;
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
set(CMAKE_MODULE_PATH "@CMAKE_MODULE_PATH@")
|
||||
|
||||
file(GLOB_RECURSE QTPLUGINS "${CMAKE_INSTALL_PREFIX}/@PLUGIN_DEST_DIR@/*@CMAKE_SHARED_LIBRARY_SUFFIX@")
|
||||
function(gp_resolved_file_type_override resolved_file type_var)
|
||||
if(resolved_file MATCHES "^/(usr/)?lib/libQt")
|
||||
|
@ -55,6 +55,9 @@ void JavaChecker::performCheck()
|
||||
qDebug() << "Java checker library could not be found. Please check your installation.";
|
||||
return;
|
||||
}
|
||||
#ifdef Q_OS_WIN
|
||||
checkerJar = FS::getPathNameInLocal8bit(checkerJar);
|
||||
#endif
|
||||
|
||||
QStringList args;
|
||||
|
||||
|
@ -362,6 +362,12 @@ QList<QString> JavaUtils::FindJavaPaths()
|
||||
javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java");
|
||||
javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/java");
|
||||
}
|
||||
|
||||
auto home = qEnvironmentVariable("HOME");
|
||||
|
||||
// javas downloaded by sdkman
|
||||
javas.append(FS::PathCombine(home, ".sdkman/candidates/java"));
|
||||
|
||||
javas.append(getMinecraftJavaBundle());
|
||||
javas = addJavasFromEnv(javas);
|
||||
javas.removeDuplicates();
|
||||
@ -413,6 +419,8 @@ QList<QString> JavaUtils::FindJavaPaths()
|
||||
scanJavaDirs(FS::PathCombine(home, ".jdks"));
|
||||
// javas downloaded by sdkman
|
||||
scanJavaDirs(FS::PathCombine(home, ".sdkman/candidates/java"));
|
||||
// javas downloaded by gradle (toolchains)
|
||||
scanJavaDirs(FS::PathCombine(home, ".gradle/jdks"));
|
||||
|
||||
javas.append(getMinecraftJavaBundle());
|
||||
javas = addJavasFromEnv(javas);
|
||||
@ -439,26 +447,25 @@ QString JavaUtils::getJavaCheckPath()
|
||||
|
||||
QStringList getMinecraftJavaBundle()
|
||||
{
|
||||
QString partialPath;
|
||||
QString executable = "java";
|
||||
QStringList processpaths;
|
||||
#if defined(Q_OS_OSX)
|
||||
partialPath = FS::PathCombine(QDir::homePath(), "Library/Application Support");
|
||||
processpaths << FS::PathCombine(QDir::homePath(), FS::PathCombine("Library", "Application Support", "minecraft", "runtime"));
|
||||
#elif defined(Q_OS_WIN32)
|
||||
partialPath = QProcessEnvironment::systemEnvironment().value("LOCALAPPDATA", "");
|
||||
executable += "w.exe";
|
||||
|
||||
auto appDataPath = QProcessEnvironment::systemEnvironment().value("APPDATA", "");
|
||||
processpaths << FS::PathCombine(QFileInfo(appDataPath).absoluteFilePath(), ".minecraft", "runtime");
|
||||
|
||||
// add the microsoft store version of the launcher to the search. the current path is:
|
||||
// C:\Users\USERNAME\AppData\Local\Packages\Microsoft.4297127D64EC6_8wekyb3d8bbwe\LocalCache\Local\runtime
|
||||
auto localAppDataPath = QProcessEnvironment::systemEnvironment().value("LOCALAPPDATA", "");
|
||||
auto minecraftMSStorePath =
|
||||
FS::PathCombine(QFileInfo(partialPath).absolutePath(), "Local", "Packages", "Microsoft.4297127D64EC6_8wekyb3d8bbwe");
|
||||
minecraftMSStorePath = FS::PathCombine(minecraftMSStorePath, "LocalCache", "Local", "runtime");
|
||||
processpaths << minecraftMSStorePath;
|
||||
FS::PathCombine(QFileInfo(localAppDataPath).absoluteFilePath(), "Packages", "Microsoft.4297127D64EC6_8wekyb3d8bbwe");
|
||||
processpaths << FS::PathCombine(minecraftMSStorePath, "LocalCache", "Local", "runtime");
|
||||
#else
|
||||
partialPath = QDir::homePath();
|
||||
processpaths << FS::PathCombine(QDir::homePath(), ".minecraft", "runtime");
|
||||
#endif
|
||||
auto minecraftDataPath = FS::PathCombine(partialPath, ".minecraft", "runtime");
|
||||
processpaths << minecraftDataPath;
|
||||
|
||||
QStringList javas;
|
||||
while (!processpaths.isEmpty()) {
|
||||
|
@ -51,6 +51,7 @@
|
||||
#include "net/Download.h"
|
||||
|
||||
#include "Application.h"
|
||||
#include "net/NetRequest.h"
|
||||
|
||||
namespace {
|
||||
QSet<QString> collectPathsFromDir(QString dirPath)
|
||||
@ -276,7 +277,7 @@ bool reconstructAssets(QString assetsId, QString resourcesFolder)
|
||||
|
||||
} // namespace AssetsUtils
|
||||
|
||||
NetAction::Ptr AssetObject::getDownloadAction()
|
||||
Net::NetRequest::Ptr AssetObject::getDownloadAction()
|
||||
{
|
||||
QFileInfo objectFile(getLocalPath());
|
||||
if ((!objectFile.isFile()) || (objectFile.size() != size)) {
|
||||
|
@ -17,14 +17,14 @@
|
||||
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
#include "net/NetAction.h"
|
||||
#include "net/NetJob.h"
|
||||
#include "net/NetRequest.h"
|
||||
|
||||
struct AssetObject {
|
||||
QString getRelPath();
|
||||
QUrl getUrl();
|
||||
QString getLocalPath();
|
||||
NetAction::Ptr getDownloadAction();
|
||||
Net::NetRequest::Ptr getDownloadAction();
|
||||
|
||||
QString hash;
|
||||
qint64 size;
|
||||
|
@ -35,6 +35,7 @@
|
||||
|
||||
#include "Library.h"
|
||||
#include "MinecraftInstance.h"
|
||||
#include "net/NetRequest.h"
|
||||
|
||||
#include <BuildConfig.h>
|
||||
#include <FileSystem.h>
|
||||
@ -74,12 +75,12 @@ void Library::getApplicableFiles(const RuntimeContext& runtimeContext,
|
||||
}
|
||||
}
|
||||
|
||||
QList<NetAction::Ptr> Library::getDownloads(const RuntimeContext& runtimeContext,
|
||||
class HttpMetaCache* cache,
|
||||
QStringList& failedLocalFiles,
|
||||
const QString& overridePath) const
|
||||
QList<Net::NetRequest::Ptr> Library::getDownloads(const RuntimeContext& runtimeContext,
|
||||
class HttpMetaCache* cache,
|
||||
QStringList& failedLocalFiles,
|
||||
const QString& overridePath) const
|
||||
{
|
||||
QList<NetAction::Ptr> out;
|
||||
QList<Net::NetRequest::Ptr> out;
|
||||
bool stale = isAlwaysStale();
|
||||
bool local = isLocal();
|
||||
|
||||
|
@ -34,7 +34,6 @@
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <net/NetAction.h>
|
||||
#include <QDir>
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
@ -48,6 +47,7 @@
|
||||
#include "MojangDownloadInfo.h"
|
||||
#include "Rule.h"
|
||||
#include "RuntimeContext.h"
|
||||
#include "net/NetRequest.h"
|
||||
|
||||
class Library;
|
||||
class MinecraftInstance;
|
||||
@ -144,10 +144,10 @@ class Library {
|
||||
bool isForge() const;
|
||||
|
||||
// Get a list of downloads for this library
|
||||
QList<NetAction::Ptr> getDownloads(const RuntimeContext& runtimeContext,
|
||||
class HttpMetaCache* cache,
|
||||
QStringList& failedLocalFiles,
|
||||
const QString& overridePath) const;
|
||||
QList<Net::NetRequest::Ptr> getDownloads(const RuntimeContext& runtimeContext,
|
||||
class HttpMetaCache* cache,
|
||||
QStringList& failedLocalFiles,
|
||||
const QString& overridePath) const;
|
||||
|
||||
QString getCompatibleNative(const RuntimeContext& runtimeContext) const;
|
||||
|
||||
|
@ -173,11 +173,12 @@ void MinecraftInstance::loadSpecificSettings()
|
||||
m_settings->registerOverride(global_settings->getSetting("UseNativeGLFW"), nativeLibraryWorkaroundsOverride);
|
||||
m_settings->registerOverride(global_settings->getSetting("CustomGLFWPath"), nativeLibraryWorkaroundsOverride);
|
||||
|
||||
// Peformance related options
|
||||
// Performance related options
|
||||
auto performanceOverride = m_settings->registerSetting("OverridePerformance", false);
|
||||
m_settings->registerOverride(global_settings->getSetting("EnableFeralGamemode"), performanceOverride);
|
||||
m_settings->registerOverride(global_settings->getSetting("EnableMangoHud"), performanceOverride);
|
||||
m_settings->registerOverride(global_settings->getSetting("UseDiscreteGpu"), performanceOverride);
|
||||
m_settings->registerOverride(global_settings->getSetting("UseZink"), performanceOverride);
|
||||
|
||||
// Miscellaneous
|
||||
auto miscellaneousOverride = m_settings->registerSetting("OverrideMiscellaneous", false);
|
||||
@ -292,10 +293,10 @@ QString MinecraftInstance::gameRoot() const
|
||||
QFileInfo mcDir(FS::PathCombine(instanceRoot(), "minecraft"));
|
||||
QFileInfo dotMCDir(FS::PathCombine(instanceRoot(), ".minecraft"));
|
||||
|
||||
if (mcDir.exists() && !dotMCDir.exists())
|
||||
return mcDir.filePath();
|
||||
else
|
||||
if (dotMCDir.exists() && !mcDir.exists())
|
||||
return dotMCDir.filePath();
|
||||
else
|
||||
return mcDir.filePath();
|
||||
}
|
||||
|
||||
QString MinecraftInstance::binRoot() const
|
||||
@ -594,9 +595,6 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment()
|
||||
QStringList preloadList;
|
||||
if (auto value = env.value("LD_PRELOAD"); !value.isEmpty())
|
||||
preloadList = value.split(QLatin1String(":"));
|
||||
QStringList libPaths;
|
||||
if (auto value = env.value("LD_LIBRARY_PATH"); !value.isEmpty())
|
||||
libPaths = value.split(QLatin1String(":"));
|
||||
|
||||
auto mangoHudLibString = MangoHud::getLibraryString();
|
||||
if (!mangoHudLibString.isEmpty()) {
|
||||
@ -604,18 +602,16 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment()
|
||||
QString libPath = mangoHudLib.absolutePath();
|
||||
auto appendLib = [libPath, &preloadList](QString fileName) {
|
||||
if (QFileInfo(FS::PathCombine(libPath, fileName)).exists())
|
||||
preloadList << fileName;
|
||||
preloadList << FS::PathCombine(libPath, fileName);
|
||||
};
|
||||
|
||||
// dlsym variant is only needed for OpenGL and not included in the vulkan layer
|
||||
appendLib("libMangoHud_dlsym.so");
|
||||
appendLib("libMangoHud_opengl.so");
|
||||
appendLib(mangoHudLib.fileName());
|
||||
libPaths << libPath;
|
||||
}
|
||||
|
||||
env.insert("LD_PRELOAD", preloadList.join(QLatin1String(":")));
|
||||
env.insert("LD_LIBRARY_PATH", libPaths.join(QLatin1String(":")));
|
||||
env.insert("MANGOHUD", "1");
|
||||
}
|
||||
|
||||
@ -627,6 +623,13 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment()
|
||||
env.insert("__VK_LAYER_NV_optimus", "NVIDIA_only");
|
||||
env.insert("__GLX_VENDOR_LIBRARY_NAME", "nvidia");
|
||||
}
|
||||
|
||||
if (settings()->get("UseZink").toBool()) {
|
||||
// taken from https://wiki.archlinux.org/title/OpenGL#OpenGL_over_Vulkan_(Zink)
|
||||
env.insert("__GLX_VENDOR_LIBRARY_NAME", "mesa");
|
||||
env.insert("MESA_LOADER_DRIVER_OVERRIDE", "zink");
|
||||
env.insert("GALLIUM_DRIVER", "zink");
|
||||
}
|
||||
#endif
|
||||
return env;
|
||||
}
|
||||
@ -662,8 +665,12 @@ QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session, Mine
|
||||
}
|
||||
|
||||
if (serverToJoin && !serverToJoin->address.isEmpty()) {
|
||||
args_pattern += " --server " + serverToJoin->address;
|
||||
args_pattern += " --port " + QString::number(serverToJoin->port);
|
||||
if (profile->hasTrait("feature:is_quick_play_multiplayer")) {
|
||||
args_pattern += " --quickPlayMultiplayer " + serverToJoin->address + ':' + QString::number(serverToJoin->port);
|
||||
} else {
|
||||
args_pattern += " --server " + serverToJoin->address;
|
||||
args_pattern += " --port " + QString::number(serverToJoin->port);
|
||||
}
|
||||
}
|
||||
|
||||
QMap<QString, QString> token_mapping;
|
||||
|
@ -157,20 +157,6 @@ void MojangVersionFormat::readVersionProperties(const QJsonObject& in, VersionFi
|
||||
Bits::readString(in, "id", out->minecraftVersion);
|
||||
Bits::readString(in, "mainClass", out->mainClass);
|
||||
Bits::readString(in, "minecraftArguments", out->minecraftArguments);
|
||||
if (out->minecraftArguments.isEmpty()) {
|
||||
QString processArguments;
|
||||
Bits::readString(in, "processArguments", processArguments);
|
||||
QString toCompare = processArguments.toLower();
|
||||
if (toCompare == "legacy") {
|
||||
out->minecraftArguments = " ${auth_player_name} ${auth_session}";
|
||||
} else if (toCompare == "username_session") {
|
||||
out->minecraftArguments = "--username ${auth_player_name} --session ${auth_session}";
|
||||
} else if (toCompare == "username_session_version") {
|
||||
out->minecraftArguments = "--username ${auth_player_name} --session ${auth_session} --version ${profile_name}";
|
||||
} else if (!toCompare.isEmpty()) {
|
||||
out->addProblem(ProblemSeverity::Error, QObject::tr("processArguments is set to unknown value '%1'").arg(processArguments));
|
||||
}
|
||||
}
|
||||
Bits::readString(in, "type", out->type);
|
||||
|
||||
Bits::readString(in, "assets", out->assets);
|
||||
|
@ -42,7 +42,7 @@
|
||||
#include <QUuid>
|
||||
|
||||
namespace {
|
||||
void tokenToJSONV3(QJsonObject& parent, Katabasis::Token t, const char* tokenName)
|
||||
void tokenToJSONV3(QJsonObject& parent, Token t, const char* tokenName)
|
||||
{
|
||||
if (!t.persistent) {
|
||||
return;
|
||||
@ -74,9 +74,9 @@ void tokenToJSONV3(QJsonObject& parent, Katabasis::Token t, const char* tokenNam
|
||||
}
|
||||
}
|
||||
|
||||
Katabasis::Token tokenFromJSONV3(const QJsonObject& parent, const char* tokenName)
|
||||
Token tokenFromJSONV3(const QJsonObject& parent, const char* tokenName)
|
||||
{
|
||||
Katabasis::Token out;
|
||||
Token out;
|
||||
auto tokenObject = parent.value(tokenName).toObject();
|
||||
if (tokenObject.isEmpty()) {
|
||||
return out;
|
||||
@ -94,7 +94,7 @@ Katabasis::Token tokenFromJSONV3(const QJsonObject& parent, const char* tokenNam
|
||||
auto token = tokenObject.value("token");
|
||||
if (token.isString()) {
|
||||
out.token = token.toString();
|
||||
out.validity = Katabasis::Validity::Assumed;
|
||||
out.validity = Validity::Assumed;
|
||||
}
|
||||
|
||||
auto refresh_token = tokenObject.value("refresh_token");
|
||||
@ -241,13 +241,13 @@ MinecraftProfile profileFromJSONV3(const QJsonObject& parent, const char* tokenN
|
||||
}
|
||||
}
|
||||
}
|
||||
out.validity = Katabasis::Validity::Assumed;
|
||||
out.validity = Validity::Assumed;
|
||||
return out;
|
||||
}
|
||||
|
||||
void entitlementToJSONV3(QJsonObject& parent, MinecraftEntitlement p)
|
||||
{
|
||||
if (p.validity == Katabasis::Validity::None) {
|
||||
if (p.validity == Validity::None) {
|
||||
return;
|
||||
}
|
||||
QJsonObject out;
|
||||
@ -271,7 +271,7 @@ bool entitlementFromJSONV3(const QJsonObject& parent, MinecraftEntitlement& out)
|
||||
}
|
||||
out.canPlayMinecraft = canPlayMinecraftV.toBool(false);
|
||||
out.ownsMinecraft = ownsMinecraftV.toBool(false);
|
||||
out.validity = Katabasis::Validity::Assumed;
|
||||
out.validity = Validity::Assumed;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -313,10 +313,10 @@ bool AccountData::resumeStateFromV3(QJsonObject data)
|
||||
|
||||
minecraftProfile = profileFromJSONV3(data, "profile");
|
||||
if (!entitlementFromJSONV3(data, minecraftEntitlement)) {
|
||||
if (minecraftProfile.validity != Katabasis::Validity::None) {
|
||||
if (minecraftProfile.validity != Validity::None) {
|
||||
minecraftEntitlement.canPlayMinecraft = true;
|
||||
minecraftEntitlement.ownsMinecraft = true;
|
||||
minecraftEntitlement.validity = Katabasis::Validity::Assumed;
|
||||
minecraftEntitlement.validity = Validity::Assumed;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -34,12 +34,29 @@
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <katabasis/Bits.h>
|
||||
#include <QByteArray>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
#include <QVariantMap>
|
||||
|
||||
enum class Validity { None, Assumed, Certain };
|
||||
|
||||
struct Token {
|
||||
QDateTime issueInstant;
|
||||
QDateTime notAfter;
|
||||
QString token;
|
||||
QString refresh_token;
|
||||
QVariantMap extra;
|
||||
|
||||
Validity validity = Validity::None;
|
||||
bool persistent = true;
|
||||
};
|
||||
|
||||
struct Skin {
|
||||
QString id;
|
||||
QString url;
|
||||
@ -59,7 +76,7 @@ struct Cape {
|
||||
struct MinecraftEntitlement {
|
||||
bool ownsMinecraft = false;
|
||||
bool canPlayMinecraft = false;
|
||||
Katabasis::Validity validity = Katabasis::Validity::None;
|
||||
Validity validity = Validity::None;
|
||||
};
|
||||
|
||||
struct MinecraftProfile {
|
||||
@ -68,7 +85,7 @@ struct MinecraftProfile {
|
||||
Skin skin;
|
||||
QString currentCape;
|
||||
QMap<QString, Cape> capes;
|
||||
Katabasis::Validity validity = Katabasis::Validity::None;
|
||||
Validity validity = Validity::None;
|
||||
};
|
||||
|
||||
enum class AccountType { MSA, Offline };
|
||||
@ -93,15 +110,15 @@ struct AccountData {
|
||||
AccountType type = AccountType::MSA;
|
||||
|
||||
QString msaClientID;
|
||||
Katabasis::Token msaToken;
|
||||
Katabasis::Token userToken;
|
||||
Katabasis::Token xboxApiToken;
|
||||
Katabasis::Token mojangservicesToken;
|
||||
Token msaToken;
|
||||
Token userToken;
|
||||
Token xboxApiToken;
|
||||
Token mojangservicesToken;
|
||||
|
||||
Katabasis::Token yggdrasilToken;
|
||||
Token yggdrasilToken;
|
||||
MinecraftProfile minecraftProfile;
|
||||
MinecraftEntitlement minecraftEntitlement;
|
||||
Katabasis::Validity validity_ = Katabasis::Validity::None;
|
||||
Validity validity_ = Validity::None;
|
||||
|
||||
// runtime only information (not saved with the account)
|
||||
QString internalId;
|
||||
|
@ -35,7 +35,7 @@
|
||||
|
||||
#include "AccountList.h"
|
||||
#include "AccountData.h"
|
||||
#include "AccountTask.h"
|
||||
#include "tasks/Task.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
@ -52,8 +52,6 @@
|
||||
#include <FileSystem.h>
|
||||
#include <QSaveFile>
|
||||
|
||||
#include <chrono>
|
||||
|
||||
enum AccountListVersion { MojangMSA = 3 };
|
||||
|
||||
AccountList::AccountList(QObject* parent) : QAbstractListModel(parent)
|
||||
@ -641,8 +639,8 @@ void AccountList::tryNext()
|
||||
if (account->internalId() == accountId) {
|
||||
m_currentTask = account->refresh();
|
||||
if (m_currentTask) {
|
||||
connect(m_currentTask.get(), &AccountTask::succeeded, this, &AccountList::authSucceeded);
|
||||
connect(m_currentTask.get(), &AccountTask::failed, this, &AccountList::authFailed);
|
||||
connect(m_currentTask.get(), &Task::succeeded, this, &AccountList::authSucceeded);
|
||||
connect(m_currentTask.get(), &Task::failed, this, &AccountList::authFailed);
|
||||
m_currentTask->start();
|
||||
qDebug() << "RefreshSchedule: Processing account " << account->accountDisplayString() << " with internal ID "
|
||||
<< accountId;
|
||||
|
@ -36,6 +36,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "MinecraftAccount.h"
|
||||
#include "minecraft/auth/AuthFlow.h"
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QObject>
|
||||
@ -144,7 +145,7 @@ class AccountList : public QAbstractListModel {
|
||||
QList<QString> m_refreshQueue;
|
||||
QTimer* m_refreshTimer;
|
||||
QTimer* m_nextTimer;
|
||||
shared_qobject_ptr<AccountTask> m_currentTask;
|
||||
shared_qobject_ptr<AuthFlow> m_currentTask;
|
||||
|
||||
/*!
|
||||
* Called whenever the list changes.
|
||||
|
@ -1,134 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This file incorporates work covered by the following copyright and
|
||||
* permission notice:
|
||||
*
|
||||
* Copyright 2013-2021 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "AccountTask.h"
|
||||
#include "MinecraftAccount.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkReply>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
AccountTask::AccountTask(AccountData* data, QObject* parent) : Task(parent), m_data(data)
|
||||
{
|
||||
changeState(AccountTaskState::STATE_CREATED);
|
||||
}
|
||||
|
||||
QString AccountTask::getStateMessage() const
|
||||
{
|
||||
switch (m_taskState) {
|
||||
case AccountTaskState::STATE_CREATED:
|
||||
return "Waiting...";
|
||||
case AccountTaskState::STATE_WORKING:
|
||||
return tr("Sending request to auth servers...");
|
||||
case AccountTaskState::STATE_SUCCEEDED:
|
||||
return tr("Authentication task succeeded.");
|
||||
case AccountTaskState::STATE_OFFLINE:
|
||||
return tr("Failed to contact the authentication server.");
|
||||
case AccountTaskState::STATE_DISABLED:
|
||||
return tr("Client ID has changed. New session needs to be created.");
|
||||
case AccountTaskState::STATE_FAILED_SOFT:
|
||||
return tr("Encountered an error during authentication.");
|
||||
case AccountTaskState::STATE_FAILED_HARD:
|
||||
return tr("Failed to authenticate. The session has expired.");
|
||||
case AccountTaskState::STATE_FAILED_GONE:
|
||||
return tr("Failed to authenticate. The account no longer exists.");
|
||||
default:
|
||||
return tr("...");
|
||||
}
|
||||
}
|
||||
|
||||
bool AccountTask::changeState(AccountTaskState newState, QString reason)
|
||||
{
|
||||
m_taskState = newState;
|
||||
// FIXME: virtual method invoked in constructor.
|
||||
// We want that behavior, but maybe make it less weird?
|
||||
setStatus(getStateMessage());
|
||||
switch (newState) {
|
||||
case AccountTaskState::STATE_CREATED: {
|
||||
m_data->errorString.clear();
|
||||
return true;
|
||||
}
|
||||
case AccountTaskState::STATE_WORKING: {
|
||||
m_data->accountState = AccountState::Working;
|
||||
return true;
|
||||
}
|
||||
case AccountTaskState::STATE_SUCCEEDED: {
|
||||
m_data->accountState = AccountState::Online;
|
||||
emitSucceeded();
|
||||
return false;
|
||||
}
|
||||
case AccountTaskState::STATE_OFFLINE: {
|
||||
m_data->errorString = reason;
|
||||
m_data->accountState = AccountState::Offline;
|
||||
emitFailed(reason);
|
||||
return false;
|
||||
}
|
||||
case AccountTaskState::STATE_DISABLED: {
|
||||
m_data->errorString = reason;
|
||||
m_data->accountState = AccountState::Disabled;
|
||||
emitFailed(reason);
|
||||
return false;
|
||||
}
|
||||
case AccountTaskState::STATE_FAILED_SOFT: {
|
||||
m_data->errorString = reason;
|
||||
m_data->accountState = AccountState::Errored;
|
||||
emitFailed(reason);
|
||||
return false;
|
||||
}
|
||||
case AccountTaskState::STATE_FAILED_HARD: {
|
||||
m_data->errorString = reason;
|
||||
m_data->accountState = AccountState::Expired;
|
||||
emitFailed(reason);
|
||||
return false;
|
||||
}
|
||||
case AccountTaskState::STATE_FAILED_GONE: {
|
||||
m_data->errorString = reason;
|
||||
m_data->accountState = AccountState::Gone;
|
||||
emitFailed(reason);
|
||||
return false;
|
||||
}
|
||||
default: {
|
||||
QString error = tr("Unknown account task state: %1").arg(int(newState));
|
||||
m_data->accountState = AccountState::Errored;
|
||||
emitFailed(error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This file incorporates work covered by the following copyright and
|
||||
* permission notice:
|
||||
*
|
||||
* Copyright 2013-2021 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <tasks/Task.h>
|
||||
|
||||
#include <qsslerror.h>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
|
||||
#include "MinecraftAccount.h"
|
||||
|
||||
class QNetworkReply;
|
||||
|
||||
/**
|
||||
* Enum for describing the state of the current task.
|
||||
* Used by the getStateMessage function to determine what the status message should be.
|
||||
*/
|
||||
enum class AccountTaskState {
|
||||
STATE_CREATED,
|
||||
STATE_WORKING,
|
||||
STATE_SUCCEEDED,
|
||||
STATE_DISABLED, //!< MSA Client ID has changed. Tell user to reloginn
|
||||
STATE_FAILED_SOFT, //!< soft failure. authentication went through partially
|
||||
STATE_FAILED_HARD, //!< hard failure. main tokens are invalid
|
||||
STATE_FAILED_GONE, //!< hard failure. main tokens are invalid, and the account no longer exists
|
||||
STATE_OFFLINE //!< soft failure. authentication failed in the first step in a 'soft' way
|
||||
};
|
||||
|
||||
class AccountTask : public Task {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit AccountTask(AccountData* data, QObject* parent = 0);
|
||||
virtual ~AccountTask(){};
|
||||
|
||||
AccountTaskState m_taskState = AccountTaskState::STATE_CREATED;
|
||||
|
||||
AccountTaskState taskState() { return m_taskState; }
|
||||
|
||||
signals:
|
||||
void showVerificationUriAndCode(const QUrl& uri, const QString& code, int expiresIn);
|
||||
void hideVerificationUriAndCode();
|
||||
|
||||
protected:
|
||||
/**
|
||||
* Returns the state message for the given state.
|
||||
* Used to set the status message for the task.
|
||||
* Should be overridden by subclasses that want to change messages for a given state.
|
||||
*/
|
||||
virtual QString getStateMessage() const;
|
||||
|
||||
protected slots:
|
||||
// NOTE: true -> non-terminal state, false -> terminal state
|
||||
bool changeState(AccountTaskState newState, QString reason = QString());
|
||||
|
||||
protected:
|
||||
AccountData* m_data = nullptr;
|
||||
};
|
146
launcher/minecraft/auth/AuthFlow.cpp
Normal file
146
launcher/minecraft/auth/AuthFlow.cpp
Normal file
@ -0,0 +1,146 @@
|
||||
#include <QDebug>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
|
||||
#include "minecraft/auth/AccountData.h"
|
||||
#include "minecraft/auth/steps/EntitlementsStep.h"
|
||||
#include "minecraft/auth/steps/GetSkinStep.h"
|
||||
#include "minecraft/auth/steps/LauncherLoginStep.h"
|
||||
#include "minecraft/auth/steps/MSADeviceCodeStep.h"
|
||||
#include "minecraft/auth/steps/MSAStep.h"
|
||||
#include "minecraft/auth/steps/MinecraftProfileStep.h"
|
||||
#include "minecraft/auth/steps/XboxAuthorizationStep.h"
|
||||
#include "minecraft/auth/steps/XboxProfileStep.h"
|
||||
#include "minecraft/auth/steps/XboxUserStep.h"
|
||||
#include "tasks/Task.h"
|
||||
|
||||
#include "AuthFlow.h"
|
||||
|
||||
#include <Application.h>
|
||||
|
||||
AuthFlow::AuthFlow(AccountData* data, Action action, QObject* parent) : Task(parent), m_data(data)
|
||||
{
|
||||
if (data->type == AccountType::MSA) {
|
||||
if (action == Action::DeviceCode) {
|
||||
auto oauthStep = makeShared<MSADeviceCodeStep>(m_data);
|
||||
connect(oauthStep.get(), &MSADeviceCodeStep::authorizeWithBrowser, this, &AuthFlow::authorizeWithBrowserWithExtra);
|
||||
connect(this, &Task::aborted, oauthStep.get(), &MSADeviceCodeStep::abort);
|
||||
m_steps.append(oauthStep);
|
||||
} else {
|
||||
auto oauthStep = makeShared<MSAStep>(m_data, action == Action::Refresh);
|
||||
connect(oauthStep.get(), &MSAStep::authorizeWithBrowser, this, &AuthFlow::authorizeWithBrowser);
|
||||
m_steps.append(oauthStep);
|
||||
}
|
||||
m_steps.append(makeShared<XboxUserStep>(m_data));
|
||||
m_steps.append(makeShared<XboxAuthorizationStep>(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox"));
|
||||
m_steps.append(
|
||||
makeShared<XboxAuthorizationStep>(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang"));
|
||||
m_steps.append(makeShared<LauncherLoginStep>(m_data));
|
||||
m_steps.append(makeShared<XboxProfileStep>(m_data));
|
||||
m_steps.append(makeShared<EntitlementsStep>(m_data));
|
||||
m_steps.append(makeShared<MinecraftProfileStep>(m_data));
|
||||
m_steps.append(makeShared<GetSkinStep>(m_data));
|
||||
}
|
||||
changeState(AccountTaskState::STATE_CREATED);
|
||||
}
|
||||
|
||||
void AuthFlow::succeed()
|
||||
{
|
||||
m_data->validity_ = Validity::Certain;
|
||||
changeState(AccountTaskState::STATE_SUCCEEDED, tr("Finished all authentication steps"));
|
||||
}
|
||||
|
||||
void AuthFlow::executeTask()
|
||||
{
|
||||
changeState(AccountTaskState::STATE_WORKING, tr("Initializing"));
|
||||
nextStep();
|
||||
}
|
||||
|
||||
void AuthFlow::nextStep()
|
||||
{
|
||||
if (m_steps.size() == 0) {
|
||||
// we got to the end without an incident... assume this is all.
|
||||
m_currentStep.reset();
|
||||
succeed();
|
||||
return;
|
||||
}
|
||||
m_currentStep = m_steps.front();
|
||||
qDebug() << "AuthFlow:" << m_currentStep->describe();
|
||||
m_steps.pop_front();
|
||||
connect(m_currentStep.get(), &AuthStep::finished, this, &AuthFlow::stepFinished);
|
||||
|
||||
m_currentStep->perform();
|
||||
}
|
||||
|
||||
void AuthFlow::stepFinished(AccountTaskState resultingState, QString message)
|
||||
{
|
||||
if (changeState(resultingState, message))
|
||||
nextStep();
|
||||
}
|
||||
|
||||
bool AuthFlow::changeState(AccountTaskState newState, QString reason)
|
||||
{
|
||||
m_taskState = newState;
|
||||
setDetails(reason);
|
||||
switch (newState) {
|
||||
case AccountTaskState::STATE_CREATED: {
|
||||
setStatus(tr("Waiting..."));
|
||||
m_data->errorString.clear();
|
||||
return true;
|
||||
}
|
||||
case AccountTaskState::STATE_WORKING: {
|
||||
setStatus(m_currentStep ? m_currentStep->describe() : tr("Working..."));
|
||||
m_data->accountState = AccountState::Working;
|
||||
return true;
|
||||
}
|
||||
case AccountTaskState::STATE_SUCCEEDED: {
|
||||
setStatus(tr("Authentication task succeeded."));
|
||||
m_data->accountState = AccountState::Online;
|
||||
emitSucceeded();
|
||||
return false;
|
||||
}
|
||||
case AccountTaskState::STATE_OFFLINE: {
|
||||
setStatus(tr("Failed to contact the authentication server."));
|
||||
m_data->errorString = reason;
|
||||
m_data->accountState = AccountState::Offline;
|
||||
emitFailed(reason);
|
||||
return false;
|
||||
}
|
||||
case AccountTaskState::STATE_DISABLED: {
|
||||
setStatus(tr("Client ID has changed. New session needs to be created."));
|
||||
m_data->errorString = reason;
|
||||
m_data->accountState = AccountState::Disabled;
|
||||
emitFailed(reason);
|
||||
return false;
|
||||
}
|
||||
case AccountTaskState::STATE_FAILED_SOFT: {
|
||||
setStatus(tr("Encountered an error during authentication."));
|
||||
m_data->errorString = reason;
|
||||
m_data->accountState = AccountState::Errored;
|
||||
emitFailed(reason);
|
||||
return false;
|
||||
}
|
||||
case AccountTaskState::STATE_FAILED_HARD: {
|
||||
setStatus(tr("Failed to authenticate. The session has expired."));
|
||||
m_data->errorString = reason;
|
||||
m_data->accountState = AccountState::Expired;
|
||||
emitFailed(reason);
|
||||
return false;
|
||||
}
|
||||
case AccountTaskState::STATE_FAILED_GONE: {
|
||||
setStatus(tr("Failed to authenticate. The account no longer exists."));
|
||||
m_data->errorString = reason;
|
||||
m_data->accountState = AccountState::Gone;
|
||||
emitFailed(reason);
|
||||
return false;
|
||||
}
|
||||
default: {
|
||||
setStatus(tr("..."));
|
||||
QString error = tr("Unknown account task state: %1").arg(int(newState));
|
||||
m_data->accountState = AccountState::Errored;
|
||||
emitFailed(error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
45
launcher/minecraft/auth/AuthFlow.h
Normal file
45
launcher/minecraft/auth/AuthFlow.h
Normal file
@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <QImage>
|
||||
#include <QList>
|
||||
#include <QNetworkReply>
|
||||
#include <QObject>
|
||||
#include <QSet>
|
||||
#include <QVector>
|
||||
|
||||
#include "minecraft/auth/AccountData.h"
|
||||
#include "minecraft/auth/AuthStep.h"
|
||||
#include "tasks/Task.h"
|
||||
|
||||
class AuthFlow : public Task {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum class Action { Refresh, Login, DeviceCode };
|
||||
|
||||
explicit AuthFlow(AccountData* data, Action action = Action::Refresh, QObject* parent = 0);
|
||||
virtual ~AuthFlow() = default;
|
||||
|
||||
void executeTask() override;
|
||||
|
||||
AccountTaskState taskState() { return m_taskState; }
|
||||
|
||||
signals:
|
||||
void authorizeWithBrowser(const QUrl& url);
|
||||
void authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn);
|
||||
|
||||
protected:
|
||||
void succeed();
|
||||
void nextStep();
|
||||
|
||||
private slots:
|
||||
// NOTE: true -> non-terminal state, false -> terminal state
|
||||
bool changeState(AccountTaskState newState, QString reason = QString());
|
||||
void stepFinished(AccountTaskState resultingState, QString message);
|
||||
|
||||
private:
|
||||
AccountTaskState m_taskState = AccountTaskState::STATE_CREATED;
|
||||
QList<AuthStep::Ptr> m_steps;
|
||||
AuthStep::Ptr m_currentStep;
|
||||
AccountData* m_data = nullptr;
|
||||
};
|
@ -1,175 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This file incorporates work covered by the following copyright and
|
||||
* permission notice:
|
||||
*
|
||||
* Copyright 2013-2021 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <cassert>
|
||||
|
||||
#include <QBuffer>
|
||||
#include <QDebug>
|
||||
#include <QTimer>
|
||||
#include <QUrlQuery>
|
||||
|
||||
#include "Application.h"
|
||||
#include "AuthRequest.h"
|
||||
#include "katabasis/Globals.h"
|
||||
|
||||
AuthRequest::AuthRequest(QObject* parent) : QObject(parent) {}
|
||||
|
||||
AuthRequest::~AuthRequest() {}
|
||||
|
||||
void AuthRequest::get(const QNetworkRequest& req, int timeout /* = 60*1000*/)
|
||||
{
|
||||
setup(req, QNetworkAccessManager::GetOperation);
|
||||
reply_ = APPLICATION->network()->get(request_);
|
||||
status_ = Requesting;
|
||||
timedReplies_.add(new Katabasis::Reply(reply_, timeout));
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15
|
||||
connect(reply_, &QNetworkReply::errorOccurred, this, &AuthRequest::onRequestError);
|
||||
#else // &QNetworkReply::error SIGNAL depricated
|
||||
connect(reply_, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &AuthRequest::onRequestError);
|
||||
#endif
|
||||
connect(reply_, &QNetworkReply::finished, this, &AuthRequest::onRequestFinished);
|
||||
connect(reply_, &QNetworkReply::sslErrors, this, &AuthRequest::onSslErrors);
|
||||
}
|
||||
|
||||
void AuthRequest::post(const QNetworkRequest& req, const QByteArray& data, int timeout /* = 60*1000*/)
|
||||
{
|
||||
setup(req, QNetworkAccessManager::PostOperation);
|
||||
data_ = data;
|
||||
status_ = Requesting;
|
||||
reply_ = APPLICATION->network()->post(request_, data_);
|
||||
timedReplies_.add(new Katabasis::Reply(reply_, timeout));
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15
|
||||
connect(reply_, &QNetworkReply::errorOccurred, this, &AuthRequest::onRequestError);
|
||||
#else // &QNetworkReply::error SIGNAL depricated
|
||||
connect(reply_, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &AuthRequest::onRequestError);
|
||||
#endif
|
||||
connect(reply_, &QNetworkReply::finished, this, &AuthRequest::onRequestFinished);
|
||||
connect(reply_, &QNetworkReply::sslErrors, this, &AuthRequest::onSslErrors);
|
||||
connect(reply_, &QNetworkReply::uploadProgress, this, &AuthRequest::onUploadProgress);
|
||||
}
|
||||
|
||||
void AuthRequest::onRequestFinished()
|
||||
{
|
||||
if (status_ == Idle) {
|
||||
return;
|
||||
}
|
||||
if (reply_ != qobject_cast<QNetworkReply*>(sender())) {
|
||||
return;
|
||||
}
|
||||
httpStatus_ = reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
finish();
|
||||
}
|
||||
|
||||
void AuthRequest::onRequestError(QNetworkReply::NetworkError error)
|
||||
{
|
||||
qWarning() << "AuthRequest::onRequestError: Error" << (int)error;
|
||||
if (status_ == Idle) {
|
||||
return;
|
||||
}
|
||||
if (reply_ != qobject_cast<QNetworkReply*>(sender())) {
|
||||
return;
|
||||
}
|
||||
errorString_ = reply_->errorString();
|
||||
httpStatus_ = reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
error_ = error;
|
||||
qWarning() << "AuthRequest::onRequestError: Error string: " << errorString_;
|
||||
qWarning() << "AuthRequest::onRequestError: HTTP status" << httpStatus_
|
||||
<< reply_->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
|
||||
|
||||
// QTimer::singleShot(10, this, SLOT(finish()));
|
||||
}
|
||||
|
||||
void AuthRequest::onSslErrors(QList<QSslError> errors)
|
||||
{
|
||||
int i = 1;
|
||||
for (auto error : errors) {
|
||||
qCritical() << "LOGIN SSL Error #" << i << " : " << error.errorString();
|
||||
auto cert = error.certificate();
|
||||
qCritical() << "Certificate in question:\n" << cert.toText();
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
void AuthRequest::onUploadProgress(qint64 uploaded, qint64 total)
|
||||
{
|
||||
if (status_ == Idle) {
|
||||
qWarning() << "AuthRequest::onUploadProgress: No pending request";
|
||||
return;
|
||||
}
|
||||
if (reply_ != qobject_cast<QNetworkReply*>(sender())) {
|
||||
return;
|
||||
}
|
||||
// Restart timeout because request in progress
|
||||
Katabasis::Reply* o2Reply = timedReplies_.find(reply_);
|
||||
if (o2Reply) {
|
||||
o2Reply->start();
|
||||
}
|
||||
emit uploadProgress(uploaded, total);
|
||||
}
|
||||
|
||||
void AuthRequest::setup(const QNetworkRequest& req, QNetworkAccessManager::Operation operation, const QByteArray& verb)
|
||||
{
|
||||
request_ = req;
|
||||
operation_ = operation;
|
||||
url_ = req.url();
|
||||
|
||||
QUrl url = url_;
|
||||
request_.setUrl(url);
|
||||
|
||||
if (!verb.isEmpty()) {
|
||||
request_.setRawHeader(Katabasis::HTTP_HTTP_HEADER, verb);
|
||||
}
|
||||
|
||||
status_ = Requesting;
|
||||
error_ = QNetworkReply::NoError;
|
||||
errorString_.clear();
|
||||
httpStatus_ = 0;
|
||||
}
|
||||
|
||||
void AuthRequest::finish()
|
||||
{
|
||||
QByteArray data;
|
||||
if (status_ == Idle) {
|
||||
qWarning() << "AuthRequest::finish: No pending request";
|
||||
return;
|
||||
}
|
||||
data = reply_->readAll();
|
||||
status_ = Idle;
|
||||
timedReplies_.remove(reply_);
|
||||
reply_->disconnect(this);
|
||||
reply_->deleteLater();
|
||||
QList<QNetworkReply::RawHeaderPair> headers = reply_->rawHeaderPairs();
|
||||
emit finished(error_, data, headers);
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
#pragma once
|
||||
#include <QByteArray>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QObject>
|
||||
#include <QUrl>
|
||||
|
||||
#include "katabasis/Reply.h"
|
||||
|
||||
/// Makes authentication requests.
|
||||
class AuthRequest : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AuthRequest(QObject* parent = 0);
|
||||
~AuthRequest();
|
||||
|
||||
public slots:
|
||||
void get(const QNetworkRequest& req, int timeout = 60 * 1000);
|
||||
void post(const QNetworkRequest& req, const QByteArray& data, int timeout = 60 * 1000);
|
||||
|
||||
signals:
|
||||
|
||||
/// Emitted when a request has been completed or failed.
|
||||
void finished(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers);
|
||||
|
||||
/// Emitted when an upload has progressed.
|
||||
void uploadProgress(qint64 bytesSent, qint64 bytesTotal);
|
||||
|
||||
protected slots:
|
||||
|
||||
/// Handle request finished.
|
||||
void onRequestFinished();
|
||||
|
||||
/// Handle request error.
|
||||
void onRequestError(QNetworkReply::NetworkError error);
|
||||
|
||||
/// Handle ssl errors.
|
||||
void onSslErrors(QList<QSslError> errors);
|
||||
|
||||
/// Finish the request, emit finished() signal.
|
||||
void finish();
|
||||
|
||||
/// Handle upload progress.
|
||||
void onUploadProgress(qint64 uploaded, qint64 total);
|
||||
|
||||
public:
|
||||
QNetworkReply::NetworkError error_;
|
||||
int httpStatus_ = 0;
|
||||
QString errorString_;
|
||||
|
||||
protected:
|
||||
void setup(const QNetworkRequest& request, QNetworkAccessManager::Operation operation, const QByteArray& verb = QByteArray());
|
||||
|
||||
enum Status { Idle, Requesting, ReRequesting };
|
||||
|
||||
QNetworkRequest request_;
|
||||
QByteArray data_;
|
||||
QNetworkReply* reply_;
|
||||
Status status_;
|
||||
QNetworkAccessManager::Operation operation_;
|
||||
QUrl url_;
|
||||
Katabasis::ReplyList timedReplies_;
|
||||
|
||||
QTimer* timer_;
|
||||
};
|
@ -1,5 +0,0 @@
|
||||
#include "AuthStep.h"
|
||||
|
||||
AuthStep::AuthStep(AccountData* data) : QObject(nullptr), m_data(data) {}
|
||||
|
||||
AuthStep::~AuthStep() noexcept = default;
|
@ -3,30 +3,40 @@
|
||||
#include <QNetworkReply>
|
||||
#include <QObject>
|
||||
|
||||
#include "AccountTask.h"
|
||||
#include "QObjectPtr.h"
|
||||
#include "minecraft/auth/AccountData.h"
|
||||
|
||||
/**
|
||||
* Enum for describing the state of the current task.
|
||||
* Used by the getStateMessage function to determine what the status message should be.
|
||||
*/
|
||||
enum class AccountTaskState {
|
||||
STATE_CREATED,
|
||||
STATE_WORKING,
|
||||
STATE_SUCCEEDED,
|
||||
STATE_DISABLED, //!< MSA Client ID has changed. Tell user to reloginn
|
||||
STATE_FAILED_SOFT, //!< soft failure. authentication went through partially
|
||||
STATE_FAILED_HARD, //!< hard failure. main tokens are invalid
|
||||
STATE_FAILED_GONE, //!< hard failure. main tokens are invalid, and the account no longer exists
|
||||
STATE_OFFLINE //!< soft failure. authentication failed in the first step in a 'soft' way
|
||||
};
|
||||
|
||||
class AuthStep : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
using Ptr = shared_qobject_ptr<AuthStep>;
|
||||
|
||||
public:
|
||||
explicit AuthStep(AccountData* data);
|
||||
virtual ~AuthStep() noexcept;
|
||||
explicit AuthStep(AccountData* data) : QObject(nullptr), m_data(data){};
|
||||
virtual ~AuthStep() noexcept = default;
|
||||
|
||||
virtual QString describe() = 0;
|
||||
|
||||
public slots:
|
||||
virtual void perform() = 0;
|
||||
virtual void rehydrate() = 0;
|
||||
|
||||
signals:
|
||||
void finished(AccountTaskState resultingState, QString message);
|
||||
void showVerificationUriAndCode(const QUrl& uri, const QString& code, int expiresIn);
|
||||
void hideVerificationUriAndCode();
|
||||
|
||||
protected:
|
||||
AccountData* m_data;
|
||||
|
@ -50,9 +50,8 @@
|
||||
|
||||
#include <QPainter>
|
||||
|
||||
#include "flows/MSA.h"
|
||||
#include "flows/Offline.h"
|
||||
#include "minecraft/auth/AccountData.h"
|
||||
#include "minecraft/auth/AuthFlow.h"
|
||||
|
||||
MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent)
|
||||
{
|
||||
@ -80,7 +79,7 @@ MinecraftAccountPtr MinecraftAccount::createOffline(const QString& username)
|
||||
auto account = makeShared<MinecraftAccount>();
|
||||
account->data.type = AccountType::Offline;
|
||||
account->data.yggdrasilToken.token = "0";
|
||||
account->data.yggdrasilToken.validity = Katabasis::Validity::Certain;
|
||||
account->data.yggdrasilToken.validity = Validity::Certain;
|
||||
account->data.yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc();
|
||||
account->data.yggdrasilToken.extra["userName"] = username;
|
||||
account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]"));
|
||||
@ -88,7 +87,7 @@ MinecraftAccountPtr MinecraftAccount::createOffline(const QString& username)
|
||||
account->data.minecraftEntitlement.canPlayMinecraft = true;
|
||||
account->data.minecraftProfile.id = uuidFromUsername(username).toString().remove(QRegularExpression("[{}-]"));
|
||||
account->data.minecraftProfile.name = username;
|
||||
account->data.minecraftProfile.validity = Katabasis::Validity::Certain;
|
||||
account->data.minecraftProfile.validity = Validity::Certain;
|
||||
return account;
|
||||
}
|
||||
|
||||
@ -120,11 +119,11 @@ QPixmap MinecraftAccount::getFace() const
|
||||
return skin.scaled(64, 64, Qt::KeepAspectRatio);
|
||||
}
|
||||
|
||||
shared_qobject_ptr<AccountTask> MinecraftAccount::loginMSA()
|
||||
shared_qobject_ptr<AuthFlow> MinecraftAccount::login(bool useDeviceCode)
|
||||
{
|
||||
Q_ASSERT(m_currentTask.get() == nullptr);
|
||||
|
||||
m_currentTask.reset(new MSAInteractive(&data));
|
||||
m_currentTask.reset(new AuthFlow(&data, useDeviceCode ? AuthFlow::Action::DeviceCode : AuthFlow::Action::Login, this));
|
||||
connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded);
|
||||
connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed);
|
||||
connect(m_currentTask.get(), &Task::aborted, this, [this] { authFailed(tr("Aborted")); });
|
||||
@ -132,29 +131,13 @@ shared_qobject_ptr<AccountTask> MinecraftAccount::loginMSA()
|
||||
return m_currentTask;
|
||||
}
|
||||
|
||||
shared_qobject_ptr<AccountTask> MinecraftAccount::loginOffline()
|
||||
{
|
||||
Q_ASSERT(m_currentTask.get() == nullptr);
|
||||
|
||||
m_currentTask.reset(new OfflineLogin(&data));
|
||||
connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded);
|
||||
connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed);
|
||||
connect(m_currentTask.get(), &Task::aborted, this, [this] { authFailed(tr("Aborted")); });
|
||||
emit activityChanged(true);
|
||||
return m_currentTask;
|
||||
}
|
||||
|
||||
shared_qobject_ptr<AccountTask> MinecraftAccount::refresh()
|
||||
shared_qobject_ptr<AuthFlow> MinecraftAccount::refresh()
|
||||
{
|
||||
if (m_currentTask) {
|
||||
return m_currentTask;
|
||||
}
|
||||
|
||||
if (data.type == AccountType::MSA) {
|
||||
m_currentTask.reset(new MSASilent(&data));
|
||||
} else {
|
||||
m_currentTask.reset(new OfflineRefresh(&data));
|
||||
}
|
||||
m_currentTask.reset(new AuthFlow(&data, AuthFlow::Action::Refresh, this));
|
||||
|
||||
connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded);
|
||||
connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed);
|
||||
@ -163,7 +146,7 @@ shared_qobject_ptr<AccountTask> MinecraftAccount::refresh()
|
||||
return m_currentTask;
|
||||
}
|
||||
|
||||
shared_qobject_ptr<AccountTask> MinecraftAccount::currentTask()
|
||||
shared_qobject_ptr<AuthFlow> MinecraftAccount::currentTask()
|
||||
{
|
||||
return m_currentTask;
|
||||
}
|
||||
@ -189,17 +172,17 @@ void MinecraftAccount::authFailed(QString reason)
|
||||
if (accountType() == AccountType::MSA) {
|
||||
data.msaToken.token = QString();
|
||||
data.msaToken.refresh_token = QString();
|
||||
data.msaToken.validity = Katabasis::Validity::None;
|
||||
data.validity_ = Katabasis::Validity::None;
|
||||
data.msaToken.validity = Validity::None;
|
||||
data.validity_ = Validity::None;
|
||||
} else {
|
||||
data.yggdrasilToken.token = QString();
|
||||
data.yggdrasilToken.validity = Katabasis::Validity::None;
|
||||
data.validity_ = Katabasis::Validity::None;
|
||||
data.yggdrasilToken.validity = Validity::None;
|
||||
data.validity_ = Validity::None;
|
||||
}
|
||||
emit changed();
|
||||
} break;
|
||||
case AccountTaskState::STATE_FAILED_GONE: {
|
||||
data.validity_ = Katabasis::Validity::None;
|
||||
data.validity_ = Validity::None;
|
||||
emit changed();
|
||||
} break;
|
||||
case AccountTaskState::STATE_CREATED:
|
||||
@ -229,13 +212,13 @@ bool MinecraftAccount::shouldRefresh() const
|
||||
return false;
|
||||
}
|
||||
switch (data.validity_) {
|
||||
case Katabasis::Validity::Certain: {
|
||||
case Validity::Certain: {
|
||||
break;
|
||||
}
|
||||
case Katabasis::Validity::None: {
|
||||
case Validity::None: {
|
||||
return false;
|
||||
}
|
||||
case Katabasis::Validity::Assumed: {
|
||||
case Validity::Assumed: {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -43,15 +43,13 @@
|
||||
#include <QPixmap>
|
||||
#include <QString>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "AccountData.h"
|
||||
#include "AuthSession.h"
|
||||
#include "QObjectPtr.h"
|
||||
#include "Usable.h"
|
||||
#include "minecraft/auth/AuthFlow.h"
|
||||
|
||||
class Task;
|
||||
class AccountTask;
|
||||
class MinecraftAccount;
|
||||
|
||||
using MinecraftAccountPtr = shared_qobject_ptr<MinecraftAccount>;
|
||||
@ -97,13 +95,11 @@ class MinecraftAccount : public QObject, public Usable {
|
||||
QJsonObject saveToJson() const;
|
||||
|
||||
public: /* manipulation */
|
||||
shared_qobject_ptr<AccountTask> loginMSA();
|
||||
shared_qobject_ptr<AuthFlow> login(bool useDeviceCode = false);
|
||||
|
||||
shared_qobject_ptr<AccountTask> loginOffline();
|
||||
shared_qobject_ptr<AuthFlow> refresh();
|
||||
|
||||
shared_qobject_ptr<AccountTask> refresh();
|
||||
|
||||
shared_qobject_ptr<AccountTask> currentTask();
|
||||
shared_qobject_ptr<AuthFlow> currentTask();
|
||||
|
||||
public: /* queries */
|
||||
QString internalId() const { return data.internalId; }
|
||||
@ -166,7 +162,7 @@ class MinecraftAccount : public QObject, public Usable {
|
||||
AccountData data;
|
||||
|
||||
// current task we are executing here
|
||||
shared_qobject_ptr<AccountTask> m_currentTask;
|
||||
shared_qobject_ptr<AuthFlow> m_currentTask;
|
||||
|
||||
protected: /* methods */
|
||||
void incrementUses() override;
|
||||
|
@ -79,7 +79,7 @@ bool getBool(QJsonValue value, bool& out)
|
||||
// 2148916238 = child account not linked to a family
|
||||
*/
|
||||
|
||||
bool parseXTokenResponse(QByteArray& data, Katabasis::Token& output, QString name)
|
||||
bool parseXTokenResponse(QByteArray& data, Token& output, QString name)
|
||||
{
|
||||
qDebug() << "Parsing" << name << ":";
|
||||
qCDebug(authCredentials()) << data;
|
||||
@ -135,7 +135,7 @@ bool parseXTokenResponse(QByteArray& data, Katabasis::Token& output, QString nam
|
||||
qWarning() << "Missing uhs";
|
||||
return false;
|
||||
}
|
||||
output.validity = Katabasis::Validity::Certain;
|
||||
output.validity = Validity::Certain;
|
||||
qDebug() << name << "is valid.";
|
||||
return true;
|
||||
}
|
||||
@ -213,7 +213,7 @@ bool parseMinecraftProfile(QByteArray& data, MinecraftProfile& output)
|
||||
output.capes[capeOut.id] = capeOut;
|
||||
}
|
||||
output.currentCape = currentCape;
|
||||
output.validity = Katabasis::Validity::Certain;
|
||||
output.validity = Validity::Certain;
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -388,7 +388,7 @@ bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output)
|
||||
output.currentCape = capeOut.alias;
|
||||
}
|
||||
|
||||
output.validity = Katabasis::Validity::Certain;
|
||||
output.validity = Validity::Certain;
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -422,7 +422,7 @@ bool parseMinecraftEntitlements(QByteArray& data, MinecraftEntitlement& output)
|
||||
output.ownsMinecraft = true;
|
||||
}
|
||||
}
|
||||
output.validity = Katabasis::Validity::Certain;
|
||||
output.validity = Validity::Certain;
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -456,7 +456,7 @@ bool parseRolloutResponse(QByteArray& data, bool& result)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parseMojangResponse(QByteArray& data, Katabasis::Token& output)
|
||||
bool parseMojangResponse(QByteArray& data, Token& output)
|
||||
{
|
||||
QJsonParseError jsonError;
|
||||
qDebug() << "Parsing Mojang response...";
|
||||
@ -488,7 +488,7 @@ bool parseMojangResponse(QByteArray& data, Katabasis::Token& output)
|
||||
qWarning() << "access_token is not valid";
|
||||
return false;
|
||||
}
|
||||
output.validity = Katabasis::Validity::Certain;
|
||||
output.validity = Validity::Certain;
|
||||
qDebug() << "Mojang response is valid.";
|
||||
return true;
|
||||
}
|
||||
|
@ -9,8 +9,8 @@ bool getNumber(QJsonValue value, double& out);
|
||||
bool getNumber(QJsonValue value, int64_t& out);
|
||||
bool getBool(QJsonValue value, bool& out);
|
||||
|
||||
bool parseXTokenResponse(QByteArray& data, Katabasis::Token& output, QString name);
|
||||
bool parseMojangResponse(QByteArray& data, Katabasis::Token& output);
|
||||
bool parseXTokenResponse(QByteArray& data, Token& output, QString name);
|
||||
bool parseMojangResponse(QByteArray& data, Token& output);
|
||||
|
||||
bool parseMinecraftProfile(QByteArray& data, MinecraftProfile& output);
|
||||
bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output);
|
||||
|
@ -1,67 +0,0 @@
|
||||
#include <QDebug>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
|
||||
#include "AuthFlow.h"
|
||||
#include "katabasis/Globals.h"
|
||||
|
||||
#include <Application.h>
|
||||
|
||||
AuthFlow::AuthFlow(AccountData* data, QObject* parent) : AccountTask(data, parent) {}
|
||||
|
||||
void AuthFlow::succeed()
|
||||
{
|
||||
m_data->validity_ = Katabasis::Validity::Certain;
|
||||
changeState(AccountTaskState::STATE_SUCCEEDED, tr("Finished all authentication steps"));
|
||||
}
|
||||
|
||||
void AuthFlow::executeTask()
|
||||
{
|
||||
if (m_currentStep) {
|
||||
return;
|
||||
}
|
||||
changeState(AccountTaskState::STATE_WORKING, tr("Initializing"));
|
||||
nextStep();
|
||||
}
|
||||
|
||||
void AuthFlow::nextStep()
|
||||
{
|
||||
if (m_steps.size() == 0) {
|
||||
// we got to the end without an incident... assume this is all.
|
||||
m_currentStep.reset();
|
||||
succeed();
|
||||
return;
|
||||
}
|
||||
m_currentStep = m_steps.front();
|
||||
qDebug() << "AuthFlow:" << m_currentStep->describe();
|
||||
m_steps.pop_front();
|
||||
connect(m_currentStep.get(), &AuthStep::finished, this, &AuthFlow::stepFinished);
|
||||
connect(m_currentStep.get(), &AuthStep::showVerificationUriAndCode, this, &AuthFlow::showVerificationUriAndCode);
|
||||
connect(m_currentStep.get(), &AuthStep::hideVerificationUriAndCode, this, &AuthFlow::hideVerificationUriAndCode);
|
||||
|
||||
m_currentStep->perform();
|
||||
}
|
||||
|
||||
QString AuthFlow::getStateMessage() const
|
||||
{
|
||||
switch (m_taskState) {
|
||||
case AccountTaskState::STATE_WORKING: {
|
||||
if (m_currentStep) {
|
||||
return m_currentStep->describe();
|
||||
} else {
|
||||
return tr("Working...");
|
||||
}
|
||||
}
|
||||
default: {
|
||||
return AccountTask::getStateMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AuthFlow::stepFinished(AccountTaskState resultingState, QString message)
|
||||
{
|
||||
if (changeState(resultingState, message)) {
|
||||
nextStep();
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QImage>
|
||||
#include <QList>
|
||||
#include <QNetworkReply>
|
||||
#include <QObject>
|
||||
#include <QSet>
|
||||
#include <QVector>
|
||||
|
||||
#include <katabasis/DeviceFlow.h>
|
||||
|
||||
#include "minecraft/auth/AccountData.h"
|
||||
#include "minecraft/auth/AccountTask.h"
|
||||
#include "minecraft/auth/AuthStep.h"
|
||||
|
||||
class AuthFlow : public AccountTask {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AuthFlow(AccountData* data, QObject* parent = 0);
|
||||
|
||||
Katabasis::Validity validity() { return m_data->validity_; };
|
||||
|
||||
QString getStateMessage() const override;
|
||||
|
||||
void executeTask() override;
|
||||
|
||||
signals:
|
||||
void activityChanged(Katabasis::Activity activity);
|
||||
|
||||
private slots:
|
||||
void stepFinished(AccountTaskState resultingState, QString message);
|
||||
|
||||
protected:
|
||||
void succeed();
|
||||
void nextStep();
|
||||
|
||||
protected:
|
||||
QList<AuthStep::Ptr> m_steps;
|
||||
AuthStep::Ptr m_currentStep;
|
||||
};
|
@ -1,36 +0,0 @@
|
||||
#include "MSA.h"
|
||||
|
||||
#include "minecraft/auth/steps/EntitlementsStep.h"
|
||||
#include "minecraft/auth/steps/GetSkinStep.h"
|
||||
#include "minecraft/auth/steps/LauncherLoginStep.h"
|
||||
#include "minecraft/auth/steps/MSAStep.h"
|
||||
#include "minecraft/auth/steps/MinecraftProfileStep.h"
|
||||
#include "minecraft/auth/steps/XboxAuthorizationStep.h"
|
||||
#include "minecraft/auth/steps/XboxProfileStep.h"
|
||||
#include "minecraft/auth/steps/XboxUserStep.h"
|
||||
|
||||
MSASilent::MSASilent(AccountData* data, QObject* parent) : AuthFlow(data, parent)
|
||||
{
|
||||
m_steps.append(makeShared<MSAStep>(m_data, MSAStep::Action::Refresh));
|
||||
m_steps.append(makeShared<XboxUserStep>(m_data));
|
||||
m_steps.append(makeShared<XboxAuthorizationStep>(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox"));
|
||||
m_steps.append(makeShared<XboxAuthorizationStep>(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang"));
|
||||
m_steps.append(makeShared<LauncherLoginStep>(m_data));
|
||||
m_steps.append(makeShared<XboxProfileStep>(m_data));
|
||||
m_steps.append(makeShared<EntitlementsStep>(m_data));
|
||||
m_steps.append(makeShared<MinecraftProfileStep>(m_data));
|
||||
m_steps.append(makeShared<GetSkinStep>(m_data));
|
||||
}
|
||||
|
||||
MSAInteractive::MSAInteractive(AccountData* data, QObject* parent) : AuthFlow(data, parent)
|
||||
{
|
||||
m_steps.append(makeShared<MSAStep>(m_data, MSAStep::Action::Login));
|
||||
m_steps.append(makeShared<XboxUserStep>(m_data));
|
||||
m_steps.append(makeShared<XboxAuthorizationStep>(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox"));
|
||||
m_steps.append(makeShared<XboxAuthorizationStep>(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang"));
|
||||
m_steps.append(makeShared<LauncherLoginStep>(m_data));
|
||||
m_steps.append(makeShared<XboxProfileStep>(m_data));
|
||||
m_steps.append(makeShared<EntitlementsStep>(m_data));
|
||||
m_steps.append(makeShared<MinecraftProfileStep>(m_data));
|
||||
m_steps.append(makeShared<GetSkinStep>(m_data));
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
#pragma once
|
||||
#include "AuthFlow.h"
|
||||
|
||||
class MSAInteractive : public AuthFlow {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit MSAInteractive(AccountData* data, QObject* parent = 0);
|
||||
};
|
||||
|
||||
class MSASilent : public AuthFlow {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit MSASilent(AccountData* data, QObject* parent = 0);
|
||||
};
|
@ -1,13 +0,0 @@
|
||||
#include "Offline.h"
|
||||
|
||||
#include "minecraft/auth/steps/OfflineStep.h"
|
||||
|
||||
OfflineRefresh::OfflineRefresh(AccountData* data, QObject* parent) : AuthFlow(data, parent)
|
||||
{
|
||||
m_steps.append(makeShared<OfflineStep>(m_data));
|
||||
}
|
||||
|
||||
OfflineLogin::OfflineLogin(AccountData* data, QObject* parent) : AuthFlow(data, parent)
|
||||
{
|
||||
m_steps.append(makeShared<OfflineStep>(m_data));
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
#pragma once
|
||||
#include "AuthFlow.h"
|
||||
|
||||
class OfflineRefresh : public AuthFlow {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit OfflineRefresh(AccountData* data, QObject* parent = 0);
|
||||
};
|
||||
|
||||
class OfflineLogin : public AuthFlow {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit OfflineLogin(AccountData* data, QObject* parent = 0);
|
||||
};
|
@ -1,16 +1,20 @@
|
||||
#include "EntitlementsStep.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QNetworkRequest>
|
||||
#include <QUrl>
|
||||
#include <QUuid>
|
||||
#include <memory>
|
||||
|
||||
#include "Application.h"
|
||||
#include "Logging.h"
|
||||
#include "minecraft/auth/AuthRequest.h"
|
||||
#include "minecraft/auth/Parsers.h"
|
||||
#include "net/Download.h"
|
||||
#include "net/StaticHeaderProxy.h"
|
||||
#include "tasks/Task.h"
|
||||
|
||||
EntitlementsStep::EntitlementsStep(AccountData* data) : AuthStep(data) {}
|
||||
|
||||
EntitlementsStep::~EntitlementsStep() noexcept = default;
|
||||
|
||||
QString EntitlementsStep::describe()
|
||||
{
|
||||
return tr("Determining game ownership.");
|
||||
@ -19,35 +23,31 @@ QString EntitlementsStep::describe()
|
||||
void EntitlementsStep::perform()
|
||||
{
|
||||
auto uuid = QUuid::createUuid();
|
||||
m_entitlementsRequestId = uuid.toString().remove('{').remove('}');
|
||||
auto url = "https://api.minecraftservices.com/entitlements/license?requestId=" + m_entitlementsRequestId;
|
||||
QNetworkRequest request = QNetworkRequest(url);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
request.setRawHeader("Accept", "application/json");
|
||||
request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8());
|
||||
AuthRequest* requestor = new AuthRequest(this);
|
||||
connect(requestor, &AuthRequest::finished, this, &EntitlementsStep::onRequestDone);
|
||||
requestor->get(request);
|
||||
m_entitlements_request_id = uuid.toString().remove('{').remove('}');
|
||||
|
||||
QUrl url("https://api.minecraftservices.com/entitlements/license?requestId=" + m_entitlements_request_id);
|
||||
auto headers = QList<Net::HeaderPair>{ { "Content-Type", "application/json" },
|
||||
{ "Accept", "application/json" },
|
||||
{ "Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8() } };
|
||||
|
||||
m_response.reset(new QByteArray());
|
||||
m_task = Net::Download::makeByteArray(url, m_response);
|
||||
m_task->addHeaderProxy(new Net::StaticHeaderProxy(headers));
|
||||
|
||||
connect(m_task.get(), &Task::finished, this, &EntitlementsStep::onRequestDone);
|
||||
|
||||
m_task->setNetwork(APPLICATION->network());
|
||||
m_task->start();
|
||||
qDebug() << "Getting entitlements...";
|
||||
}
|
||||
|
||||
void EntitlementsStep::rehydrate()
|
||||
void EntitlementsStep::onRequestDone()
|
||||
{
|
||||
// NOOP, for now. We only save bools and there's nothing to check.
|
||||
}
|
||||
|
||||
void EntitlementsStep::onRequestDone([[maybe_unused]] QNetworkReply::NetworkError error,
|
||||
QByteArray data,
|
||||
[[maybe_unused]] QList<QNetworkReply::RawHeaderPair> headers)
|
||||
{
|
||||
auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
|
||||
requestor->deleteLater();
|
||||
|
||||
qCDebug(authCredentials()) << data;
|
||||
qCDebug(authCredentials()) << *m_response;
|
||||
|
||||
// TODO: check presence of same entitlementsRequestId?
|
||||
// TODO: validate JWTs?
|
||||
Parsers::parseMinecraftEntitlements(data, m_data->minecraftEntitlement);
|
||||
Parsers::parseMinecraftEntitlements(*m_response, m_data->minecraftEntitlement);
|
||||
|
||||
emit finished(AccountTaskState::STATE_WORKING, tr("Got entitlements"));
|
||||
}
|
||||
|
@ -1,24 +1,26 @@
|
||||
#pragma once
|
||||
#include <QObject>
|
||||
#include <memory>
|
||||
|
||||
#include "QObjectPtr.h"
|
||||
#include "minecraft/auth/AuthStep.h"
|
||||
#include "net/Download.h"
|
||||
|
||||
class EntitlementsStep : public AuthStep {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit EntitlementsStep(AccountData* data);
|
||||
virtual ~EntitlementsStep() noexcept;
|
||||
virtual ~EntitlementsStep() noexcept = default;
|
||||
|
||||
void perform() override;
|
||||
void rehydrate() override;
|
||||
|
||||
QString describe() override;
|
||||
|
||||
private slots:
|
||||
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
|
||||
void onRequestDone();
|
||||
|
||||
private:
|
||||
QString m_entitlementsRequestId;
|
||||
QString m_entitlements_request_id;
|
||||
std::shared_ptr<QByteArray> m_response;
|
||||
Net::Download::Ptr m_task;
|
||||
};
|
||||
|
@ -3,13 +3,10 @@
|
||||
|
||||
#include <QNetworkRequest>
|
||||
|
||||
#include "minecraft/auth/AuthRequest.h"
|
||||
#include "minecraft/auth/Parsers.h"
|
||||
#include "Application.h"
|
||||
|
||||
GetSkinStep::GetSkinStep(AccountData* data) : AuthStep(data) {}
|
||||
|
||||
GetSkinStep::~GetSkinStep() noexcept = default;
|
||||
|
||||
QString GetSkinStep::describe()
|
||||
{
|
||||
return tr("Getting skin.");
|
||||
@ -17,25 +14,20 @@ QString GetSkinStep::describe()
|
||||
|
||||
void GetSkinStep::perform()
|
||||
{
|
||||
auto url = QUrl(m_data->minecraftProfile.skin.url);
|
||||
QNetworkRequest request = QNetworkRequest(url);
|
||||
AuthRequest* requestor = new AuthRequest(this);
|
||||
connect(requestor, &AuthRequest::finished, this, &GetSkinStep::onRequestDone);
|
||||
requestor->get(request);
|
||||
QUrl url(m_data->minecraftProfile.skin.url);
|
||||
|
||||
m_response.reset(new QByteArray());
|
||||
m_task = Net::Download::makeByteArray(url, m_response);
|
||||
|
||||
connect(m_task.get(), &Task::finished, this, &GetSkinStep::onRequestDone);
|
||||
|
||||
m_task->setNetwork(APPLICATION->network());
|
||||
m_task->start();
|
||||
}
|
||||
|
||||
void GetSkinStep::rehydrate()
|
||||
void GetSkinStep::onRequestDone()
|
||||
{
|
||||
// NOOP, for now.
|
||||
}
|
||||
|
||||
void GetSkinStep::onRequestDone(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers)
|
||||
{
|
||||
auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
|
||||
requestor->deleteLater();
|
||||
|
||||
if (error == QNetworkReply::NoError) {
|
||||
m_data->minecraftProfile.skin.data = data;
|
||||
}
|
||||
if (m_task->error() == QNetworkReply::NoError)
|
||||
m_data->minecraftProfile.skin.data = *m_response;
|
||||
emit finished(AccountTaskState::STATE_SUCCEEDED, tr("Got skin"));
|
||||
}
|
||||
|
@ -1,21 +1,25 @@
|
||||
#pragma once
|
||||
#include <QObject>
|
||||
#include <memory>
|
||||
|
||||
#include "QObjectPtr.h"
|
||||
#include "minecraft/auth/AuthStep.h"
|
||||
#include "net/Download.h"
|
||||
|
||||
class GetSkinStep : public AuthStep {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit GetSkinStep(AccountData* data);
|
||||
virtual ~GetSkinStep() noexcept;
|
||||
virtual ~GetSkinStep() noexcept = default;
|
||||
|
||||
void perform() override;
|
||||
void rehydrate() override;
|
||||
|
||||
QString describe() override;
|
||||
|
||||
private slots:
|
||||
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
|
||||
void onRequestDone();
|
||||
|
||||
private:
|
||||
std::shared_ptr<QByteArray> m_response;
|
||||
Net::Download::Ptr m_task;
|
||||
};
|
||||
|
@ -1,17 +1,17 @@
|
||||
#include "LauncherLoginStep.h"
|
||||
|
||||
#include <QNetworkRequest>
|
||||
#include <QUrl>
|
||||
|
||||
#include "Application.h"
|
||||
#include "Logging.h"
|
||||
#include "minecraft/auth/AccountTask.h"
|
||||
#include "minecraft/auth/AuthRequest.h"
|
||||
#include "minecraft/auth/Parsers.h"
|
||||
#include "net/NetUtils.h"
|
||||
#include "net/StaticHeaderProxy.h"
|
||||
#include "net/Upload.h"
|
||||
|
||||
LauncherLoginStep::LauncherLoginStep(AccountData* data) : AuthStep(data) {}
|
||||
|
||||
LauncherLoginStep::~LauncherLoginStep() noexcept = default;
|
||||
|
||||
QString LauncherLoginStep::describe()
|
||||
{
|
||||
return tr("Accessing Mojang services.");
|
||||
@ -19,7 +19,7 @@ QString LauncherLoginStep::describe()
|
||||
|
||||
void LauncherLoginStep::perform()
|
||||
{
|
||||
auto requestURL = "https://api.minecraftservices.com/launcher/login";
|
||||
QUrl url("https://api.minecraftservices.com/launcher/login");
|
||||
auto uhs = m_data->mojangservicesToken.extra["uhs"].toString();
|
||||
auto xToken = m_data->mojangservicesToken.token;
|
||||
|
||||
@ -31,40 +31,37 @@ void LauncherLoginStep::perform()
|
||||
)XXX";
|
||||
auto requestBody = mc_auth_template.arg(uhs, xToken);
|
||||
|
||||
QNetworkRequest request = QNetworkRequest(QUrl(requestURL));
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
request.setRawHeader("Accept", "application/json");
|
||||
AuthRequest* requestor = new AuthRequest(this);
|
||||
connect(requestor, &AuthRequest::finished, this, &LauncherLoginStep::onRequestDone);
|
||||
requestor->post(request, requestBody.toUtf8());
|
||||
auto headers = QList<Net::HeaderPair>{
|
||||
{ "Content-Type", "application/json" },
|
||||
{ "Accept", "application/json" },
|
||||
};
|
||||
|
||||
m_response.reset(new QByteArray());
|
||||
m_task = Net::Upload::makeByteArray(url, m_response, requestBody.toUtf8());
|
||||
m_task->addHeaderProxy(new Net::StaticHeaderProxy(headers));
|
||||
|
||||
connect(m_task.get(), &Task::finished, this, &LauncherLoginStep::onRequestDone);
|
||||
|
||||
m_task->setNetwork(APPLICATION->network());
|
||||
m_task->start();
|
||||
qDebug() << "Getting Minecraft access token...";
|
||||
}
|
||||
|
||||
void LauncherLoginStep::rehydrate()
|
||||
void LauncherLoginStep::onRequestDone()
|
||||
{
|
||||
// TODO: check the token validity
|
||||
}
|
||||
|
||||
void LauncherLoginStep::onRequestDone(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers)
|
||||
{
|
||||
auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
|
||||
requestor->deleteLater();
|
||||
|
||||
qCDebug(authCredentials()) << data;
|
||||
if (error != QNetworkReply::NoError) {
|
||||
qWarning() << "Reply error:" << error;
|
||||
qCDebug(authCredentials()) << data;
|
||||
if (Net::isApplicationError(error)) {
|
||||
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to get Minecraft access token: %1").arg(requestor->errorString_));
|
||||
qCDebug(authCredentials()) << *m_response;
|
||||
if (m_task->error() != QNetworkReply::NoError) {
|
||||
qWarning() << "Reply error:" << m_task->error();
|
||||
if (Net::isApplicationError(m_task->error())) {
|
||||
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to get Minecraft access token: %1").arg(m_task->errorString()));
|
||||
} else {
|
||||
emit finished(AccountTaskState::STATE_OFFLINE, tr("Failed to get Minecraft access token: %1").arg(requestor->errorString_));
|
||||
emit finished(AccountTaskState::STATE_OFFLINE, tr("Failed to get Minecraft access token: %1").arg(m_task->errorString()));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Parsers::parseMojangResponse(data, m_data->yggdrasilToken)) {
|
||||
if (!Parsers::parseMojangResponse(*m_response, m_data->yggdrasilToken)) {
|
||||
qWarning() << "Could not parse login_with_xbox response...";
|
||||
qCDebug(authCredentials()) << data;
|
||||
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to parse the Minecraft access token response."));
|
||||
return;
|
||||
}
|
||||
|
@ -1,21 +1,25 @@
|
||||
#pragma once
|
||||
#include <QObject>
|
||||
#include <memory>
|
||||
|
||||
#include "QObjectPtr.h"
|
||||
#include "minecraft/auth/AuthStep.h"
|
||||
#include "net/Upload.h"
|
||||
|
||||
class LauncherLoginStep : public AuthStep {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit LauncherLoginStep(AccountData* data);
|
||||
virtual ~LauncherLoginStep() noexcept;
|
||||
virtual ~LauncherLoginStep() noexcept = default;
|
||||
|
||||
void perform() override;
|
||||
void rehydrate() override;
|
||||
|
||||
QString describe() override;
|
||||
|
||||
private slots:
|
||||
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
|
||||
void onRequestDone();
|
||||
|
||||
private:
|
||||
std::shared_ptr<QByteArray> m_response;
|
||||
Net::Upload::Ptr m_task;
|
||||
};
|
||||
|
270
launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp
Normal file
270
launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp
Normal file
@ -0,0 +1,270 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (c) 2024 Trial97 <alexandru.tripon97@gmail.com>
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This file incorporates work covered by the following copyright and
|
||||
* permission notice:
|
||||
*
|
||||
* Copyright 2013-2021 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "MSADeviceCodeStep.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QUrlQuery>
|
||||
|
||||
#include "Application.h"
|
||||
#include "Json.h"
|
||||
#include "net/StaticHeaderProxy.h"
|
||||
|
||||
// https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-device-code
|
||||
MSADeviceCodeStep::MSADeviceCodeStep(AccountData* data) : AuthStep(data)
|
||||
{
|
||||
m_clientId = APPLICATION->getMSAClientID();
|
||||
}
|
||||
|
||||
QString MSADeviceCodeStep::describe()
|
||||
{
|
||||
return tr("Logging in with Microsoft account(device code).");
|
||||
}
|
||||
|
||||
void MSADeviceCodeStep::perform()
|
||||
{
|
||||
QUrlQuery data;
|
||||
data.addQueryItem("client_id", m_clientId);
|
||||
data.addQueryItem("scope", "XboxLive.SignIn XboxLive.offline_access");
|
||||
auto payload = data.query(QUrl::FullyEncoded).toUtf8();
|
||||
QUrl url("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode");
|
||||
auto headers = QList<Net::HeaderPair>{
|
||||
{ "Content-Type", "application/x-www-form-urlencoded" },
|
||||
{ "Accept", "application/json" },
|
||||
};
|
||||
m_response.reset(new QByteArray());
|
||||
m_task = Net::Upload::makeByteArray(url, m_response, payload);
|
||||
m_task->addHeaderProxy(new Net::StaticHeaderProxy(headers));
|
||||
|
||||
connect(m_task.get(), &Task::finished, this, &MSADeviceCodeStep::deviceAutorizationFinished);
|
||||
|
||||
m_task->setNetwork(APPLICATION->network());
|
||||
m_task->start();
|
||||
}
|
||||
|
||||
struct DeviceAutorizationResponse {
|
||||
QString device_code;
|
||||
QString user_code;
|
||||
QString verification_uri;
|
||||
int expires_in;
|
||||
int interval;
|
||||
|
||||
QString error;
|
||||
QString error_description;
|
||||
};
|
||||
|
||||
DeviceAutorizationResponse parseDeviceAutorizationResponse(const QByteArray& data)
|
||||
{
|
||||
QJsonParseError err;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(data, &err);
|
||||
if (err.error != QJsonParseError::NoError) {
|
||||
qWarning() << "Failed to parse device autorization response due to err:" << err.errorString();
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!doc.isObject()) {
|
||||
qWarning() << "Device autorization response is not an object";
|
||||
return {};
|
||||
}
|
||||
auto obj = doc.object();
|
||||
return {
|
||||
Json::ensureString(obj, "device_code"), Json::ensureString(obj, "user_code"), Json::ensureString(obj, "verification_uri"),
|
||||
Json::ensureInteger(obj, "expires_in"), Json::ensureInteger(obj, "interval"), Json::ensureString(obj, "error"),
|
||||
Json::ensureString(obj, "error_description"),
|
||||
};
|
||||
}
|
||||
|
||||
void MSADeviceCodeStep::deviceAutorizationFinished()
|
||||
{
|
||||
auto rsp = parseDeviceAutorizationResponse(*m_response);
|
||||
if (!rsp.error.isEmpty() || !rsp.error_description.isEmpty()) {
|
||||
qWarning() << "Device authorization failed:" << rsp.error;
|
||||
emit finished(AccountTaskState::STATE_FAILED_HARD,
|
||||
tr("Device authorization failed: %1").arg(rsp.error_description.isEmpty() ? rsp.error : rsp.error_description));
|
||||
return;
|
||||
}
|
||||
if (!m_task->wasSuccessful() || m_task->error() != QNetworkReply::NoError) {
|
||||
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Failed to retrieve device authorization"));
|
||||
qDebug() << *m_response;
|
||||
return;
|
||||
}
|
||||
|
||||
if (rsp.device_code.isEmpty() || rsp.user_code.isEmpty() || rsp.verification_uri.isEmpty() || rsp.expires_in == 0) {
|
||||
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Device authorization failed: required fields missing"));
|
||||
return;
|
||||
}
|
||||
if (rsp.interval != 0) {
|
||||
interval = rsp.interval;
|
||||
}
|
||||
m_device_code = rsp.device_code;
|
||||
emit authorizeWithBrowser(rsp.verification_uri, rsp.user_code, rsp.expires_in);
|
||||
m_expiration_timer.setTimerType(Qt::VeryCoarseTimer);
|
||||
m_expiration_timer.setInterval(rsp.expires_in * 1000);
|
||||
m_expiration_timer.setSingleShot(true);
|
||||
connect(&m_expiration_timer, &QTimer::timeout, this, &MSADeviceCodeStep::abort);
|
||||
m_expiration_timer.start();
|
||||
|
||||
m_pool_timer.setTimerType(Qt::VeryCoarseTimer);
|
||||
m_pool_timer.setSingleShot(true);
|
||||
startPoolTimer();
|
||||
}
|
||||
|
||||
void MSADeviceCodeStep::abort()
|
||||
{
|
||||
m_expiration_timer.stop();
|
||||
m_pool_timer.stop();
|
||||
if (m_task) {
|
||||
m_task->abort();
|
||||
}
|
||||
m_is_aborted = true;
|
||||
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Task aborted"));
|
||||
}
|
||||
|
||||
void MSADeviceCodeStep::startPoolTimer()
|
||||
{
|
||||
if (m_is_aborted) {
|
||||
return;
|
||||
}
|
||||
m_pool_timer.setInterval(interval * 1000);
|
||||
connect(&m_pool_timer, &QTimer::timeout, this, &MSADeviceCodeStep::authenticateUser);
|
||||
m_pool_timer.start();
|
||||
}
|
||||
|
||||
void MSADeviceCodeStep::authenticateUser()
|
||||
{
|
||||
QUrlQuery data;
|
||||
data.addQueryItem("client_id", m_clientId);
|
||||
data.addQueryItem("grant_type", "urn:ietf:params:oauth:grant-type:device_code");
|
||||
data.addQueryItem("device_code", m_device_code);
|
||||
auto payload = data.query(QUrl::FullyEncoded).toUtf8();
|
||||
QUrl url("https://login.microsoftonline.com/consumers/oauth2/v2.0/token");
|
||||
auto headers = QList<Net::HeaderPair>{
|
||||
{ "Content-Type", "application/x-www-form-urlencoded" },
|
||||
{ "Accept", "application/json" },
|
||||
};
|
||||
m_response.reset(new QByteArray());
|
||||
m_task = Net::Upload::makeByteArray(url, m_response, payload);
|
||||
m_task->addHeaderProxy(new Net::StaticHeaderProxy(headers));
|
||||
|
||||
connect(m_task.get(), &Task::finished, this, &MSADeviceCodeStep::authenticationFinished);
|
||||
|
||||
m_task->setNetwork(APPLICATION->network());
|
||||
m_task->start();
|
||||
}
|
||||
|
||||
struct AuthenticationResponse {
|
||||
QString access_token;
|
||||
QString token_type;
|
||||
QString refresh_token;
|
||||
int expires_in;
|
||||
|
||||
QString error;
|
||||
QString error_description;
|
||||
|
||||
QVariantMap extra;
|
||||
};
|
||||
|
||||
AuthenticationResponse parseAuthenticationResponse(const QByteArray& data)
|
||||
{
|
||||
QJsonParseError err;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(data, &err);
|
||||
if (err.error != QJsonParseError::NoError) {
|
||||
qWarning() << "Failed to parse device autorization response due to err:" << err.errorString();
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!doc.isObject()) {
|
||||
qWarning() << "Device autorization response is not an object";
|
||||
return {};
|
||||
}
|
||||
auto obj = doc.object();
|
||||
return { Json::ensureString(obj, "access_token"),
|
||||
Json::ensureString(obj, "token_type"),
|
||||
Json::ensureString(obj, "refresh_token"),
|
||||
Json::ensureInteger(obj, "expires_in"),
|
||||
Json::ensureString(obj, "error"),
|
||||
Json::ensureString(obj, "error_description"),
|
||||
obj.toVariantMap() };
|
||||
}
|
||||
|
||||
void MSADeviceCodeStep::authenticationFinished()
|
||||
{
|
||||
if (m_task->error() == QNetworkReply::TimeoutError) {
|
||||
// rfc8628#section-3.5
|
||||
// "On encountering a connection timeout, clients MUST unilaterally
|
||||
// reduce their polling frequency before retrying. The use of an
|
||||
// exponential backoff algorithm to achieve this, such as doubling the
|
||||
// polling interval on each such connection timeout, is RECOMMENDED."
|
||||
interval *= 2;
|
||||
startPoolTimer();
|
||||
return;
|
||||
}
|
||||
auto rsp = parseAuthenticationResponse(*m_response);
|
||||
if (rsp.error == "slow_down") {
|
||||
// rfc8628#section-3.5
|
||||
// "A variant of 'authorization_pending', the authorization request is
|
||||
// still pending and polling should continue, but the interval MUST
|
||||
// be increased by 5 seconds for this and all subsequent requests."
|
||||
interval += 5;
|
||||
startPoolTimer();
|
||||
return;
|
||||
}
|
||||
if (rsp.error == "authorization_pending") {
|
||||
// keep trying - rfc8628#section-3.5
|
||||
// "The authorization request is still pending as the end user hasn't
|
||||
// yet completed the user-interaction steps (Section 3.3)."
|
||||
startPoolTimer();
|
||||
return;
|
||||
}
|
||||
if (!rsp.error.isEmpty() || !rsp.error_description.isEmpty()) {
|
||||
qWarning() << "Device Access failed:" << rsp.error;
|
||||
emit finished(AccountTaskState::STATE_FAILED_HARD,
|
||||
tr("Device Access failed: %1").arg(rsp.error_description.isEmpty() ? rsp.error : rsp.error_description));
|
||||
return;
|
||||
}
|
||||
if (!m_task->wasSuccessful() || m_task->error() != QNetworkReply::NoError) {
|
||||
startPoolTimer(); // it failed so just try again without increasing the interval
|
||||
return;
|
||||
}
|
||||
|
||||
m_expiration_timer.stop();
|
||||
m_data->msaClientID = m_clientId;
|
||||
m_data->msaToken.issueInstant = QDateTime::currentDateTimeUtc();
|
||||
m_data->msaToken.notAfter = QDateTime::currentDateTime().addSecs(rsp.expires_in);
|
||||
m_data->msaToken.extra = rsp.extra;
|
||||
m_data->msaToken.refresh_token = rsp.refresh_token;
|
||||
m_data->msaToken.token = rsp.access_token;
|
||||
emit finished(AccountTaskState::STATE_WORKING, tr("Got"));
|
||||
}
|
76
launcher/minecraft/auth/steps/MSADeviceCodeStep.h
Normal file
76
launcher/minecraft/auth/steps/MSADeviceCodeStep.h
Normal file
@ -0,0 +1,76 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (c) 2024 Trial97 <alexandru.tripon97@gmail.com>
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This file incorporates work covered by the following copyright and
|
||||
* permission notice:
|
||||
*
|
||||
* Copyright 2013-2021 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <QObject>
|
||||
#include <QTimer>
|
||||
|
||||
#include "minecraft/auth/AuthStep.h"
|
||||
#include "net/Upload.h"
|
||||
|
||||
class MSADeviceCodeStep : public AuthStep {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit MSADeviceCodeStep(AccountData* data);
|
||||
virtual ~MSADeviceCodeStep() noexcept = default;
|
||||
|
||||
void perform() override;
|
||||
|
||||
QString describe() override;
|
||||
|
||||
public slots:
|
||||
void abort();
|
||||
|
||||
signals:
|
||||
void authorizeWithBrowser(QString url, QString code, int expiresIn);
|
||||
|
||||
private slots:
|
||||
void deviceAutorizationFinished();
|
||||
void startPoolTimer();
|
||||
void authenticateUser();
|
||||
void authenticationFinished();
|
||||
|
||||
private:
|
||||
QString m_clientId;
|
||||
QString m_device_code;
|
||||
bool m_is_aborted = false;
|
||||
int interval = 5;
|
||||
|
||||
QTimer m_pool_timer;
|
||||
QTimer m_expiration_timer;
|
||||
|
||||
std::shared_ptr<QByteArray> m_response;
|
||||
Net::Upload::Ptr m_task;
|
||||
};
|
@ -35,123 +35,74 @@
|
||||
|
||||
#include "MSAStep.h"
|
||||
|
||||
#include <QtNetworkAuth/qoauthhttpserverreplyhandler.h>
|
||||
#include <QAbstractOAuth2>
|
||||
#include <QNetworkRequest>
|
||||
|
||||
#include "BuildConfig.h"
|
||||
#include "minecraft/auth/AuthRequest.h"
|
||||
#include "minecraft/auth/Parsers.h"
|
||||
|
||||
#include "Application.h"
|
||||
#include "Logging.h"
|
||||
|
||||
using OAuth2 = Katabasis::DeviceFlow;
|
||||
using Activity = Katabasis::Activity;
|
||||
|
||||
MSAStep::MSAStep(AccountData* data, Action action) : AuthStep(data), m_action(action)
|
||||
MSAStep::MSAStep(AccountData* data, bool silent) : AuthStep(data), m_silent(silent)
|
||||
{
|
||||
m_clientId = APPLICATION->getMSAClientID();
|
||||
OAuth2::Options opts;
|
||||
opts.scope = "XboxLive.signin offline_access";
|
||||
opts.clientIdentifier = m_clientId;
|
||||
opts.authorizationUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode";
|
||||
opts.accessTokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
|
||||
|
||||
// FIXME: OAuth2 is not aware of our fancy shared pointers
|
||||
m_oauth2 = new OAuth2(opts, m_data->msaToken, this, APPLICATION->network().get());
|
||||
auto replyHandler = new QOAuthHttpServerReplyHandler(1337, this);
|
||||
replyHandler->setCallbackText(
|
||||
" <iframe src=\"https://prismlauncher.org/successful-login\" title=\"PrismLauncher Microsoft login\" style=\"position:fixed; "
|
||||
"top:0; left:0; bottom:0; right:0; width:100%; height:100%; border:none; margin:0; padding:0; overflow:hidden; "
|
||||
"z-index:999999;\"/> ");
|
||||
oauth2.setReplyHandler(replyHandler);
|
||||
oauth2.setAuthorizationUrl(QUrl("https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize"));
|
||||
oauth2.setAccessTokenUrl(QUrl("https://login.microsoftonline.com/consumers/oauth2/v2.0/token"));
|
||||
oauth2.setScope("XboxLive.SignIn XboxLive.offline_access");
|
||||
oauth2.setClientIdentifier(m_clientId);
|
||||
oauth2.setNetworkAccessManager(APPLICATION->network().get());
|
||||
|
||||
connect(m_oauth2, &OAuth2::activityChanged, this, &MSAStep::onOAuthActivityChanged);
|
||||
connect(m_oauth2, &OAuth2::showVerificationUriAndCode, this, &MSAStep::showVerificationUriAndCode);
|
||||
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::granted, this, [this] {
|
||||
m_data->msaClientID = oauth2.clientIdentifier();
|
||||
m_data->msaToken.issueInstant = QDateTime::currentDateTimeUtc();
|
||||
m_data->msaToken.notAfter = oauth2.expirationAt();
|
||||
m_data->msaToken.extra = oauth2.extraTokens();
|
||||
m_data->msaToken.refresh_token = oauth2.refreshToken();
|
||||
m_data->msaToken.token = oauth2.token();
|
||||
emit finished(AccountTaskState::STATE_WORKING, tr("Got "));
|
||||
});
|
||||
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, this, &MSAStep::authorizeWithBrowser);
|
||||
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::requestFailed, this, [this](const QAbstractOAuth2::Error err) {
|
||||
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Microsoft user authentication failed."));
|
||||
});
|
||||
|
||||
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::extraTokensChanged, this,
|
||||
[this](const QVariantMap& tokens) { m_data->msaToken.extra = tokens; });
|
||||
|
||||
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::clientIdentifierChanged, this,
|
||||
[this](const QString& clientIdentifier) { m_data->msaClientID = clientIdentifier; });
|
||||
}
|
||||
|
||||
MSAStep::~MSAStep() noexcept = default;
|
||||
|
||||
QString MSAStep::describe()
|
||||
{
|
||||
return tr("Logging in with Microsoft account.");
|
||||
}
|
||||
|
||||
void MSAStep::rehydrate()
|
||||
{
|
||||
switch (m_action) {
|
||||
case Refresh: {
|
||||
// TODO: check the tokens and see if they are old (older than a day)
|
||||
return;
|
||||
}
|
||||
case Login: {
|
||||
// NOOP
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MSAStep::perform()
|
||||
{
|
||||
switch (m_action) {
|
||||
case Refresh: {
|
||||
if (m_data->msaClientID != m_clientId) {
|
||||
emit hideVerificationUriAndCode();
|
||||
emit finished(AccountTaskState::STATE_DISABLED,
|
||||
tr("Microsoft user authentication failed - client identification has changed."));
|
||||
}
|
||||
m_oauth2->refresh();
|
||||
return;
|
||||
if (m_silent) {
|
||||
if (m_data->msaClientID != m_clientId) {
|
||||
emit finished(AccountTaskState::STATE_DISABLED,
|
||||
tr("Microsoft user authentication failed - client identification has changed."));
|
||||
}
|
||||
case Login: {
|
||||
QVariantMap extraOpts;
|
||||
extraOpts["prompt"] = "select_account";
|
||||
m_oauth2->setExtraRequestParams(extraOpts);
|
||||
oauth2.setRefreshToken(m_data->msaToken.refresh_token);
|
||||
oauth2.refreshAccessToken();
|
||||
} else {
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) // QMultiMap param changed in 6.0
|
||||
oauth2.setModifyParametersFunction([](QAbstractOAuth::Stage stage, QMultiMap<QString, QVariant>* map) {
|
||||
#else
|
||||
oauth2.setModifyParametersFunction([](QAbstractOAuth::Stage stage, QMap<QString, QVariant>* map) {
|
||||
#endif
|
||||
map->insert("prompt", "select_account");
|
||||
});
|
||||
|
||||
*m_data = AccountData();
|
||||
m_data->msaClientID = m_clientId;
|
||||
m_oauth2->login();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MSAStep::onOAuthActivityChanged(Katabasis::Activity activity)
|
||||
{
|
||||
switch (activity) {
|
||||
case Katabasis::Activity::Idle:
|
||||
case Katabasis::Activity::LoggingIn:
|
||||
case Katabasis::Activity::Refreshing:
|
||||
case Katabasis::Activity::LoggingOut: {
|
||||
// We asked it to do something, it's doing it. Nothing to act upon.
|
||||
return;
|
||||
}
|
||||
case Katabasis::Activity::Succeeded: {
|
||||
// Succeeded or did not invalidate tokens
|
||||
emit hideVerificationUriAndCode();
|
||||
QVariantMap extraTokens = m_oauth2->extraTokens();
|
||||
if (!extraTokens.isEmpty()) {
|
||||
qCDebug(authCredentials()) << "Extra tokens in response:";
|
||||
foreach (QString key, extraTokens.keys()) {
|
||||
qCDebug(authCredentials()) << "\t" << key << ":" << extraTokens.value(key);
|
||||
}
|
||||
}
|
||||
emit finished(AccountTaskState::STATE_WORKING, tr("Got "));
|
||||
return;
|
||||
}
|
||||
case Katabasis::Activity::FailedSoft: {
|
||||
// NOTE: soft error in the first step means 'offline'
|
||||
emit hideVerificationUriAndCode();
|
||||
emit finished(AccountTaskState::STATE_OFFLINE, tr("Microsoft user authentication ended with a network error."));
|
||||
return;
|
||||
}
|
||||
case Katabasis::Activity::FailedGone: {
|
||||
emit hideVerificationUriAndCode();
|
||||
emit finished(AccountTaskState::STATE_FAILED_GONE, tr("Microsoft user authentication failed - user no longer exists."));
|
||||
return;
|
||||
}
|
||||
case Katabasis::Activity::FailedHard: {
|
||||
emit hideVerificationUriAndCode();
|
||||
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Microsoft user authentication failed."));
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
emit hideVerificationUriAndCode();
|
||||
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Microsoft user authentication completed with an unrecognized result."));
|
||||
return;
|
||||
}
|
||||
*m_data = AccountData();
|
||||
m_data->msaClientID = m_clientId;
|
||||
oauth2.grant();
|
||||
}
|
||||
}
|
||||
|
@ -36,30 +36,24 @@
|
||||
#pragma once
|
||||
#include <QObject>
|
||||
|
||||
#include "QObjectPtr.h"
|
||||
#include "minecraft/auth/AuthStep.h"
|
||||
|
||||
#include <katabasis/DeviceFlow.h>
|
||||
|
||||
#include <QtNetworkAuth/qoauth2authorizationcodeflow.h>
|
||||
class MSAStep : public AuthStep {
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum Action { Refresh, Login };
|
||||
|
||||
public:
|
||||
explicit MSAStep(AccountData* data, Action action);
|
||||
virtual ~MSAStep() noexcept;
|
||||
explicit MSAStep(AccountData* data, bool silent = false);
|
||||
virtual ~MSAStep() noexcept = default;
|
||||
|
||||
void perform() override;
|
||||
void rehydrate() override;
|
||||
|
||||
QString describe() override;
|
||||
|
||||
private slots:
|
||||
void onOAuthActivityChanged(Katabasis::Activity activity);
|
||||
signals:
|
||||
void authorizeWithBrowser(const QUrl& url);
|
||||
|
||||
private:
|
||||
Katabasis::DeviceFlow* m_oauth2 = nullptr;
|
||||
Action m_action;
|
||||
bool m_silent;
|
||||
QString m_clientId;
|
||||
QOAuth2AuthorizationCodeFlow oauth2;
|
||||
};
|
||||
|
@ -2,15 +2,13 @@
|
||||
|
||||
#include <QNetworkRequest>
|
||||
|
||||
#include "Logging.h"
|
||||
#include "minecraft/auth/AuthRequest.h"
|
||||
#include "Application.h"
|
||||
#include "minecraft/auth/Parsers.h"
|
||||
#include "net/NetUtils.h"
|
||||
#include "net/StaticHeaderProxy.h"
|
||||
|
||||
MinecraftProfileStep::MinecraftProfileStep(AccountData* data) : AuthStep(data) {}
|
||||
|
||||
MinecraftProfileStep::~MinecraftProfileStep() noexcept = default;
|
||||
|
||||
QString MinecraftProfileStep::describe()
|
||||
{
|
||||
return tr("Fetching the Minecraft profile.");
|
||||
@ -18,52 +16,47 @@ QString MinecraftProfileStep::describe()
|
||||
|
||||
void MinecraftProfileStep::perform()
|
||||
{
|
||||
auto url = QUrl("https://api.minecraftservices.com/minecraft/profile");
|
||||
QNetworkRequest request = QNetworkRequest(url);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8());
|
||||
QUrl url("https://api.minecraftservices.com/minecraft/profile");
|
||||
auto headers = QList<Net::HeaderPair>{ { "Content-Type", "application/json" },
|
||||
{ "Accept", "application/json" },
|
||||
{ "Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8() } };
|
||||
|
||||
AuthRequest* requestor = new AuthRequest(this);
|
||||
connect(requestor, &AuthRequest::finished, this, &MinecraftProfileStep::onRequestDone);
|
||||
requestor->get(request);
|
||||
m_response.reset(new QByteArray());
|
||||
m_task = Net::Download::makeByteArray(url, m_response);
|
||||
m_task->addHeaderProxy(new Net::StaticHeaderProxy(headers));
|
||||
|
||||
connect(m_task.get(), &Task::finished, this, &MinecraftProfileStep::onRequestDone);
|
||||
|
||||
m_task->setNetwork(APPLICATION->network());
|
||||
m_task->start();
|
||||
}
|
||||
|
||||
void MinecraftProfileStep::rehydrate()
|
||||
void MinecraftProfileStep::onRequestDone()
|
||||
{
|
||||
// NOOP, for now. We only save bools and there's nothing to check.
|
||||
}
|
||||
|
||||
void MinecraftProfileStep::onRequestDone(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers)
|
||||
{
|
||||
auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
|
||||
requestor->deleteLater();
|
||||
|
||||
qCDebug(authCredentials()) << data;
|
||||
if (error == QNetworkReply::ContentNotFoundError) {
|
||||
if (m_task->error() == QNetworkReply::ContentNotFoundError) {
|
||||
// NOTE: Succeed even if we do not have a profile. This is a valid account state.
|
||||
m_data->minecraftProfile = MinecraftProfile();
|
||||
emit finished(AccountTaskState::STATE_SUCCEEDED, tr("Account has no Minecraft profile."));
|
||||
return;
|
||||
}
|
||||
if (error != QNetworkReply::NoError) {
|
||||
if (m_task->error() != QNetworkReply::NoError) {
|
||||
qWarning() << "Error getting profile:";
|
||||
qWarning() << " HTTP Status: " << requestor->httpStatus_;
|
||||
qWarning() << " Internal error no.: " << error;
|
||||
qWarning() << " Error string: " << requestor->errorString_;
|
||||
qWarning() << " HTTP Status: " << m_task->replyStatusCode();
|
||||
qWarning() << " Internal error no.: " << m_task->error();
|
||||
qWarning() << " Error string: " << m_task->errorString();
|
||||
|
||||
qWarning() << " Response:";
|
||||
qWarning() << QString::fromUtf8(data);
|
||||
qWarning() << QString::fromUtf8(*m_response);
|
||||
|
||||
if (Net::isApplicationError(error)) {
|
||||
if (Net::isApplicationError(m_task->error())) {
|
||||
emit finished(AccountTaskState::STATE_FAILED_SOFT,
|
||||
tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_));
|
||||
tr("Minecraft Java profile acquisition failed: %1").arg(m_task->errorString()));
|
||||
} else {
|
||||
emit finished(AccountTaskState::STATE_OFFLINE,
|
||||
tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_));
|
||||
emit finished(AccountTaskState::STATE_OFFLINE, tr("Minecraft Java profile acquisition failed: %1").arg(m_task->errorString()));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!Parsers::parseMinecraftProfile(data, m_data->minecraftProfile)) {
|
||||
if (!Parsers::parseMinecraftProfile(*m_response, m_data->minecraftProfile)) {
|
||||
m_data->minecraftProfile = MinecraftProfile();
|
||||
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Minecraft Java profile response could not be parsed"));
|
||||
return;
|
||||
|
@ -1,21 +1,25 @@
|
||||
#pragma once
|
||||
#include <QObject>
|
||||
#include <memory>
|
||||
|
||||
#include "QObjectPtr.h"
|
||||
#include "minecraft/auth/AuthStep.h"
|
||||
#include "net/Download.h"
|
||||
|
||||
class MinecraftProfileStep : public AuthStep {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MinecraftProfileStep(AccountData* data);
|
||||
virtual ~MinecraftProfileStep() noexcept;
|
||||
virtual ~MinecraftProfileStep() noexcept = default;
|
||||
|
||||
void perform() override;
|
||||
void rehydrate() override;
|
||||
|
||||
QString describe() override;
|
||||
|
||||
private slots:
|
||||
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
|
||||
void onRequestDone();
|
||||
|
||||
private:
|
||||
std::shared_ptr<QByteArray> m_response;
|
||||
Net::Download::Ptr m_task;
|
||||
};
|
||||
|
@ -1,21 +0,0 @@
|
||||
#include "OfflineStep.h"
|
||||
|
||||
#include "Application.h"
|
||||
|
||||
OfflineStep::OfflineStep(AccountData* data) : AuthStep(data) {}
|
||||
OfflineStep::~OfflineStep() noexcept = default;
|
||||
|
||||
QString OfflineStep::describe()
|
||||
{
|
||||
return tr("Creating offline account.");
|
||||
}
|
||||
|
||||
void OfflineStep::rehydrate()
|
||||
{
|
||||
// NOOP
|
||||
}
|
||||
|
||||
void OfflineStep::perform()
|
||||
{
|
||||
emit finished(AccountTaskState::STATE_WORKING, tr("Created offline account."));
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
#pragma once
|
||||
#include <QObject>
|
||||
|
||||
#include "QObjectPtr.h"
|
||||
#include "minecraft/auth/AuthStep.h"
|
||||
|
||||
#include <katabasis/DeviceFlow.h>
|
||||
|
||||
class OfflineStep : public AuthStep {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit OfflineStep(AccountData* data);
|
||||
virtual ~OfflineStep() noexcept;
|
||||
|
||||
void perform() override;
|
||||
void rehydrate() override;
|
||||
|
||||
QString describe() override;
|
||||
};
|
@ -4,27 +4,22 @@
|
||||
#include <QJsonParseError>
|
||||
#include <QNetworkRequest>
|
||||
|
||||
#include "Application.h"
|
||||
#include "Logging.h"
|
||||
#include "minecraft/auth/AuthRequest.h"
|
||||
#include "minecraft/auth/Parsers.h"
|
||||
#include "net/NetUtils.h"
|
||||
#include "net/StaticHeaderProxy.h"
|
||||
#include "net/Upload.h"
|
||||
|
||||
XboxAuthorizationStep::XboxAuthorizationStep(AccountData* data, Katabasis::Token* token, QString relyingParty, QString authorizationKind)
|
||||
XboxAuthorizationStep::XboxAuthorizationStep(AccountData* data, Token* token, QString relyingParty, QString authorizationKind)
|
||||
: AuthStep(data), m_token(token), m_relyingParty(relyingParty), m_authorizationKind(authorizationKind)
|
||||
{}
|
||||
|
||||
XboxAuthorizationStep::~XboxAuthorizationStep() noexcept = default;
|
||||
|
||||
QString XboxAuthorizationStep::describe()
|
||||
{
|
||||
return tr("Getting authorization to access %1 services.").arg(m_authorizationKind);
|
||||
}
|
||||
|
||||
void XboxAuthorizationStep::rehydrate()
|
||||
{
|
||||
// FIXME: check if the tokens are good?
|
||||
}
|
||||
|
||||
void XboxAuthorizationStep::perform()
|
||||
{
|
||||
QString xbox_auth_template = R"XXX(
|
||||
@ -41,40 +36,44 @@ void XboxAuthorizationStep::perform()
|
||||
)XXX";
|
||||
auto xbox_auth_data = xbox_auth_template.arg(m_data->userToken.token, m_relyingParty);
|
||||
// http://xboxlive.com
|
||||
QNetworkRequest request = QNetworkRequest(QUrl("https://xsts.auth.xboxlive.com/xsts/authorize"));
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
request.setRawHeader("Accept", "application/json");
|
||||
AuthRequest* requestor = new AuthRequest(this);
|
||||
connect(requestor, &AuthRequest::finished, this, &XboxAuthorizationStep::onRequestDone);
|
||||
requestor->post(request, xbox_auth_data.toUtf8());
|
||||
QUrl url("https://xsts.auth.xboxlive.com/xsts/authorize");
|
||||
auto headers = QList<Net::HeaderPair>{
|
||||
{ "Content-Type", "application/json" },
|
||||
{ "Accept", "application/json" },
|
||||
};
|
||||
m_response.reset(new QByteArray());
|
||||
m_task = Net::Upload::makeByteArray(url, m_response, xbox_auth_data.toUtf8());
|
||||
m_task->addHeaderProxy(new Net::StaticHeaderProxy(headers));
|
||||
|
||||
connect(m_task.get(), &Task::finished, this, &XboxAuthorizationStep::onRequestDone);
|
||||
|
||||
m_task->setNetwork(APPLICATION->network());
|
||||
m_task->start();
|
||||
qDebug() << "Getting authorization token for " << m_relyingParty;
|
||||
}
|
||||
|
||||
void XboxAuthorizationStep::onRequestDone(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers)
|
||||
void XboxAuthorizationStep::onRequestDone()
|
||||
{
|
||||
auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
|
||||
requestor->deleteLater();
|
||||
|
||||
qCDebug(authCredentials()) << data;
|
||||
if (error != QNetworkReply::NoError) {
|
||||
qWarning() << "Reply error:" << error;
|
||||
if (Net::isApplicationError(error)) {
|
||||
if (!processSTSError(error, data, headers)) {
|
||||
qCDebug(authCredentials()) << *m_response;
|
||||
if (m_task->error() != QNetworkReply::NoError) {
|
||||
qWarning() << "Reply error:" << m_task->error();
|
||||
if (Net::isApplicationError(m_task->error())) {
|
||||
if (!processSTSError()) {
|
||||
emit finished(AccountTaskState::STATE_FAILED_SOFT,
|
||||
tr("Failed to get authorization for %1 services. Error %2.").arg(m_authorizationKind, error));
|
||||
tr("Failed to get authorization for %1 services. Error %2.").arg(m_authorizationKind, m_task->error()));
|
||||
} else {
|
||||
emit finished(AccountTaskState::STATE_FAILED_SOFT,
|
||||
tr("Unknown STS error for %1 services: %2").arg(m_authorizationKind, requestor->errorString_));
|
||||
tr("Unknown STS error for %1 services: %2").arg(m_authorizationKind, m_task->errorString()));
|
||||
}
|
||||
} else {
|
||||
emit finished(AccountTaskState::STATE_OFFLINE,
|
||||
tr("Failed to get authorization for %1 services: %2").arg(m_authorizationKind, requestor->errorString_));
|
||||
tr("Failed to get authorization for %1 services: %2").arg(m_authorizationKind, m_task->errorString()));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Katabasis::Token temp;
|
||||
if (!Parsers::parseXTokenResponse(data, temp, m_authorizationKind)) {
|
||||
Token temp;
|
||||
if (!Parsers::parseXTokenResponse(*m_response, temp, m_authorizationKind)) {
|
||||
emit finished(AccountTaskState::STATE_FAILED_SOFT,
|
||||
tr("Could not parse authorization response for access to %1 services.").arg(m_authorizationKind));
|
||||
return;
|
||||
@ -91,11 +90,11 @@ void XboxAuthorizationStep::onRequestDone(QNetworkReply::NetworkError error, QBy
|
||||
emit finished(AccountTaskState::STATE_WORKING, tr("Got authorization to access %1").arg(m_relyingParty));
|
||||
}
|
||||
|
||||
bool XboxAuthorizationStep::processSTSError(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers)
|
||||
bool XboxAuthorizationStep::processSTSError()
|
||||
{
|
||||
if (error == QNetworkReply::AuthenticationRequiredError) {
|
||||
if (m_task->error() == QNetworkReply::AuthenticationRequiredError) {
|
||||
QJsonParseError jsonError;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
|
||||
QJsonDocument doc = QJsonDocument::fromJson(*m_response, &jsonError);
|
||||
if (jsonError.error) {
|
||||
qWarning() << "Cannot parse error XSTS response as JSON: " << jsonError.errorString();
|
||||
emit finished(AccountTaskState::STATE_FAILED_SOFT,
|
||||
@ -126,7 +125,35 @@ bool XboxAuthorizationStep::processSTSError(QNetworkReply::NetworkError error, Q
|
||||
emit finished(
|
||||
AccountTaskState::STATE_FAILED_SOFT,
|
||||
tr("This Microsoft account is underaged and is not linked to a family.\n\nPlease set up your account according to %1.")
|
||||
.arg("<a href=\"https://help.minecraft.net/hc/en-us/articles/4403181904525\">help.minecraft.net</a>"));
|
||||
.arg("<a href=\"https://help.minecraft.net/hc/en-us/articles/4408968616077\">help.minecraft.net</a>"));
|
||||
return true;
|
||||
}
|
||||
// the following codes where copied from: https://github.com/PrismarineJS/prismarine-auth/pull/44
|
||||
case 2148916236: {
|
||||
emit finished(AccountTaskState::STATE_FAILED_SOFT,
|
||||
tr("This Microsoft account requires proof of age to play. Please login to %1 to provide proof of age.")
|
||||
.arg("<a href=\"https://login.live.com/login.srf\">login.live.com</a>"));
|
||||
return true;
|
||||
}
|
||||
case 2148916237:
|
||||
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("This Microsoft account has reached its limit for playtime. This "
|
||||
"Microsoft account has been blocked from logging in."));
|
||||
return true;
|
||||
case 2148916227: {
|
||||
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("This Microsoft account was banned by Xbox for violating one or more "
|
||||
"Community Standards for Xbox and is unable to be used."));
|
||||
return true;
|
||||
}
|
||||
case 2148916229: {
|
||||
emit finished(AccountTaskState::STATE_FAILED_SOFT,
|
||||
tr("This Microsoft account is currently restricted and your guardian has not given you permission to play "
|
||||
"online. Login to %1 and have your guardian change your permissions.")
|
||||
.arg("<a href=\"https://account.microsoft.com/family/\">account.microsoft.com</a>"));
|
||||
return true;
|
||||
}
|
||||
case 2148916234: {
|
||||
emit finished(AccountTaskState::STATE_FAILED_SOFT,
|
||||
tr("This Microsoft account has not accepted Xbox's Terms of Service. Please login and accept them."));
|
||||
return true;
|
||||
}
|
||||
default: {
|
||||
|
@ -1,29 +1,32 @@
|
||||
#pragma once
|
||||
#include <QObject>
|
||||
#include <memory>
|
||||
|
||||
#include "QObjectPtr.h"
|
||||
#include "minecraft/auth/AuthStep.h"
|
||||
#include "net/Upload.h"
|
||||
|
||||
class XboxAuthorizationStep : public AuthStep {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit XboxAuthorizationStep(AccountData* data, Katabasis::Token* token, QString relyingParty, QString authorizationKind);
|
||||
virtual ~XboxAuthorizationStep() noexcept;
|
||||
explicit XboxAuthorizationStep(AccountData* data, Token* token, QString relyingParty, QString authorizationKind);
|
||||
virtual ~XboxAuthorizationStep() noexcept = default;
|
||||
|
||||
void perform() override;
|
||||
void rehydrate() override;
|
||||
|
||||
QString describe() override;
|
||||
|
||||
private:
|
||||
bool processSTSError(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers);
|
||||
bool processSTSError();
|
||||
|
||||
private slots:
|
||||
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
|
||||
void onRequestDone();
|
||||
|
||||
private:
|
||||
Katabasis::Token* m_token;
|
||||
Token* m_token;
|
||||
QString m_relyingParty;
|
||||
QString m_authorizationKind;
|
||||
|
||||
std::shared_ptr<QByteArray> m_response;
|
||||
Net::Upload::Ptr m_task;
|
||||
};
|
||||
|
@ -3,28 +3,21 @@
|
||||
#include <QNetworkRequest>
|
||||
#include <QUrlQuery>
|
||||
|
||||
#include "Application.h"
|
||||
#include "Logging.h"
|
||||
#include "minecraft/auth/AuthRequest.h"
|
||||
#include "minecraft/auth/Parsers.h"
|
||||
#include "net/NetUtils.h"
|
||||
#include "net/StaticHeaderProxy.h"
|
||||
|
||||
XboxProfileStep::XboxProfileStep(AccountData* data) : AuthStep(data) {}
|
||||
|
||||
XboxProfileStep::~XboxProfileStep() noexcept = default;
|
||||
|
||||
QString XboxProfileStep::describe()
|
||||
{
|
||||
return tr("Fetching Xbox profile.");
|
||||
}
|
||||
|
||||
void XboxProfileStep::rehydrate()
|
||||
{
|
||||
// NOOP, for now. We only save bools and there's nothing to check.
|
||||
}
|
||||
|
||||
void XboxProfileStep::perform()
|
||||
{
|
||||
auto url = QUrl("https://profile.xboxlive.com/users/me/profile/settings");
|
||||
QUrl url("https://profile.xboxlive.com/users/me/profile/settings");
|
||||
QUrlQuery q;
|
||||
q.addQueryItem("settings",
|
||||
"GameDisplayName,AppDisplayName,AppDisplayPicRaw,GameDisplayPicRaw,"
|
||||
@ -33,36 +26,38 @@ void XboxProfileStep::perform()
|
||||
"PreferredColor,Location,Bio,Watermarks,"
|
||||
"RealName,RealNameOverride,IsQuarantined");
|
||||
url.setQuery(q);
|
||||
auto headers = QList<Net::HeaderPair>{
|
||||
{ "Content-Type", "application/json" },
|
||||
{ "Accept", "application/json" },
|
||||
{ "x-xbl-contract-version", "3" },
|
||||
{ "Authorization", QString("XBL3.0 x=%1;%2").arg(m_data->userToken.extra["uhs"].toString(), m_data->xboxApiToken.token).toUtf8() }
|
||||
};
|
||||
|
||||
QNetworkRequest request = QNetworkRequest(url);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
request.setRawHeader("Accept", "application/json");
|
||||
request.setRawHeader("x-xbl-contract-version", "3");
|
||||
request.setRawHeader("Authorization",
|
||||
QString("XBL3.0 x=%1;%2").arg(m_data->userToken.extra["uhs"].toString(), m_data->xboxApiToken.token).toUtf8());
|
||||
AuthRequest* requestor = new AuthRequest(this);
|
||||
connect(requestor, &AuthRequest::finished, this, &XboxProfileStep::onRequestDone);
|
||||
requestor->get(request);
|
||||
m_response.reset(new QByteArray());
|
||||
m_task = Net::Download::makeByteArray(url, m_response);
|
||||
m_task->addHeaderProxy(new Net::StaticHeaderProxy(headers));
|
||||
|
||||
connect(m_task.get(), &Task::finished, this, &XboxProfileStep::onRequestDone);
|
||||
|
||||
m_task->setNetwork(APPLICATION->network());
|
||||
m_task->start();
|
||||
qDebug() << "Getting Xbox profile...";
|
||||
}
|
||||
|
||||
void XboxProfileStep::onRequestDone(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers)
|
||||
void XboxProfileStep::onRequestDone()
|
||||
{
|
||||
auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
|
||||
requestor->deleteLater();
|
||||
|
||||
if (error != QNetworkReply::NoError) {
|
||||
qWarning() << "Reply error:" << error;
|
||||
qCDebug(authCredentials()) << data;
|
||||
if (Net::isApplicationError(error)) {
|
||||
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to retrieve the Xbox profile: %1").arg(requestor->errorString_));
|
||||
if (m_task->error() != QNetworkReply::NoError) {
|
||||
qWarning() << "Reply error:" << m_task->error();
|
||||
qCDebug(authCredentials()) << *m_response;
|
||||
if (Net::isApplicationError(m_task->error())) {
|
||||
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to retrieve the Xbox profile: %1").arg(m_task->errorString()));
|
||||
} else {
|
||||
emit finished(AccountTaskState::STATE_OFFLINE, tr("Failed to retrieve the Xbox profile: %1").arg(requestor->errorString_));
|
||||
emit finished(AccountTaskState::STATE_OFFLINE, tr("Failed to retrieve the Xbox profile: %1").arg(m_task->errorString()));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(authCredentials()) << "XBox profile: " << data;
|
||||
qCDebug(authCredentials()) << "XBox profile: " << *m_response;
|
||||
|
||||
emit finished(AccountTaskState::STATE_WORKING, tr("Got Xbox profile"));
|
||||
}
|
||||
|
@ -1,21 +1,25 @@
|
||||
#pragma once
|
||||
#include <QObject>
|
||||
#include <memory>
|
||||
|
||||
#include "QObjectPtr.h"
|
||||
#include "minecraft/auth/AuthStep.h"
|
||||
#include "net/Download.h"
|
||||
|
||||
class XboxProfileStep : public AuthStep {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit XboxProfileStep(AccountData* data);
|
||||
virtual ~XboxProfileStep() noexcept;
|
||||
virtual ~XboxProfileStep() noexcept = default;
|
||||
|
||||
void perform() override;
|
||||
void rehydrate() override;
|
||||
|
||||
QString describe() override;
|
||||
|
||||
private slots:
|
||||
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
|
||||
void onRequestDone();
|
||||
|
||||
private:
|
||||
std::shared_ptr<QByteArray> m_response;
|
||||
Net::Download::Ptr m_task;
|
||||
};
|
||||
|
@ -2,24 +2,18 @@
|
||||
|
||||
#include <QNetworkRequest>
|
||||
|
||||
#include "minecraft/auth/AuthRequest.h"
|
||||
#include "Application.h"
|
||||
#include "minecraft/auth/Parsers.h"
|
||||
#include "net/NetUtils.h"
|
||||
#include "net/StaticHeaderProxy.h"
|
||||
|
||||
XboxUserStep::XboxUserStep(AccountData* data) : AuthStep(data) {}
|
||||
|
||||
XboxUserStep::~XboxUserStep() noexcept = default;
|
||||
|
||||
QString XboxUserStep::describe()
|
||||
{
|
||||
return tr("Logging in as an Xbox user.");
|
||||
}
|
||||
|
||||
void XboxUserStep::rehydrate()
|
||||
{
|
||||
// NOOP, for now. We only save bools and there's nothing to check.
|
||||
}
|
||||
|
||||
void XboxUserStep::perform()
|
||||
{
|
||||
QString xbox_auth_template = R"XXX(
|
||||
@ -35,36 +29,39 @@ void XboxUserStep::perform()
|
||||
)XXX";
|
||||
auto xbox_auth_data = xbox_auth_template.arg(m_data->msaToken.token);
|
||||
|
||||
QNetworkRequest request = QNetworkRequest(QUrl("https://user.auth.xboxlive.com/user/authenticate"));
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
request.setRawHeader("Accept", "application/json");
|
||||
// set contract-version header (prevent err 400 bad-request?)
|
||||
// https://learn.microsoft.com/en-us/gaming/gdk/_content/gc/reference/live/rest/additional/httpstandardheaders
|
||||
request.setRawHeader("x-xbl-contract-version", "1");
|
||||
QUrl url("https://user.auth.xboxlive.com/user/authenticate");
|
||||
auto headers = QList<Net::HeaderPair>{
|
||||
{ "Content-Type", "application/json" },
|
||||
{ "Accept", "application/json" },
|
||||
// set contract-version header (prevent err 400 bad-request?)
|
||||
// https://learn.microsoft.com/en-us/gaming/gdk/_content/gc/reference/live/rest/additional/httpstandardheaders
|
||||
{ "x-xbl-contract-version", "1" }
|
||||
};
|
||||
m_response.reset(new QByteArray());
|
||||
m_task = Net::Upload::makeByteArray(url, m_response, xbox_auth_data.toUtf8());
|
||||
m_task->addHeaderProxy(new Net::StaticHeaderProxy(headers));
|
||||
|
||||
auto* requestor = new AuthRequest(this);
|
||||
connect(requestor, &AuthRequest::finished, this, &XboxUserStep::onRequestDone);
|
||||
requestor->post(request, xbox_auth_data.toUtf8());
|
||||
connect(m_task.get(), &Task::finished, this, &XboxUserStep::onRequestDone);
|
||||
|
||||
m_task->setNetwork(APPLICATION->network());
|
||||
m_task->start();
|
||||
qDebug() << "First layer of XBox auth ... commencing.";
|
||||
}
|
||||
|
||||
void XboxUserStep::onRequestDone(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers)
|
||||
void XboxUserStep::onRequestDone()
|
||||
{
|
||||
auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
|
||||
requestor->deleteLater();
|
||||
|
||||
if (error != QNetworkReply::NoError) {
|
||||
qWarning() << "Reply error:" << error;
|
||||
if (Net::isApplicationError(error)) {
|
||||
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("XBox user authentication failed: %1").arg(requestor->errorString_));
|
||||
if (m_task->error() != QNetworkReply::NoError) {
|
||||
qWarning() << "Reply error:" << m_task->error();
|
||||
if (Net::isApplicationError(m_task->error())) {
|
||||
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("XBox user authentication failed: %1").arg(m_task->errorString()));
|
||||
} else {
|
||||
emit finished(AccountTaskState::STATE_OFFLINE, tr("XBox user authentication failed: %1").arg(requestor->errorString_));
|
||||
emit finished(AccountTaskState::STATE_OFFLINE, tr("XBox user authentication failed: %1").arg(m_task->errorString()));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Katabasis::Token temp;
|
||||
if (!Parsers::parseXTokenResponse(data, temp, "UToken")) {
|
||||
Token temp;
|
||||
if (!Parsers::parseXTokenResponse(*m_response, temp, "UToken")) {
|
||||
qWarning() << "Could not parse user authentication response...";
|
||||
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("XBox user authentication response could not be understood."));
|
||||
return;
|
||||
|
@ -1,21 +1,25 @@
|
||||
#pragma once
|
||||
#include <QObject>
|
||||
#include <memory>
|
||||
|
||||
#include "QObjectPtr.h"
|
||||
#include "minecraft/auth/AuthStep.h"
|
||||
#include "net/Upload.h"
|
||||
|
||||
class XboxUserStep : public AuthStep {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit XboxUserStep(AccountData* data);
|
||||
virtual ~XboxUserStep() noexcept;
|
||||
virtual ~XboxUserStep() noexcept = default;
|
||||
|
||||
void perform() override;
|
||||
void rehydrate() override;
|
||||
|
||||
QString describe() override;
|
||||
|
||||
private slots:
|
||||
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
|
||||
void onRequestDone();
|
||||
|
||||
private:
|
||||
std::shared_ptr<QByteArray> m_response;
|
||||
Net::Upload::Ptr m_task;
|
||||
};
|
||||
|
@ -79,6 +79,7 @@ void ExtractNatives::executeTask()
|
||||
auto settings = minecraftInstance->settings();
|
||||
|
||||
auto outputPath = minecraftInstance->getNativePath();
|
||||
FS::ensureFolderPathExists(outputPath);
|
||||
auto javaVersion = minecraftInstance->getJavaVersion();
|
||||
bool jniHackEnabled = javaVersion.major() >= 8;
|
||||
for (const auto& source : toExtract) {
|
||||
|
@ -16,8 +16,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <launch/LaunchStep.h>
|
||||
#include <memory>
|
||||
#include "minecraft/auth/AuthSession.h"
|
||||
|
||||
// FIXME: temporary wrapper for existing task.
|
||||
class ExtractNatives : public LaunchStep {
|
||||
|
@ -66,32 +66,6 @@ LauncherPartLaunch::LauncherPartLaunch(LaunchTask* parent) : LaunchStep(parent)
|
||||
connect(&m_process, &LoggedProcess::stateChanged, this, &LauncherPartLaunch::on_state);
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
// returns 8.3 file format from long path
|
||||
#include <windows.h>
|
||||
QString shortPathName(const QString& file)
|
||||
{
|
||||
auto input = file.toStdWString();
|
||||
std::wstring output;
|
||||
long length = GetShortPathNameW(input.c_str(), NULL, 0);
|
||||
// NOTE: this resizing might seem weird...
|
||||
// when GetShortPathNameW fails, it returns length including null character
|
||||
// when it succeeds, it returns length excluding null character
|
||||
// See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa364989(v=vs.85).aspx
|
||||
output.resize(length);
|
||||
GetShortPathNameW(input.c_str(), (LPWSTR)output.c_str(), length);
|
||||
output.resize(length - 1);
|
||||
QString ret = QString::fromStdWString(output);
|
||||
return ret;
|
||||
}
|
||||
#endif
|
||||
|
||||
// if the string survives roundtrip through local 8bit encoding...
|
||||
bool fitsInLocal8bit(const QString& string)
|
||||
{
|
||||
return string == QString::fromLocal8Bit(string.toLocal8Bit());
|
||||
}
|
||||
|
||||
void LauncherPartLaunch::executeTask()
|
||||
{
|
||||
QString jarPath = APPLICATION->getJarPath("NewLaunch.jar");
|
||||
@ -136,24 +110,15 @@ void LauncherPartLaunch::executeTask()
|
||||
|
||||
auto natPath = minecraftInstance->getNativePath();
|
||||
#ifdef Q_OS_WIN
|
||||
if (!fitsInLocal8bit(natPath)) {
|
||||
args << "-Djava.library.path=" + shortPathName(natPath);
|
||||
} else {
|
||||
args << "-Djava.library.path=" + natPath;
|
||||
}
|
||||
#else
|
||||
args << "-Djava.library.path=" + natPath;
|
||||
natPath = FS::getPathNameInLocal8bit(natPath);
|
||||
#endif
|
||||
args << "-Djava.library.path=" + natPath;
|
||||
|
||||
args << "-cp";
|
||||
#ifdef Q_OS_WIN
|
||||
QStringList processed;
|
||||
for (auto& item : classPath) {
|
||||
if (!fitsInLocal8bit(item)) {
|
||||
processed << shortPathName(item);
|
||||
} else {
|
||||
processed << item;
|
||||
}
|
||||
processed << FS::getPathNameInLocal8bit(item);
|
||||
}
|
||||
args << processed.join(';');
|
||||
#else
|
||||
|
@ -274,7 +274,7 @@ QPixmap Mod::icon(QSize size, Qt::AspectRatioMode mode) const
|
||||
return {};
|
||||
|
||||
if (m_pack_image_cache_key.was_ever_used) {
|
||||
qDebug() << "Mod" << name() << "Had it's icon evicted form the cache. reloading...";
|
||||
qDebug() << "Mod" << name() << "Had it's icon evicted from the cache. reloading...";
|
||||
PixmapCache::markCacheMissByEviciton();
|
||||
}
|
||||
// Image got evicted from the cache or an attempt to load it has not been made. load it and retry.
|
||||
|
@ -284,7 +284,12 @@ void ResourceFolderModel::resolveResource(Resource* res)
|
||||
connect(
|
||||
task.get(), &Task::failed, this, [=] { onParseFailed(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection);
|
||||
connect(
|
||||
task.get(), &Task::finished, this, [=] { m_active_parse_tasks.remove(ticket); }, Qt::ConnectionType::QueuedConnection);
|
||||
task.get(), &Task::finished, this,
|
||||
[=] {
|
||||
m_active_parse_tasks.remove(ticket);
|
||||
emit parseFinished();
|
||||
},
|
||||
Qt::ConnectionType::QueuedConnection);
|
||||
|
||||
m_helper_thread_task.addTask(task);
|
||||
|
||||
@ -617,3 +622,26 @@ QString ResourceFolderModel::instDirPath() const
|
||||
{
|
||||
return QFileInfo(m_instance->instanceRoot()).absoluteFilePath();
|
||||
}
|
||||
|
||||
void ResourceFolderModel::onParseFailed(int ticket, QString resource_id)
|
||||
{
|
||||
auto iter = m_active_parse_tasks.constFind(ticket);
|
||||
if (iter == m_active_parse_tasks.constEnd())
|
||||
return;
|
||||
|
||||
auto removed_index = m_resources_index[resource_id];
|
||||
auto removed_it = m_resources.begin() + removed_index;
|
||||
Q_ASSERT(removed_it != m_resources.end());
|
||||
|
||||
beginRemoveRows(QModelIndex(), removed_index, removed_index);
|
||||
m_resources.erase(removed_it);
|
||||
|
||||
// update index
|
||||
m_resources_index.clear();
|
||||
int idx = 0;
|
||||
for (auto const& mod : qAsConst(m_resources)) {
|
||||
m_resources_index[mod->internal_id()] = idx;
|
||||
idx++;
|
||||
}
|
||||
endRemoveRows();
|
||||
}
|
||||
|
@ -143,6 +143,7 @@ class ResourceFolderModel : public QAbstractListModel {
|
||||
|
||||
signals:
|
||||
void updateFinished();
|
||||
void parseFinished();
|
||||
|
||||
protected:
|
||||
/** This creates a new update task to be executed by update().
|
||||
@ -189,11 +190,7 @@ class ResourceFolderModel : public QAbstractListModel {
|
||||
* if the resource is complex and has more stuff to parse.
|
||||
*/
|
||||
virtual void onParseSucceeded(int ticket, QString resource_id);
|
||||
virtual void onParseFailed(int ticket, QString resource_id)
|
||||
{
|
||||
Q_UNUSED(ticket);
|
||||
Q_UNUSED(resource_id);
|
||||
}
|
||||
virtual void onParseFailed(int ticket, QString resource_id);
|
||||
|
||||
protected:
|
||||
// Represents the relationship between a column's index (represented by the list index), and it's sorting key.
|
||||
@ -306,7 +303,6 @@ void ResourceFolderModel::applyUpdates(QSet<QString>& current_set, QSet<QString>
|
||||
auto removed_it = m_resources.begin() + removed_index;
|
||||
|
||||
Q_ASSERT(removed_it != m_resources.end());
|
||||
Q_ASSERT(removed_set.contains(removed_it->get()->internal_id()));
|
||||
|
||||
if ((*removed_it)->isResolving()) {
|
||||
auto ticket = (*removed_it)->resolutionTicket();
|
||||
|
@ -35,8 +35,3 @@ bool ShaderPack::valid() const
|
||||
{
|
||||
return m_pack_format != ShaderPackFormat::INVALID;
|
||||
}
|
||||
|
||||
bool ShaderPack::applyFilter(QRegularExpression filter) const
|
||||
{
|
||||
return valid() && Resource::applyFilter(filter);
|
||||
}
|
||||
|
@ -54,7 +54,6 @@ class ShaderPack : public Resource {
|
||||
void setPackFormat(ShaderPackFormat new_format);
|
||||
|
||||
bool valid() const override;
|
||||
[[nodiscard]] bool applyFilter(QRegularExpression filter) const override;
|
||||
|
||||
protected:
|
||||
mutable QMutex m_data_lock;
|
||||
|
@ -57,9 +57,11 @@ GetModDependenciesTask::GetModDependenciesTask(QObject* parent,
|
||||
, m_version(mcVersion(instance))
|
||||
, m_loaderType(mcLoaders(instance))
|
||||
{
|
||||
for (auto mod : folder->allMods())
|
||||
for (auto mod : folder->allMods()) {
|
||||
m_mods_file_names << mod->fileinfo().fileName();
|
||||
if (auto meta = mod->metadata(); meta)
|
||||
m_mods.append(meta);
|
||||
}
|
||||
prepare();
|
||||
}
|
||||
|
||||
@ -182,7 +184,9 @@ Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Depen
|
||||
|
||||
ResourceAPI::DependencySearchArgs args = { dep, m_version, m_loaderType };
|
||||
ResourceAPI::DependencySearchCallbacks callbacks;
|
||||
|
||||
callbacks.on_fail = [](QString reason, int) {
|
||||
qCritical() << tr("A network error occurred. Could not load project dependencies:%1").arg(reason);
|
||||
};
|
||||
callbacks.on_succeed = [dep, provider, pDep, level, this](auto& doc, [[maybe_unused]] auto& pack) {
|
||||
try {
|
||||
QJsonArray arr;
|
||||
@ -229,8 +233,13 @@ Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Depen
|
||||
if (dep_.addonId != pDep->version.addonId) {
|
||||
removePack(pDep->version.addonId);
|
||||
addTask(prepareDependencyTask(dep_, provider.name, level));
|
||||
} else
|
||||
} else {
|
||||
addTask(getProjectInfoTask(pDep));
|
||||
}
|
||||
}
|
||||
if (isLocalyInstalled(pDep)) {
|
||||
removePack(pDep->version.addonId);
|
||||
return;
|
||||
}
|
||||
for (auto dep_ : getDependenciesForVersion(pDep->version, provider.name)) {
|
||||
addTask(prepareDependencyTask(dep_, provider.name, level - 1));
|
||||
@ -256,9 +265,9 @@ void GetModDependenciesTask::removePack(const QVariant& addonId)
|
||||
#endif
|
||||
}
|
||||
|
||||
QHash<QString, QStringList> GetModDependenciesTask::getRequiredBy()
|
||||
auto GetModDependenciesTask::getExtraInfo() -> QHash<QString, PackDependencyExtraInfo>
|
||||
{
|
||||
QHash<QString, QStringList> rby;
|
||||
QHash<QString, PackDependencyExtraInfo> rby;
|
||||
auto fullList = m_selected + m_pack_dependencies;
|
||||
for (auto& mod : fullList) {
|
||||
auto addonId = mod->pack->addonId;
|
||||
@ -280,7 +289,61 @@ QHash<QString, QStringList> GetModDependenciesTask::getRequiredBy()
|
||||
req.append(smod->pack->name);
|
||||
}
|
||||
}
|
||||
rby[addonId.toString()] = req;
|
||||
rby[addonId.toString()] = { maybeInstalled(mod), req };
|
||||
}
|
||||
return rby;
|
||||
}
|
||||
}
|
||||
|
||||
// super lax compare (but not fuzzy)
|
||||
// convert to lowercase
|
||||
// convert all speratores to whitespace
|
||||
// simplify sequence of internal whitespace to a single space
|
||||
// efectivly compare two strings ignoring all separators and case
|
||||
auto laxCompare = [](QString fsfilename, QString metadataFilename, bool excludeDigits = false) {
|
||||
// allowed character seperators
|
||||
QList<QChar> allowedSeperators = { '-', '+', '.', '_' };
|
||||
if (excludeDigits)
|
||||
allowedSeperators.append({ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' });
|
||||
|
||||
// copy in lowercase
|
||||
auto fsName = fsfilename.toLower();
|
||||
auto metaName = metadataFilename.toLower();
|
||||
|
||||
// replace all potential allowed seperatores with whitespace
|
||||
for (auto sep : allowedSeperators) {
|
||||
fsName = fsName.replace(sep, ' ');
|
||||
metaName = metaName.replace(sep, ' ');
|
||||
}
|
||||
|
||||
// remove extraneous whitespace
|
||||
fsName = fsName.simplified();
|
||||
metaName = metaName.simplified();
|
||||
|
||||
return fsName.compare(metaName) == 0;
|
||||
};
|
||||
|
||||
bool GetModDependenciesTask::isLocalyInstalled(std::shared_ptr<PackDependency> pDep)
|
||||
{
|
||||
return pDep->version.fileName.isEmpty() ||
|
||||
|
||||
std::find_if(m_selected.begin(), m_selected.end(),
|
||||
[pDep](std::shared_ptr<PackDependency> i) {
|
||||
return !i->version.fileName.isEmpty() && laxCompare(i->version.fileName, pDep->version.fileName);
|
||||
}) != m_selected.end() || // check the selected versions
|
||||
|
||||
std::find_if(m_mods_file_names.begin(), m_mods_file_names.end(),
|
||||
[pDep](QString i) { return !i.isEmpty() && laxCompare(i, pDep->version.fileName); }) !=
|
||||
m_mods_file_names.end() || // check the existing mods
|
||||
|
||||
std::find_if(m_pack_dependencies.begin(), m_pack_dependencies.end(), [pDep](std::shared_ptr<PackDependency> i) {
|
||||
return pDep->pack->addonId != i->pack->addonId && !i->version.fileName.isEmpty() &&
|
||||
laxCompare(pDep->version.fileName, i->version.fileName);
|
||||
}) != m_pack_dependencies.end(); // check loaded dependencies
|
||||
}
|
||||
|
||||
bool GetModDependenciesTask::maybeInstalled(std::shared_ptr<PackDependency> pDep)
|
||||
{
|
||||
return std::find_if(m_mods_file_names.begin(), m_mods_file_names.end(), [pDep](QString i) {
|
||||
return !i.isEmpty() && laxCompare(i, pDep->version.fileName, true);
|
||||
}) != m_mods_file_names.end(); // check the existing mods
|
||||
}
|
||||
|
@ -50,6 +50,11 @@ class GetModDependenciesTask : public SequentialTask {
|
||||
}
|
||||
};
|
||||
|
||||
struct PackDependencyExtraInfo {
|
||||
bool maybe_installed;
|
||||
QStringList required_by;
|
||||
};
|
||||
|
||||
struct Provider {
|
||||
ModPlatform::ResourceProvider name;
|
||||
std::shared_ptr<ResourceDownload::ModModel> mod;
|
||||
@ -62,7 +67,7 @@ class GetModDependenciesTask : public SequentialTask {
|
||||
QList<std::shared_ptr<PackDependency>> selected);
|
||||
|
||||
auto getDependecies() const -> QList<std::shared_ptr<PackDependency>> { return m_pack_dependencies; }
|
||||
QHash<QString, QStringList> getRequiredBy();
|
||||
QHash<QString, PackDependencyExtraInfo> getExtraInfo();
|
||||
|
||||
protected slots:
|
||||
Task::Ptr prepareDependencyTask(const ModPlatform::Dependency&, ModPlatform::ResourceProvider, int);
|
||||
@ -73,10 +78,14 @@ class GetModDependenciesTask : public SequentialTask {
|
||||
ModPlatform::Dependency getOverride(const ModPlatform::Dependency&, ModPlatform::ResourceProvider providerName);
|
||||
void removePack(const QVariant& addonId);
|
||||
|
||||
bool isLocalyInstalled(std::shared_ptr<PackDependency> pDep);
|
||||
bool maybeInstalled(std::shared_ptr<PackDependency> pDep);
|
||||
|
||||
private:
|
||||
QList<std::shared_ptr<PackDependency>> m_pack_dependencies;
|
||||
QList<std::shared_ptr<Metadata::ModStruct>> m_mods;
|
||||
QList<std::shared_ptr<PackDependency>> m_selected;
|
||||
QStringList m_mods_file_names;
|
||||
Provider m_flame_provider;
|
||||
Provider m_modrinth_provider;
|
||||
|
||||
|
@ -469,7 +469,7 @@ bool processZIP(Mod& mod, [[maybe_unused]] ProcessingLevel level)
|
||||
|
||||
QuaZipFile file(&zip);
|
||||
|
||||
if (zip.setCurrentFile("META-INF/mods.toml")) {
|
||||
if (zip.setCurrentFile("META-INF/mods.toml") || zip.setCurrentFile("META-INF/neoforge.mods.toml")) {
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
zip.close();
|
||||
return false;
|
||||
@ -746,7 +746,7 @@ void LocalModParseTask::executeTask()
|
||||
m_result->details = mod.details();
|
||||
|
||||
if (m_aborted)
|
||||
emit finished();
|
||||
emitAborted();
|
||||
else
|
||||
emitSucceeded();
|
||||
}
|
||||
|
@ -286,8 +286,10 @@ bool LocalResourcePackParseTask::abort()
|
||||
|
||||
void LocalResourcePackParseTask::executeTask()
|
||||
{
|
||||
if (!ResourcePackUtils::process(m_resource_pack))
|
||||
if (!ResourcePackUtils::process(m_resource_pack)) {
|
||||
emitFailed("this is not a resource pack");
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_aborted)
|
||||
emitAborted();
|
||||
|
@ -103,8 +103,10 @@ bool LocalShaderPackParseTask::abort()
|
||||
|
||||
void LocalShaderPackParseTask::executeTask()
|
||||
{
|
||||
if (!ShaderPackUtils::process(m_shader_pack))
|
||||
if (!ShaderPackUtils::process(m_shader_pack)) {
|
||||
emitFailed("this is not a shader pack");
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_aborted)
|
||||
emitAborted();
|
||||
|
@ -241,8 +241,10 @@ bool LocalTexturePackParseTask::abort()
|
||||
|
||||
void LocalTexturePackParseTask::executeTask()
|
||||
{
|
||||
if (!TexturePackUtils::process(m_texture_pack))
|
||||
if (!TexturePackUtils::process(m_texture_pack)) {
|
||||
emitFailed("this is not a texture pack");
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_aborted)
|
||||
emitAborted();
|
||||
|
@ -28,6 +28,7 @@ class CheckUpdateTask : public Task {
|
||||
QString changelog;
|
||||
ModPlatform::ResourceProvider provider;
|
||||
shared_qobject_ptr<ResourceDownloadTask> download;
|
||||
bool enabled = true;
|
||||
|
||||
public:
|
||||
UpdatableMod(QString name,
|
||||
@ -37,7 +38,8 @@ class CheckUpdateTask : public Task {
|
||||
std::optional<ModPlatform::IndexedVersionType> new_v_type,
|
||||
QString changelog,
|
||||
ModPlatform::ResourceProvider p,
|
||||
shared_qobject_ptr<ResourceDownloadTask> t)
|
||||
shared_qobject_ptr<ResourceDownloadTask> t,
|
||||
bool enabled = true)
|
||||
: name(name)
|
||||
, old_hash(old_h)
|
||||
, old_version(old_v)
|
||||
@ -46,6 +48,7 @@ class CheckUpdateTask : public Task {
|
||||
, changelog(changelog)
|
||||
, provider(p)
|
||||
, download(t)
|
||||
, enabled(enabled)
|
||||
{}
|
||||
};
|
||||
|
||||
|
@ -149,6 +149,7 @@ void EnsureMetadataTask::executeTask()
|
||||
if (m_current_task)
|
||||
m_current_task.reset();
|
||||
});
|
||||
connect(project_task.get(), &Task::failed, this, &EnsureMetadataTask::emitFailed);
|
||||
|
||||
m_current_task = project_task;
|
||||
project_task->start();
|
||||
|
@ -122,6 +122,8 @@ struct ExtraPackData {
|
||||
QString wikiUrl;
|
||||
QString discordUrl;
|
||||
|
||||
QString status;
|
||||
|
||||
QString body;
|
||||
};
|
||||
|
||||
|
@ -96,6 +96,7 @@ class ResourceAPI {
|
||||
};
|
||||
struct VersionSearchCallbacks {
|
||||
std::function<void(QJsonDocument&, ModPlatform::IndexedPack)> on_succeed;
|
||||
std::function<void(QString const& reason, int network_error_code)> on_fail;
|
||||
};
|
||||
|
||||
struct ProjectInfoArgs {
|
||||
@ -118,6 +119,7 @@ class ResourceAPI {
|
||||
|
||||
struct DependencySearchCallbacks {
|
||||
std::function<void(QJsonDocument&, const ModPlatform::Dependency&)> on_succeed;
|
||||
std::function<void(QString const& reason, int network_error_code)> on_fail;
|
||||
};
|
||||
|
||||
public:
|
||||
|
@ -1031,6 +1031,12 @@ void PackInstallTask::install()
|
||||
return;
|
||||
|
||||
components->setComponentVersion("net.minecraftforge", version);
|
||||
} else if (m_version.loader.type == QString("neoforge")) {
|
||||
auto version = getVersionForLoader("net.neoforged");
|
||||
if (version == Q_NULLPTR)
|
||||
return;
|
||||
|
||||
components->setComponentVersion("net.neoforged", version);
|
||||
} else if (m_version.loader.type == QString("fabric")) {
|
||||
auto version = getVersionForLoader("net.fabricmc.fabric-loader");
|
||||
if (version == Q_NULLPTR)
|
||||
|
@ -119,7 +119,6 @@ void Flame::FileResolvingTask::netJobFinished()
|
||||
connect(m_checkJob.get(), &NetJob::failed, this, [this, step_progress](QString reason) {
|
||||
step_progress->state = TaskStepState::Failed;
|
||||
stepProgress(*step_progress);
|
||||
emitFailed(reason);
|
||||
});
|
||||
connect(m_checkJob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propagateStepProgress);
|
||||
connect(m_checkJob.get(), &NetJob::progress, this, [this, step_progress](qint64 current, qint64 total) {
|
||||
|
@ -24,7 +24,7 @@ bool FlameCheckUpdate::abort()
|
||||
return true;
|
||||
}
|
||||
|
||||
ModPlatform::IndexedPack getProjectInfo(ModPlatform::IndexedVersion& ver_info)
|
||||
ModPlatform::IndexedPack FlameCheckUpdate::getProjectInfo(ModPlatform::IndexedVersion& ver_info)
|
||||
{
|
||||
ModPlatform::IndexedPack pack;
|
||||
|
||||
@ -57,6 +57,7 @@ ModPlatform::IndexedPack getProjectInfo(ModPlatform::IndexedVersion& ver_info)
|
||||
}
|
||||
});
|
||||
|
||||
connect(get_project_job, &NetJob::failed, this, &FlameCheckUpdate::emitFailed);
|
||||
QObject::connect(get_project_job, &NetJob::finished, [&loop, get_project_job] {
|
||||
get_project_job->deleteLater();
|
||||
loop.quit();
|
||||
@ -68,7 +69,7 @@ ModPlatform::IndexedPack getProjectInfo(ModPlatform::IndexedVersion& ver_info)
|
||||
return pack;
|
||||
}
|
||||
|
||||
ModPlatform::IndexedVersion getFileInfo(int addonId, int fileId)
|
||||
ModPlatform::IndexedVersion FlameCheckUpdate::getFileInfo(int addonId, int fileId)
|
||||
{
|
||||
ModPlatform::IndexedVersion ver;
|
||||
|
||||
@ -100,7 +101,7 @@ ModPlatform::IndexedVersion getFileInfo(int addonId, int fileId)
|
||||
qDebug() << doc;
|
||||
}
|
||||
});
|
||||
|
||||
connect(get_file_info_job, &NetJob::failed, this, &FlameCheckUpdate::emitFailed);
|
||||
QObject::connect(get_file_info_job, &NetJob::finished, [&loop, get_file_info_job] {
|
||||
get_file_info_job->deleteLater();
|
||||
loop.quit();
|
||||
|
@ -22,6 +22,9 @@ class FlameCheckUpdate : public CheckUpdateTask {
|
||||
void executeTask() override;
|
||||
|
||||
private:
|
||||
ModPlatform::IndexedPack getProjectInfo(ModPlatform::IndexedVersion& ver_info);
|
||||
ModPlatform::IndexedVersion getFileInfo(int addonId, int fileId);
|
||||
|
||||
NetJob* m_net_job = nullptr;
|
||||
|
||||
bool m_was_aborted = false;
|
||||
|
@ -227,6 +227,7 @@ bool FlameCreationTask::updateInstance()
|
||||
m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(relative_path));
|
||||
}
|
||||
});
|
||||
connect(job.get(), &Task::failed, this, [](QString reason) { qCritical() << "Failed to get files: " << reason; });
|
||||
connect(job.get(), &Task::finished, &loop, &QEventLoop::quit);
|
||||
|
||||
m_process_update_file_info_job = job;
|
||||
@ -353,6 +354,8 @@ bool FlameCreationTask::createInstance()
|
||||
auto id = loader.id;
|
||||
if (id.startsWith("neoforge-")) {
|
||||
id.remove("neoforge-");
|
||||
if (id.startsWith("1.20.1-"))
|
||||
id.remove("1.20.1-"); // this is a mess for curseforge
|
||||
loaderType = "neoforge";
|
||||
loaderUid = "net.neoforged";
|
||||
} else if (id.startsWith("forge-")) {
|
||||
@ -427,6 +430,9 @@ bool FlameCreationTask::createInstance()
|
||||
// Don't add managed info to packs without an ID (most likely imported from ZIP)
|
||||
if (!m_managed_id.isEmpty())
|
||||
instance.setManagedPack("flame", m_managed_id, m_pack.name, m_managed_version_id, m_pack.version);
|
||||
else
|
||||
instance.setManagedPack("flame", "", name(), "", "");
|
||||
|
||||
instance.setName(name());
|
||||
|
||||
m_mod_id_resolver.reset(new Flame::FileResolvingTask(APPLICATION->network(), m_pack));
|
||||
@ -531,7 +537,12 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
|
||||
selectedOptionalMods = optionalModDialog.getResult();
|
||||
}
|
||||
for (const auto& result : results) {
|
||||
auto relpath = FS::PathCombine(result.targetFolder, result.fileName);
|
||||
auto fileName = result.fileName;
|
||||
#ifdef Q_OS_WIN
|
||||
fileName = FS::RemoveInvalidPathChars(fileName);
|
||||
#endif
|
||||
auto relpath = FS::PathCombine(result.targetFolder, fileName);
|
||||
|
||||
if (!result.required && !selectedOptionalMods.contains(relpath)) {
|
||||
relpath += ".disabled";
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
#include "FlameModIndex.h"
|
||||
|
||||
#include "FileSystem.h"
|
||||
#include "Json.h"
|
||||
#include "minecraft/MinecraftInstance.h"
|
||||
#include "minecraft/PackProfile.h"
|
||||
@ -138,6 +139,9 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) ->
|
||||
file.version = Json::requireString(obj, "displayName");
|
||||
file.downloadUrl = Json::ensureString(obj, "downloadUrl");
|
||||
file.fileName = Json::requireString(obj, "fileName");
|
||||
#ifdef Q_OS_WIN
|
||||
file.fileName = FS::RemoveInvalidPathChars(file.fileName);
|
||||
#endif
|
||||
|
||||
ModPlatform::IndexedVersionType::VersionType ver_type;
|
||||
switch (Json::requireInteger(obj, "releaseType")) {
|
||||
|
@ -201,7 +201,7 @@ void FlamePackExportTask::makeApiRequest()
|
||||
<< " reason: " << parseError.errorString();
|
||||
qWarning() << *response;
|
||||
|
||||
failed(parseError.errorString());
|
||||
emitFailed(parseError.errorString());
|
||||
return;
|
||||
}
|
||||
|
||||
@ -213,6 +213,7 @@ void FlamePackExportTask::makeApiRequest()
|
||||
if (dataArr.isEmpty()) {
|
||||
qWarning() << "No matches found for fingerprint search!";
|
||||
|
||||
getProjectsInfo();
|
||||
return;
|
||||
}
|
||||
for (auto match : dataArr) {
|
||||
@ -243,9 +244,9 @@ void FlamePackExportTask::makeApiRequest()
|
||||
qDebug() << doc;
|
||||
}
|
||||
pendingHashes.clear();
|
||||
getProjectsInfo();
|
||||
});
|
||||
connect(task.get(), &Task::finished, this, &FlamePackExportTask::getProjectsInfo);
|
||||
connect(task.get(), &NetJob::failed, this, &FlamePackExportTask::emitFailed);
|
||||
connect(task.get(), &NetJob::failed, this, &FlamePackExportTask::getProjectsInfo);
|
||||
task->start();
|
||||
}
|
||||
|
||||
@ -279,7 +280,7 @@ void FlamePackExportTask::getProjectsInfo()
|
||||
qWarning() << "Error while parsing JSON response from CurseForge projects task at " << parseError.offset
|
||||
<< " reason: " << parseError.errorString();
|
||||
qWarning() << *response;
|
||||
failed(parseError.errorString());
|
||||
emitFailed(parseError.errorString());
|
||||
return;
|
||||
}
|
||||
|
||||
@ -323,6 +324,7 @@ void FlamePackExportTask::getProjectsInfo()
|
||||
}
|
||||
buildZip();
|
||||
});
|
||||
connect(projTask.get(), &Task::failed, this, &FlamePackExportTask::emitFailed);
|
||||
task.reset(projTask);
|
||||
task->start();
|
||||
}
|
||||
@ -332,7 +334,7 @@ void FlamePackExportTask::buildZip()
|
||||
setStatus(tr("Adding files..."));
|
||||
setProgress(4, 5);
|
||||
|
||||
auto zipTask = makeShared<MMCZip::ExportToZipTask>(output, gameRoot, files, "overrides/", true);
|
||||
auto zipTask = makeShared<MMCZip::ExportToZipTask>(output, gameRoot, files, "overrides/", true, false);
|
||||
zipTask->addExtraFile("manifest.json", generateIndex());
|
||||
zipTask->addExtraFile("modlist.html", generateHTML());
|
||||
|
||||
@ -392,13 +394,17 @@ QByteArray FlamePackExportTask::generateIndex()
|
||||
version["version"] = minecraft->m_version;
|
||||
QString id;
|
||||
if (quilt != nullptr)
|
||||
id = "quilt-" + quilt->getVersion();
|
||||
id = "quilt-" + quilt->m_version;
|
||||
else if (fabric != nullptr)
|
||||
id = "fabric-" + fabric->getVersion();
|
||||
id = "fabric-" + fabric->m_version;
|
||||
else if (forge != nullptr)
|
||||
id = "forge-" + forge->getVersion();
|
||||
else if (neoforge != nullptr)
|
||||
id = "neoforge-" + neoforge->getVersion();
|
||||
id = "forge-" + forge->m_version;
|
||||
else if (neoforge != nullptr) {
|
||||
id = "neoforge-";
|
||||
if (minecraft->m_version == "1.20.1")
|
||||
id += "1.20.1-";
|
||||
id += neoforge->m_version;
|
||||
}
|
||||
version["modLoaders"] = QJsonArray();
|
||||
if (!id.isEmpty()) {
|
||||
QJsonObject loader;
|
||||
|
@ -43,10 +43,10 @@ Task::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallbacks&
|
||||
callbacks.on_succeed(doc);
|
||||
});
|
||||
|
||||
QObject::connect(netJob.get(), &NetJob::failed, [&netJob, callbacks](QString reason) {
|
||||
QObject::connect(netJob.get(), &NetJob::failed, [netJob, callbacks](const QString& reason) {
|
||||
int network_error_code = -1;
|
||||
if (auto* failed_action = netJob->getFailedActions().at(0); failed_action && failed_action->m_reply)
|
||||
network_error_code = failed_action->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
if (auto* failed_action = netJob->getFailedActions().at(0); failed_action)
|
||||
network_error_code = failed_action->replyStatusCode();
|
||||
|
||||
callbacks.on_fail(reason, network_error_code);
|
||||
});
|
||||
@ -102,6 +102,13 @@ Task::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, Versi
|
||||
|
||||
callbacks.on_succeed(doc, args.pack);
|
||||
});
|
||||
QObject::connect(netJob.get(), &NetJob::failed, [netJob, callbacks](const QString& reason) {
|
||||
int network_error_code = -1;
|
||||
if (auto* failed_action = netJob->getFailedActions().at(0); failed_action)
|
||||
network_error_code = failed_action->replyStatusCode();
|
||||
|
||||
callbacks.on_fail(reason, network_error_code);
|
||||
});
|
||||
|
||||
return netJob;
|
||||
}
|
||||
@ -146,6 +153,12 @@ Task::Ptr NetworkResourceAPI::getDependencyVersion(DependencySearchArgs&& args,
|
||||
|
||||
callbacks.on_succeed(doc, args.dependency);
|
||||
});
|
||||
QObject::connect(netJob.get(), &NetJob::failed, [netJob, callbacks](const QString& reason) {
|
||||
int network_error_code = -1;
|
||||
if (auto* failed_action = netJob->getFailedActions().at(0); failed_action)
|
||||
network_error_code = failed_action->replyStatusCode();
|
||||
|
||||
callbacks.on_fail(reason, network_error_code);
|
||||
});
|
||||
return netJob;
|
||||
}
|
||||
|
@ -43,6 +43,7 @@ Modpack parseDirectory(QString path)
|
||||
modpack.version = Json::requireString(root, "version", "version");
|
||||
modpack.mcVersion = Json::requireString(root, "mcVersion", "mcVersion");
|
||||
modpack.jvmArgs = Json::ensureVariant(root, "jvmArgs", {}, "jvmArgs");
|
||||
modpack.totalPlayTime = Json::requireInteger(root, "totalPlayTime", "totalPlayTime");
|
||||
} catch (const Exception& e) {
|
||||
qDebug() << "Couldn't load ftb instance json: " << e.cause();
|
||||
return {};
|
||||
|
@ -36,6 +36,7 @@ struct Modpack {
|
||||
QString name;
|
||||
QString version;
|
||||
QString mcVersion;
|
||||
int totalPlayTime;
|
||||
// not needed for instance creation
|
||||
QVariant jvmArgs;
|
||||
|
||||
|
@ -37,7 +37,7 @@ void PackInstallTask::executeTask()
|
||||
progress(1, 2);
|
||||
|
||||
m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this] {
|
||||
FS::copy folderCopy(m_pack.path, FS::PathCombine(m_stagingPath, ".minecraft"));
|
||||
FS::copy folderCopy(m_pack.path, FS::PathCombine(m_stagingPath, "minecraft"));
|
||||
folderCopy.followSymlinks(true);
|
||||
return folderCopy();
|
||||
});
|
||||
@ -55,6 +55,7 @@ void PackInstallTask::copySettings()
|
||||
instanceSettings->suspendSave();
|
||||
MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath);
|
||||
instance.settings()->set("InstanceType", "OneSix");
|
||||
instance.settings()->set("totalTimePlayed", m_pack.totalPlayTime / 1000);
|
||||
|
||||
if (m_pack.jvmArgs.isValid() && !m_pack.jvmArgs.toString().isEmpty()) {
|
||||
instance.settings()->set("OverrideJavaArgs", true);
|
||||
|
@ -137,7 +137,7 @@ void PackInstallTask::install()
|
||||
QDir unzipMcDir(m_stagingPath + "/unzip/minecraft");
|
||||
if (unzipMcDir.exists()) {
|
||||
// ok, found minecraft dir, move contents to instance dir
|
||||
if (!QDir().rename(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/.minecraft")) {
|
||||
if (!QDir().rename(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/minecraft")) {
|
||||
emitFailed(tr("Failed to move unzipped Minecraft!"));
|
||||
return;
|
||||
}
|
||||
@ -155,7 +155,7 @@ void PackInstallTask::install()
|
||||
bool fallback = true;
|
||||
|
||||
// handle different versions
|
||||
QFile packJson(m_stagingPath + "/.minecraft/pack.json");
|
||||
QFile packJson(m_stagingPath + "/minecraft/pack.json");
|
||||
QDir jarmodDir = QDir(m_stagingPath + "/unzip/instMods");
|
||||
if (packJson.exists()) {
|
||||
packJson.open(QIODevice::ReadOnly | QIODevice::Text);
|
||||
|
@ -72,9 +72,7 @@ void ModrinthCheckUpdate::executeTask()
|
||||
auto response = std::make_shared<QByteArray>();
|
||||
auto job = api.latestVersions(hashes, best_hash_type, m_game_versions, m_loaders, response);
|
||||
|
||||
QEventLoop lock;
|
||||
|
||||
connect(job.get(), &Task::succeeded, this, [this, response, &mappings, best_hash_type, job] {
|
||||
connect(job.get(), &Task::succeeded, this, [this, response, mappings, best_hash_type, job] {
|
||||
QJsonParseError parse_error{};
|
||||
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
|
||||
if (parse_error.error != QJsonParseError::NoError) {
|
||||
@ -82,7 +80,7 @@ void ModrinthCheckUpdate::executeTask()
|
||||
<< " reason: " << parse_error.errorString();
|
||||
qWarning() << *response;
|
||||
|
||||
failed(parse_error.errorString());
|
||||
emitFailed(parse_error.errorString());
|
||||
return;
|
||||
}
|
||||
|
||||
@ -167,19 +165,17 @@ void ModrinthCheckUpdate::executeTask()
|
||||
m_deps.append(std::make_shared<GetModDependenciesTask::PackDependency>(pack, project_ver));
|
||||
}
|
||||
} catch (Json::JsonException& e) {
|
||||
failed(e.cause() + " : " + e.what());
|
||||
emitFailed(e.cause() + " : " + e.what());
|
||||
return;
|
||||
}
|
||||
emitSucceeded();
|
||||
});
|
||||
|
||||
connect(job.get(), &Task::finished, &lock, &QEventLoop::quit);
|
||||
connect(job.get(), &Task::failed, this, &ModrinthCheckUpdate::emitFailed);
|
||||
|
||||
setStatus(tr("Waiting for the API response from Modrinth..."));
|
||||
setProgress(1, 3);
|
||||
|
||||
m_net_job = qSharedPointerObjectCast<NetJob, Task>(job);
|
||||
job->start();
|
||||
|
||||
lock.exec();
|
||||
|
||||
emitSucceeded();
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user