Merge branch 'develop' of https://github.com/PrismLauncher/PrismLauncher into skin_selector

Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
This commit is contained in:
Trial97 2024-05-18 13:27:31 +03:00
commit 633489884d
No known key found for this signature in database
GPG Key ID: 55EF5DA53DB36318
111 changed files with 1378 additions and 2860 deletions

View File

@ -57,14 +57,14 @@ jobs:
qt_host: linux
qt_arch: ""
qt_version: "5.12.8"
qt_modules: ""
qt_modules: "qtnetworkauth"
- os: ubuntu-20.04
qt_ver: 6
qt_host: linux
qt_arch: ""
qt_version: "6.2.4"
qt_modules: "qt5compat qtimageformats"
qt_modules: "qt5compat qtimageformats qtnetworkauth"
- os: windows-2022
name: "Windows-MinGW-w64"
@ -78,9 +78,9 @@ jobs:
vcvars_arch: "amd64"
qt_ver: 6
qt_host: windows
qt_arch: ''
qt_version: '6.7.0'
qt_modules: 'qt5compat qtimageformats'
qt_arch: ""
qt_version: "6.7.0"
qt_modules: "qt5compat qtimageformats qtnetworkauth"
- os: windows-2022
name: "Windows-MSVC-arm64"
@ -89,18 +89,18 @@ jobs:
vcvars_arch: "amd64_arm64"
qt_ver: 6
qt_host: windows
qt_arch: 'win64_msvc2019_arm64'
qt_version: '6.7.0'
qt_modules: 'qt5compat qtimageformats'
qt_arch: "win64_msvc2019_arm64"
qt_version: "6.7.0"
qt_modules: "qt5compat qtimageformats qtnetworkauth"
- os: macos-12
name: macOS
macosx_deployment_target: 11.0
qt_ver: 6
qt_host: mac
qt_arch: ''
qt_version: '6.7.0'
qt_modules: 'qt5compat qtimageformats'
qt_arch: ""
qt_version: "6.7.0"
qt_modules: "qt5compat qtimageformats qtnetworkauth"
- os: macos-12
name: macOS-Legacy
@ -108,7 +108,7 @@ jobs:
qt_ver: 5
qt_host: mac
qt_version: "5.15.2"
qt_modules: ""
qt_modules: "qtnetworkauth"
runs-on: ${{ matrix.os }}
@ -150,6 +150,7 @@ jobs:
quazip-qt6:p
ccache:p
qt6-5compat:p
qt6-networkauth:p
cmark:p
- name: Force newer ccache

View File

@ -23,7 +23,7 @@ jobs:
run:
sudo apt-get -y update
sudo apt-get -y install ninja-build extra-cmake-modules scdoc qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5
sudo apt-get -y install ninja-build extra-cmake-modules scdoc qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 libqt5networkauth5 libqt5networkauth5-dev
- name: Configure and Build
run: |

View File

@ -282,7 +282,7 @@ endif()
include(QtVersionlessBackport)
if(Launcher_QT_VERSION_MAJOR EQUAL 5)
set(QT_VERSION_MAJOR 5)
find_package(Qt5 REQUIRED COMPONENTS Core Widgets Concurrent Network Test Xml)
find_package(Qt5 REQUIRED COMPONENTS Core Widgets Concurrent Network Test Xml NetworkAuth)
if(NOT Launcher_FORCE_BUNDLED_LIBS)
find_package(QuaZip-Qt5 1.3 QUIET)
@ -296,7 +296,7 @@ if(Launcher_QT_VERSION_MAJOR EQUAL 5)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DUNICODE -D_UNICODE")
elseif(Launcher_QT_VERSION_MAJOR EQUAL 6)
set(QT_VERSION_MAJOR 6)
find_package(Qt6 REQUIRED COMPONENTS Core CoreTools Widgets Concurrent Network Test Xml Core5Compat)
find_package(Qt6 REQUIRED COMPONENTS Core CoreTools Widgets Concurrent Network Test Xml Core5Compat NetworkAuth)
list(APPEND Launcher_QT_LIBS Qt6::Core5Compat)
if(NOT Launcher_FORCE_BUNDLED_LIBS)
@ -523,7 +523,6 @@ if(NOT cmark_FOUND)
else()
message(STATUS "Using system cmark")
endif()
add_subdirectory(libraries/katabasis) # An OAuth2 library that tried to do too much
add_subdirectory(libraries/gamemode)
add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API
if (NOT ghc_filesystem_FOUND)

View File

@ -333,32 +333,6 @@
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
## O2 (Katabasis fork)
Copyright (c) 2012, Akos Polster
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
## Gamemode
Copyright (c) 2017-2022, Feral Interactive

View File

@ -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"
@ -880,6 +881,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);
}

View File

@ -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
@ -457,6 +447,8 @@ set(TOOLS_SOURCES
tools/JVisualVM.h
tools/MCEditTool.cpp
tools/MCEditTool.h
tools/GenericProfiler.cpp
tools/GenericProfiler.h
)
set(META_SOURCES
@ -628,7 +620,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
@ -1245,7 +1236,6 @@ target_link_libraries(Launcher_logic
tomlplusplus::tomlplusplus
qdcss
BuildConfig
Katabasis
Qt${QT_VERSION_MAJOR}::Widgets
ghcFilesystem::ghc_filesystem
)
@ -1263,6 +1253,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
@ -1333,7 +1324,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)

View File

@ -3,8 +3,6 @@
#include <QDebug>
#include <QFile>
InstanceCreationTask::InstanceCreationTask() = default;
void InstanceCreationTask::executeTask()
{
setAbortable(true);

View File

@ -6,7 +6,7 @@
class InstanceCreationTask : public InstanceTask {
Q_OBJECT
public:
InstanceCreationTask();
InstanceCreationTask() = default;
virtual ~InstanceCreationTask() = default;
protected:

View File

@ -57,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) {}

View File

@ -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();

View File

@ -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)) {

View File

@ -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;

View File

@ -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();

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;

View File

@ -35,7 +35,7 @@
#include "AccountList.h"
#include "AccountData.h"
#include "AccountTask.h"
#include "tasks/Task.h"
#include <QDir>
#include <QFile>
@ -639,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;

View File

@ -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.

View File

@ -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;
}
}
}

View File

@ -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;
};

View 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;
}
}
}

View 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;
};

View File

@ -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);
}

View File

@ -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_;
};

View File

@ -1,5 +0,0 @@
#include "AuthStep.h"
AuthStep::AuthStep(AccountData* data) : QObject(nullptr), m_data(data) {}
AuthStep::~AuthStep() noexcept = default;

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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);

View File

@ -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();
}
}

View File

@ -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;
};

View File

@ -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));
}

View File

@ -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);
};

View File

@ -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));
}

View File

@ -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);
};

View File

@ -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"));
}

View File

@ -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;
};

View File

@ -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"));
}

View File

@ -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;
};

View File

@ -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;
}

View File

@ -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;
};

View 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"));
}

View 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;
};

View File

@ -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();
}
}

View File

@ -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;
};

View File

@ -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;

View File

@ -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;
};

View File

@ -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."));
}

View File

@ -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;
};

View File

@ -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,

View File

@ -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;
};

View File

@ -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"));
}

View File

@ -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;
};

View File

@ -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;

View File

@ -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;
};

View File

@ -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();
}

View File

@ -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.

View File

@ -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);
}

View File

@ -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;

View File

@ -746,7 +746,7 @@ void LocalModParseTask::executeTask()
m_result->details = mod.details();
if (m_aborted)
emit finished();
emitAborted();
else
emitSucceeded();
}

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -45,8 +45,8 @@ Task::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallbacks&
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);
});
@ -104,8 +104,8 @@ Task::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, Versi
});
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);
});
@ -155,8 +155,8 @@ Task::Ptr NetworkResourceAPI::getDependencyVersion(DependencySearchArgs&& args,
});
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);
});

View File

@ -261,7 +261,7 @@ bool ModrinthCreationTask::createInstance()
// FIXME: This really needs to be put into a ConcurrentTask of
// MultipleOptionsTask's , once those exist :)
auto param = dl.toWeakRef();
connect(dl.get(), &NetAction::failed, [this, &file, file_path, param] {
connect(dl.get(), &Task::failed, [this, &file, file_path, param] {
auto ndl = Net::ApiDownload::makeFile(file.downloads.dequeue(), file_path);
ndl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash));
m_files_job->addNetAction(ndl);

View File

@ -21,7 +21,6 @@
#include "ByteArraySink.h"
#include "ChecksumValidator.h"
#include "MetaCacheSink.h"
#include "net/NetAction.h"
namespace Net {

View File

@ -19,9 +19,6 @@
#include "net/ApiUpload.h"
#include "ByteArraySink.h"
#include "ChecksumValidator.h"
#include "MetaCacheSink.h"
#include "net/NetAction.h"
namespace Net {

View File

@ -74,10 +74,6 @@ class ByteArraySink : public Sink {
auto abort() -> Task::State override
{
if (m_output)
m_output->clear();
else
qWarning() << "ByteArraySink did not clear the buffer because it's not addressable";
failAllValidators();
return Task::State::Failed;
}

View File

@ -47,8 +47,6 @@
#include "ChecksumValidator.h"
#include "MetaCacheSink.h"
#include "net/NetAction.h"
namespace Net {
#if defined(LAUNCHER_APPLICATION)

View File

@ -1,101 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.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 <QNetworkReply>
#include <QUrl>
#include "QObjectPtr.h"
#include "tasks/Task.h"
#include "HeaderProxy.h"
class NetAction : public Task {
Q_OBJECT
protected:
explicit NetAction() : Task() {}
public:
using Ptr = shared_qobject_ptr<NetAction>;
virtual ~NetAction() = default;
QUrl url() { return m_url; }
void setUrl(QUrl url) { m_url = url; }
void setNetwork(shared_qobject_ptr<QNetworkAccessManager> network) { m_network = network; }
void addHeaderProxy(Net::HeaderProxy* proxy) { m_headerProxies.push_back(std::shared_ptr<Net::HeaderProxy>(proxy)); }
virtual void init() = 0;
protected slots:
virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) = 0;
virtual void downloadError(QNetworkReply::NetworkError error) = 0;
virtual void downloadFinished() = 0;
virtual void downloadReadyRead() = 0;
virtual void sslErrors(const QList<QSslError>& errors)
{
int i = 1;
for (auto error : errors) {
qCritical() << "Network SSL Error #" << i << " : " << error.errorString();
auto cert = error.certificate();
qCritical() << "Certificate in question:\n" << cert.toText();
i++;
}
}
public slots:
void startAction(shared_qobject_ptr<QNetworkAccessManager> network)
{
m_network = network;
executeTask();
}
protected:
void executeTask() override {}
public:
shared_qobject_ptr<QNetworkAccessManager> m_network;
/// the network reply
unique_qobject_ptr<QNetworkReply> m_reply;
/// source URL
QUrl m_url;
std::vector<std::shared_ptr<Net::HeaderProxy>> m_headerProxies;
};

View File

@ -36,6 +36,7 @@
*/
#include "NetJob.h"
#include "net/NetRequest.h"
#include "tasks/ConcurrentTask.h"
#if defined(LAUNCHER_APPLICATION)
#include "Application.h"
@ -52,7 +53,7 @@ NetJob::NetJob(QString job_name, shared_qobject_ptr<QNetworkAccessManager> netwo
setMaxConcurrent(max_concurrent);
}
auto NetJob::addNetAction(NetAction::Ptr action) -> bool
auto NetJob::addNetAction(Net::NetRequest::Ptr action) -> bool
{
action->setNetwork(m_network);
@ -115,11 +116,11 @@ auto NetJob::abort() -> bool
return fullyAborted;
}
auto NetJob::getFailedActions() -> QList<NetAction*>
auto NetJob::getFailedActions() -> QList<Net::NetRequest*>
{
QList<NetAction*> failed;
QList<Net::NetRequest*> failed;
for (auto index : m_failed) {
failed.push_back(dynamic_cast<NetAction*>(index.get()));
failed.push_back(dynamic_cast<Net::NetRequest*>(index.get()));
}
return failed;
}
@ -128,7 +129,7 @@ auto NetJob::getFailedFiles() -> QList<QString>
{
QList<QString> failed;
for (auto index : m_failed) {
failed.append(static_cast<NetAction*>(index.get())->url().toString());
failed.append(static_cast<Net::NetRequest*>(index.get())->url().toString());
}
return failed;
}

View File

@ -39,7 +39,7 @@
#include <QtNetwork>
#include <QObject>
#include "NetAction.h"
#include "net/NetRequest.h"
#include "tasks/ConcurrentTask.h"
// Those are included so that they are also included by anyone using NetJob
@ -58,9 +58,9 @@ class NetJob : public ConcurrentTask {
auto size() const -> int;
auto canAbort() const -> bool override;
auto addNetAction(NetAction::Ptr action) -> bool;
auto addNetAction(Net::NetRequest::Ptr action) -> bool;
auto getFailedActions() -> QList<NetAction*>;
auto getFailedActions() -> QList<Net::NetRequest*>;
auto getFailedFiles() -> QList<QString>;
public slots:

View File

@ -38,10 +38,11 @@
*/
#include "NetRequest.h"
#include <QUrl>
#include <QDateTime>
#include <QFileInfo>
#include <QNetworkReply>
#include <QUrl>
#include <memory>
#if defined(LAUNCHER_APPLICATION)
@ -49,8 +50,6 @@
#endif
#include "BuildConfig.h"
#include "net/NetAction.h"
#include "MMCTime.h"
#include "StringUtils.h"
@ -106,7 +105,6 @@ void NetRequest::executeTask()
for (auto& header_proxy : m_headerProxies) {
header_proxy->writeHeaders(request);
}
// TODO remove duplication
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
request.setTransferTimeout();
@ -115,11 +113,12 @@ void NetRequest::executeTask()
m_last_progress_time = m_clock.now();
m_last_progress_bytes = 0;
QNetworkReply* rep = getReply(request);
auto rep = getReply(request);
if (rep == nullptr) // it failed
return;
m_reply.reset(rep);
connect(rep, &QNetworkReply::downloadProgress, this, &NetRequest::downloadProgress);
connect(rep, &QNetworkReply::uploadProgress, this, &NetRequest::onProgress);
connect(rep, &QNetworkReply::downloadProgress, this, &NetRequest::onProgress);
connect(rep, &QNetworkReply::finished, this, &NetRequest::downloadFinished);
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15
connect(rep, &QNetworkReply::errorOccurred, this, &NetRequest::downloadError);
@ -130,7 +129,7 @@ void NetRequest::executeTask()
connect(rep, &QNetworkReply::readyRead, this, &NetRequest::downloadReadyRead);
}
void NetRequest::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
void NetRequest::onProgress(qint64 bytesReceived, qint64 bytesTotal)
{
auto now = m_clock.now();
auto elapsed = now - m_last_progress_time;
@ -173,7 +172,9 @@ void NetRequest::downloadError(QNetworkReply::NetworkError error)
}
}
// error happened during download.
qCCritical(logCat) << getUid().toString() << "Failed " << m_url.toString() << " with reason " << error;
qCCritical(logCat) << getUid().toString() << "Failed" << m_url.toString() << "with reason" << error;
if (m_reply)
qCCritical(logCat) << getUid().toString() << "HTTP Status" << replyStatusCode() << ";error" << errorString();
m_state = State::Failed;
}
}
@ -238,7 +239,7 @@ auto NetRequest::handleRedirect() -> bool
m_url = QUrl(redirect.toString());
qCDebug(logCat) << getUid().toString() << "Following redirect to " << m_url.toString();
startAction(m_network);
executeTask();
return true;
}
@ -256,21 +257,18 @@ void NetRequest::downloadFinished()
{
qCDebug(logCat) << getUid().toString() << "Request failed but we are allowed to proceed:" << m_url.toString();
m_sink->abort();
m_reply.reset();
emit succeeded();
emit finished();
return;
} else if (m_state == State::Failed) {
qCDebug(logCat) << getUid().toString() << "Request failed in previous step:" << m_url.toString();
m_sink->abort();
m_reply.reset();
emit failed("");
emit failed(m_reply->errorString());
emit finished();
return;
} else if (m_state == State::AbortedByUser) {
qCDebug(logCat) << getUid().toString() << "Request aborted in previous step:" << m_url.toString();
m_sink->abort();
m_reply.reset();
emit aborted();
emit finished();
return;
@ -284,7 +282,7 @@ void NetRequest::downloadFinished()
if (m_state != State::Succeeded) {
qCDebug(logCat) << getUid().toString() << "Request failed to write:" << m_url.toString();
m_sink->abort();
emit failed("");
emit failed("failed to write in sink");
emit finished();
return;
}
@ -295,13 +293,11 @@ void NetRequest::downloadFinished()
if (m_state != State::Succeeded) {
qCDebug(logCat) << getUid().toString() << "Request failed to finalize:" << m_url.toString();
m_sink->abort();
m_reply.reset();
emit failed("");
emit failed("failed to finalize the request");
emit finished();
return;
}
m_reply.reset();
qCDebug(logCat) << getUid().toString() << "Request succeeded:" << m_url.toString();
emit succeeded();
emit finished();
@ -335,4 +331,23 @@ auto NetRequest::abort() -> bool
return true;
}
int NetRequest::replyStatusCode() const
{
return m_reply ? m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() : -1;
}
QNetworkReply::NetworkError NetRequest::error() const
{
return m_reply ? m_reply->error() : QNetworkReply::NoError;
}
QUrl NetRequest::url() const
{
return m_url;
}
QString NetRequest::errorString() const
{
return m_reply ? m_reply->errorString() : "";
}
} // namespace Net

View File

@ -40,20 +40,23 @@
#pragma once
#include <qloggingcategory.h>
#include <QNetworkReply>
#include <QUrl>
#include <chrono>
#include "NetAction.h"
#include "HeaderProxy.h"
#include "Sink.h"
#include "Validator.h"
#include "QObjectPtr.h"
#include "net/Logging.h"
#include "tasks/Task.h"
namespace Net {
class NetRequest : public NetAction {
class NetRequest : public Task {
Q_OBJECT
protected:
explicit NetRequest() : NetAction() {}
explicit NetRequest() : Task() {}
public:
using Ptr = shared_qobject_ptr<class NetRequest>;
@ -62,26 +65,31 @@ class NetRequest : public NetAction {
public:
~NetRequest() override = default;
void init() override {}
public:
void addValidator(Validator* v);
auto abort() -> bool override;
auto canAbort() const -> bool override { return true; }
void setNetwork(shared_qobject_ptr<QNetworkAccessManager> network) { m_network = network; }
void addHeaderProxy(Net::HeaderProxy* proxy) { m_headerProxies.push_back(std::shared_ptr<Net::HeaderProxy>(proxy)); }
virtual void init() {}
QUrl url() const;
void setUrl(QUrl url) { m_url = url; }
int replyStatusCode() const;
QNetworkReply::NetworkError error() const;
QString errorString() const;
private:
auto handleRedirect() -> bool;
virtual QNetworkReply* getReply(QNetworkRequest&) = 0;
protected slots:
void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override;
void downloadError(QNetworkReply::NetworkError error) override;
void sslErrors(const QList<QSslError>& errors) override;
void downloadFinished() override;
void downloadReadyRead() override;
public slots:
void onProgress(qint64 bytesReceived, qint64 bytesTotal);
void downloadError(QNetworkReply::NetworkError error);
void sslErrors(const QList<QSslError>& errors);
void downloadFinished();
void downloadReadyRead();
void executeTask() override;
protected:
@ -94,6 +102,15 @@ class NetRequest : public NetAction {
std::chrono::steady_clock m_clock;
std::chrono::time_point<std::chrono::steady_clock> m_last_progress_time;
qint64 m_last_progress_bytes;
shared_qobject_ptr<QNetworkAccessManager> m_network;
/// the network reply
unique_qobject_ptr<QNetworkReply> m_reply;
/// source URL
QUrl m_url;
std::vector<std::shared_ptr<Net::HeaderProxy>> m_headerProxies;
};
} // namespace Net

View File

@ -35,9 +35,8 @@
#pragma once
#include "net/NetAction.h"
#include "Validator.h"
#include "tasks/Task.h"
namespace Net {
class Sink {

View File

@ -46,7 +46,8 @@ namespace Net {
QNetworkReply* Upload::getReply(QNetworkRequest& request)
{
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
if (!request.hasRawHeader("Content-Type"))
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
return m_network->post(request, m_post_data);
}

View File

@ -34,7 +34,7 @@
#pragma once
#include "net/NetAction.h"
#include <QNetworkReply>
namespace Net {
class Validator {

View File

@ -5,7 +5,6 @@
qt.*.debug=false
# don't log credentials by default
launcher.auth.credentials.debug=false
katabasis.*.debug=false
# remove the debug lines, other log levels still get through
launcher.task.net.download.debug=false
# enable or disable whole catageries

View File

@ -0,0 +1,46 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023 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/>.
*/
#include "GenericProfiler.h"
#include "BaseInstance.h"
#include "launch/LaunchTask.h"
#include "settings/SettingsObject.h"
class GenericProfiler : public BaseProfiler {
Q_OBJECT
public:
GenericProfiler(SettingsObjectPtr settings, InstancePtr instance, QObject* parent = 0);
protected:
void beginProfilingImpl(shared_qobject_ptr<LaunchTask> process);
};
GenericProfiler::GenericProfiler(SettingsObjectPtr settings, InstancePtr instance, QObject* parent)
: BaseProfiler(settings, instance, parent)
{}
void GenericProfiler::beginProfilingImpl(shared_qobject_ptr<LaunchTask> process)
{
emit readyToLaunch(tr("Started process: %1").arg(process->pid()));
}
BaseExternalTool* GenericProfilerFactory::createTool(InstancePtr instance, QObject* parent)
{
return new GenericProfiler(globalSettings, instance, parent);
}
#include "GenericProfiler.moc"

View File

@ -0,0 +1,29 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023 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/>.
*/
#pragma once
#include "BaseProfiler.h"
class GenericProfilerFactory : public BaseProfilerFactory {
public:
QString name() const override { return "Generic"; }
void registerSettings([[maybe_unused]] SettingsObjectPtr settings) override{};
BaseExternalTool* createTool(InstancePtr instance, QObject* parent = 0) override;
bool check([[maybe_unused]] QString* error) override { return true; };
bool check([[maybe_unused]] const QString& path, [[maybe_unused]] QString* error) override { return true; };
};

View File

@ -37,7 +37,7 @@
#include "ui_MSALoginDialog.h"
#include "DesktopServices.h"
#include "minecraft/auth/AccountTask.h"
#include "minecraft/auth/AuthFlow.h"
#include <QApplication>
#include <QClipboard>
@ -47,30 +47,29 @@
MSALoginDialog::MSALoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::MSALoginDialog)
{
ui->setupUi(this);
ui->progressBar->setVisible(false);
ui->actionButton->setVisible(false);
// ui->buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false);
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
ui->cancel->setEnabled(false);
ui->link->setVisible(false);
ui->copy->setVisible(false);
ui->progressBar->setVisible(false);
connect(ui->cancel, &QPushButton::pressed, this, &QDialog::reject);
connect(ui->copy, &QPushButton::pressed, this, &MSALoginDialog::copyUrl);
}
int MSALoginDialog::exec()
{
setUserInputsEnabled(false);
ui->progressBar->setVisible(true);
// Setup the login task and start it
m_account = MinecraftAccount::createBlankMSA();
m_loginTask = m_account->loginMSA();
connect(m_loginTask.get(), &Task::failed, this, &MSALoginDialog::onTaskFailed);
connect(m_loginTask.get(), &Task::succeeded, this, &MSALoginDialog::onTaskSucceeded);
connect(m_loginTask.get(), &Task::status, this, &MSALoginDialog::onTaskStatus);
connect(m_loginTask.get(), &Task::progress, this, &MSALoginDialog::onTaskProgress);
connect(m_loginTask.get(), &AccountTask::showVerificationUriAndCode, this, &MSALoginDialog::showVerificationUriAndCode);
connect(m_loginTask.get(), &AccountTask::hideVerificationUriAndCode, this, &MSALoginDialog::hideVerificationUriAndCode);
connect(&m_externalLoginTimer, &QTimer::timeout, this, &MSALoginDialog::externalLoginTick);
m_loginTask->start();
m_task = m_account->login(m_using_device_code);
connect(m_task.get(), &Task::failed, this, &MSALoginDialog::onTaskFailed);
connect(m_task.get(), &Task::succeeded, this, &MSALoginDialog::onTaskSucceeded);
connect(m_task.get(), &Task::status, this, &MSALoginDialog::onTaskStatus);
connect(m_task.get(), &AuthFlow::authorizeWithBrowser, this, &MSALoginDialog::authorizeWithBrowser);
connect(m_task.get(), &AuthFlow::authorizeWithBrowserWithExtra, this, &MSALoginDialog::authorizeWithBrowserWithExtra);
connect(ui->cancel, &QPushButton::pressed, m_task.get(), &Task::abort);
connect(&m_external_timer, &QTimer::timeout, this, &MSALoginDialog::externalLoginTick);
m_task->start();
return QDialog::exec();
}
@ -80,60 +79,6 @@ MSALoginDialog::~MSALoginDialog()
delete ui;
}
void MSALoginDialog::externalLoginTick()
{
m_externalLoginElapsed++;
ui->progressBar->setValue(m_externalLoginElapsed);
ui->progressBar->repaint();
if (m_externalLoginElapsed >= m_externalLoginTimeout) {
m_externalLoginTimer.stop();
}
}
void MSALoginDialog::showVerificationUriAndCode(const QUrl& uri, const QString& code, int expiresIn)
{
m_externalLoginElapsed = 0;
m_externalLoginTimeout = expiresIn;
m_externalLoginTimer.setInterval(1000);
m_externalLoginTimer.setSingleShot(false);
m_externalLoginTimer.start();
ui->progressBar->setMaximum(expiresIn);
ui->progressBar->setValue(m_externalLoginElapsed);
QString urlString = uri.toString();
QString linkString = QString("<a href=\"%1\">%2</a>").arg(urlString, urlString);
if (urlString == "https://www.microsoft.com/link" && !code.isEmpty()) {
urlString += QString("?otc=%1").arg(code);
DesktopServices::openUrl(urlString);
ui->label->setText(tr("<p>Please login in the opened browser. If no browser was opened, please open up %1 in "
"a browser and put in the code <b>%2</b> to proceed with login.</p>")
.arg(linkString, code));
} else {
ui->label->setText(
tr("<p>Please open up %1 in a browser and put in the code <b>%2</b> to proceed with login.</p>").arg(linkString, code));
}
ui->actionButton->setVisible(true);
connect(ui->actionButton, &QPushButton::clicked, [=]() {
DesktopServices::openUrl(uri);
QClipboard* cb = QApplication::clipboard();
cb->setText(code);
});
}
void MSALoginDialog::hideVerificationUriAndCode()
{
m_externalLoginTimer.stop();
ui->actionButton->setVisible(false);
}
void MSALoginDialog::setUserInputsEnabled(bool enable)
{
ui->buttonBox->setEnabled(enable);
}
void MSALoginDialog::onTaskFailed(const QString& reason)
{
// Set message
@ -146,12 +91,7 @@ void MSALoginDialog::onTaskFailed(const QString& reason)
processed += "<br />";
}
}
ui->label->setText(processed);
// Re-enable user-interaction
setUserInputsEnabled(true);
ui->progressBar->setVisible(false);
ui->actionButton->setVisible(false);
ui->message->setText(processed);
}
void MSALoginDialog::onTaskSucceeded()
@ -161,22 +101,81 @@ void MSALoginDialog::onTaskSucceeded()
void MSALoginDialog::onTaskStatus(const QString& status)
{
ui->label->setText(status);
}
void MSALoginDialog::onTaskProgress(qint64 current, qint64 total)
{
ui->progressBar->setMaximum(total);
ui->progressBar->setValue(current);
ui->message->setText(status);
ui->cancel->setEnabled(false);
ui->link->setVisible(false);
ui->copy->setVisible(false);
ui->progressBar->setVisible(false);
}
// Public interface
MinecraftAccountPtr MSALoginDialog::newAccount(QWidget* parent, QString msg)
MinecraftAccountPtr MSALoginDialog::newAccount(QWidget* parent, QString msg, bool usingDeviceCode)
{
MSALoginDialog dlg(parent);
dlg.ui->label->setText(msg);
dlg.m_using_device_code = usingDeviceCode;
dlg.ui->message->setText(msg);
if (dlg.exec() == QDialog::Accepted) {
return dlg.m_account;
}
return nullptr;
}
void MSALoginDialog::authorizeWithBrowser(const QUrl& url)
{
ui->cancel->setEnabled(true);
ui->link->setVisible(true);
ui->copy->setVisible(true);
DesktopServices::openUrl(url);
ui->link->setText(url.toDisplayString());
ui->message->setText(
tr("Browser opened to complete the login process."
"<br /><br />"
"If your browser hasn't opened, please manually open the below link in your browser:"));
}
void MSALoginDialog::copyUrl()
{
QClipboard* cb = QApplication::clipboard();
cb->setText(ui->link->text());
}
void MSALoginDialog::authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn)
{
m_external_elapsed = 0;
m_external_timeout = expiresIn;
m_external_timer.setInterval(1000);
m_external_timer.setSingleShot(false);
m_external_timer.start();
ui->progressBar->setMaximum(expiresIn);
ui->progressBar->setValue(m_external_elapsed);
QString linkString = QString("<a href=\"%1\">%2</a>").arg(url, url);
if (url == "https://www.microsoft.com/link" && !code.isEmpty()) {
url += QString("?otc=%1").arg(code);
ui->message->setText(tr("<p>Please login in the opened browser. If no browser was opened, please open up %1 in "
"a browser and put in the code <b>%2</b> to proceed with login.</p>")
.arg(linkString, code));
} else {
ui->message->setText(
tr("<p>Please open up %1 in a browser and put in the code <b>%2</b> to proceed with login.</p>").arg(linkString, code));
}
ui->cancel->setEnabled(true);
ui->link->setVisible(true);
ui->copy->setVisible(true);
ui->progressBar->setVisible(true);
DesktopServices::openUrl(url);
ui->link->setText(code);
}
void MSALoginDialog::externalLoginTick()
{
m_external_elapsed++;
ui->progressBar->setValue(m_external_elapsed);
ui->progressBar->repaint();
if (m_external_elapsed >= m_external_timeout) {
m_external_timer.stop();
}
}

View File

@ -19,6 +19,7 @@
#include <QtCore/QEventLoop>
#include <QtWidgets/QDialog>
#include "minecraft/auth/AuthFlow.h"
#include "minecraft/auth/MinecraftAccount.h"
namespace Ui {
@ -31,29 +32,29 @@ class MSALoginDialog : public QDialog {
public:
~MSALoginDialog();
static MinecraftAccountPtr newAccount(QWidget* parent, QString message);
static MinecraftAccountPtr newAccount(QWidget* parent, QString message, bool usingDeviceCode = false);
int exec() override;
private:
explicit MSALoginDialog(QWidget* parent = 0);
void setUserInputsEnabled(bool enable);
protected slots:
void onTaskFailed(const QString& reason);
void onTaskSucceeded();
void onTaskStatus(const QString& status);
void onTaskProgress(qint64 current, qint64 total);
void showVerificationUriAndCode(const QUrl& uri, const QString& code, int expiresIn);
void hideVerificationUriAndCode();
void authorizeWithBrowser(const QUrl& url);
void authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn);
void copyUrl();
void externalLoginTick();
private:
Ui::MSALoginDialog* ui;
MinecraftAccountPtr m_account;
shared_qobject_ptr<AccountTask> m_loginTask;
QTimer m_externalLoginTimer;
int m_externalLoginElapsed = 0;
int m_externalLoginTimeout = 0;
shared_qobject_ptr<AuthFlow> m_task;
int m_external_elapsed;
int m_external_timeout;
QTimer m_external_timer;
bool m_using_device_code = false;
};

View File

@ -7,11 +7,11 @@
<x>0</x>
<y>0</y>
<width>491</width>
<height>143</height>
<height>208</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<sizepolicy hsizetype="Fixed" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
@ -21,15 +21,28 @@
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<widget class="QLabel" name="message">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>500</width>
<height>500</height>
</size>
</property>
<property name="text">
<string notr="true">Message label placeholder.
aaaaa</string>
<string notr="true"/>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
@ -38,6 +51,28 @@ aaaaa</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="linkLayout">
<item>
<widget class="QLineEdit" name="link">
<property name="readOnly">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="copy">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="copy">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
@ -49,25 +84,11 @@ aaaaa</string>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="actionButton">
<property name="text">
<string>Open page and copy code</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel</set>
</property>
</widget>
</item>
</layout>
<widget class="QPushButton" name="cancel">
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
</layout>
</widget>

View File

@ -1,8 +1,6 @@
#include "OfflineLoginDialog.h"
#include "ui_OfflineLoginDialog.h"
#include "minecraft/auth/AccountTask.h"
#include <QtWidgets/QPushButton>
OfflineLoginDialog::OfflineLoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::OfflineLoginDialog)
@ -28,7 +26,7 @@ void OfflineLoginDialog::accept()
// Setup the login task and start it
m_account = MinecraftAccount::createOffline(ui->userTextBox->text());
m_loginTask = m_account->loginOffline();
m_loginTask = m_account->login();
connect(m_loginTask.get(), &Task::failed, this, &OfflineLoginDialog::onTaskFailed);
connect(m_loginTask.get(), &Task::succeeded, this, &OfflineLoginDialog::onTaskSucceeded);
connect(m_loginTask.get(), &Task::status, this, &OfflineLoginDialog::onTaskStatus);

View File

@ -45,8 +45,9 @@
#include "ui/dialogs/ProgressDialog.h"
#include <Application.h>
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
#include "net/StaticHeaderProxy.h"
#include "net/Upload.h"
ProfileSetupDialog::ProfileSetupDialog(MinecraftAccountPtr accountToSetup, QWidget* parent)
: QDialog(parent), m_accountToSetup(accountToSetup), ui(new Ui::ProfileSetupDialog)
@ -150,28 +151,27 @@ void ProfileSetupDialog::checkName(const QString& name)
currentCheck = name;
isChecking = true;
auto token = m_accountToSetup->accessToken();
QUrl url(QString("https://api.minecraftservices.com/minecraft/profile/name/%1/available").arg(name));
auto headers = QList<Net::HeaderPair>{ { "Content-Type", "application/json" },
{ "Accept", "application/json" },
{ "Authorization", QString("Bearer %1").arg(m_accountToSetup->accessToken()).toUtf8() } };
auto url = QString("https://api.minecraftservices.com/minecraft/profile/name/%1/available").arg(name);
QNetworkRequest request = QNetworkRequest(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Accept", "application/json");
request.setRawHeader("Authorization", QString("Bearer %1").arg(token).toUtf8());
m_check_response.reset(new QByteArray());
if (m_check_task)
disconnect(m_check_task.get(), nullptr, this, nullptr);
m_check_task = Net::Download::makeByteArray(url, m_check_response);
m_check_task->addHeaderProxy(new Net::StaticHeaderProxy(headers));
AuthRequest* requestor = new AuthRequest(this);
connect(requestor, &AuthRequest::finished, this, &ProfileSetupDialog::checkFinished);
requestor->get(request);
connect(m_check_task.get(), &Task::finished, this, &ProfileSetupDialog::checkFinished);
m_check_task->setNetwork(APPLICATION->network());
m_check_task->start();
}
void ProfileSetupDialog::checkFinished(QNetworkReply::NetworkError error,
QByteArray profileData,
[[maybe_unused]] QList<QNetworkReply::RawHeaderPair> headers)
void ProfileSetupDialog::checkFinished()
{
auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
requestor->deleteLater();
if (error == QNetworkReply::NoError) {
auto doc = QJsonDocument::fromJson(profileData);
if (m_check_task->error() == QNetworkReply::NoError) {
auto doc = QJsonDocument::fromJson(*m_check_response);
auto root = doc.object();
auto statusValue = root.value("status").toString("INVALID");
if (statusValue == "AVAILABLE") {
@ -195,20 +195,22 @@ void ProfileSetupDialog::setupProfile(const QString& profileName)
return;
}
auto token = m_accountToSetup->accessToken();
auto url = QString("https://api.minecraftservices.com/minecraft/profile");
QNetworkRequest request = QNetworkRequest(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Accept", "application/json");
request.setRawHeader("Authorization", QString("Bearer %1").arg(token).toUtf8());
QString payloadTemplate("{\"profileName\":\"%1\"}");
auto profileData = payloadTemplate.arg(profileName).toUtf8();
AuthRequest* requestor = new AuthRequest(this);
connect(requestor, &AuthRequest::finished, this, &ProfileSetupDialog::setupProfileFinished);
requestor->post(request, profileData);
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_accountToSetup->accessToken()).toUtf8() } };
m_profile_response.reset(new QByteArray());
m_profile_task = Net::Upload::makeByteArray(url, m_profile_response, payloadTemplate.arg(profileName).toUtf8());
m_profile_task->addHeaderProxy(new Net::StaticHeaderProxy(headers));
connect(m_profile_task.get(), &Task::finished, this, &ProfileSetupDialog::setupProfileFinished);
m_profile_task->setNetwork(APPLICATION->network());
m_profile_task->start();
isWorking = true;
auto button = ui->buttonBox->button(QDialogButtonBox::Cancel);
@ -244,22 +246,17 @@ struct MojangError {
} // namespace
void ProfileSetupDialog::setupProfileFinished(QNetworkReply::NetworkError error,
QByteArray errorData,
[[maybe_unused]] QList<QNetworkReply::RawHeaderPair> headers)
void ProfileSetupDialog::setupProfileFinished()
{
auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
requestor->deleteLater();
isWorking = false;
if (error == QNetworkReply::NoError) {
if (m_profile_task->error() == QNetworkReply::NoError) {
/*
* data contains the profile in the response
* ... we could parse it and update the account, but let's just return back to the normal login flow instead...
*/
accept();
} else {
auto parsedError = MojangError::fromJSON(errorData);
auto parsedError = MojangError::fromJSON(*m_profile_response);
ui->errorLabel->setVisible(true);
ui->errorLabel->setText(tr("The server returned the following error:") + "\n\n" + parsedError.errorMessage);
qDebug() << parsedError.rawError;

View File

@ -22,6 +22,8 @@
#include <minecraft/auth/MinecraftAccount.h>
#include <memory>
#include "net/Download.h"
#include "net/Upload.h"
namespace Ui {
class ProfileSetupDialog;
@ -40,10 +42,10 @@ class ProfileSetupDialog : public QDialog {
void on_buttonBox_rejected();
void nameEdited(const QString& name);
void checkFinished(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers);
void startCheck();
void setupProfileFinished(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers);
void checkFinished();
void setupProfileFinished();
protected:
void scheduleCheck(const QString& name);
@ -67,4 +69,10 @@ class ProfileSetupDialog : public QDialog {
QString currentCheck;
QTimer checkStartTimer;
std::shared_ptr<QByteArray> m_check_response;
Net::Download::Ptr m_check_task;
std::shared_ptr<QByteArray> m_profile_response;
Net::Upload::Ptr m_profile_task;
};

View File

@ -36,7 +36,6 @@
#include "Json.h"
#include "QObjectPtr.h"
#include "minecraft/auth/AccountTask.h"
#include "minecraft/auth/Parsers.h"
#include "minecraft/skins/CapeChange.h"
#include "minecraft/skins/SkinDelete.h"

View File

@ -40,6 +40,7 @@
#include <QItemSelectionModel>
#include <QMenu>
#include <QPushButton>
#include <QDebug>
@ -129,8 +130,19 @@ void AccountListPage::listChanged()
void AccountListPage::on_actionAddMicrosoft_triggered()
{
MinecraftAccountPtr account =
MSALoginDialog::newAccount(this, tr("Please enter your Mojang account email and password to add your account."));
QMessageBox box(this);
box.setWindowTitle(tr("Add account"));
box.setText(tr("How do you want to login?"));
box.setIcon(QMessageBox::Question);
auto deviceCode = box.addButton(tr("Legacy"), QMessageBox::ButtonRole::YesRole);
auto authCode = box.addButton(tr("Recommended"), QMessageBox::ButtonRole::NoRole);
auto cancel = box.addButton(tr("Cancel"), QMessageBox::ButtonRole::RejectRole);
box.setDefaultButton(authCode);
box.exec();
if ((box.clickedButton() != deviceCode && box.clickedButton() != authCode) || box.clickedButton() == cancel)
return;
MinecraftAccountPtr account = MSALoginDialog::newAccount(
this, tr("Please enter your Mojang account email and password to add your account."), box.clickedButton() == deviceCode);
if (account) {
m_accounts->addAccount(account);

View File

@ -88,6 +88,7 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared
};
connect(selection_model, &QItemSelectionModel::selectionChanged, this, updateExtra);
connect(model.get(), &ResourceFolderModel::updateFinished, this, updateExtra);
connect(model.get(), &ResourceFolderModel::parseFinished, this, updateExtra);
connect(ui->filterEdit, &QLineEdit::textChanged, this, &ExternalResourcesPage::filterTextChanged);

View File

@ -331,7 +331,7 @@ std::optional<QIcon> ResourceModel::getIcon(QModelIndex& index, const QUrl& url)
auto icon_fetch_action = Net::ApiDownload::makeCached(url, cache_entry);
auto full_file_path = cache_entry->getFullPath();
connect(icon_fetch_action.get(), &NetAction::succeeded, this, [=] {
connect(icon_fetch_action.get(), &Task::succeeded, this, [=] {
auto icon = QIcon(full_file_path);
QPixmapCache::insert(url.toString(), icon.pixmap(icon.actualSize({ 64, 64 })));
@ -339,7 +339,7 @@ std::optional<QIcon> ResourceModel::getIcon(QModelIndex& index, const QUrl& url)
emit dataChanged(index, index, { Qt::DecorationRole });
});
connect(icon_fetch_action.get(), &NetAction::failed, this, [=] {
connect(icon_fetch_action.get(), &Task::failed, this, [=] {
m_currently_running_icon_actions.remove(url);
m_failed_icon_actions.insert(url);
});

View File

@ -352,10 +352,10 @@ void ModpackListModel::searchRequestForOneSucceeded(QJsonDocument& doc)
void ModpackListModel::searchRequestFailed(QString reason)
{
auto failed_action = dynamic_cast<NetJob*>(jobPtr.get())->getFailedActions().at(0);
if (!failed_action->m_reply) {
if (failed_action->replyStatusCode() == -1) {
// Network error
QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load modpacks."));
} else if (failed_action->m_reply && failed_action->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) {
} else if (failed_action->replyStatusCode() == 409) {
// 409 Gone, notify user to update
QMessageBox::critical(nullptr, tr("Error"),
//: %1 refers to the launcher itself

View File

@ -32,14 +32,6 @@ Simple Java tool that prints the JVM details - version and platform bitness.
Do what you want with it. It is so trivial that noone cares.
## Katabasis
Oauth2 library customized for Microsoft authentication.
This is a fork of the [O2 library](https://github.com/pipacs/o2).
MIT licensed.
## launcher
Java launcher part for Minecraft.

View File

@ -1,2 +0,0 @@
build/
*.kdev4

View File

@ -1,58 +0,0 @@
cmake_minimum_required(VERSION 3.9.4)
string(COMPARE EQUAL "${CMAKE_SOURCE_DIR}" "${CMAKE_BUILD_DIR}" IS_IN_SOURCE_BUILD)
if(IS_IN_SOURCE_BUILD)
message(FATAL_ERROR "You are building Katabasis in-source. Please separate the build tree from the source tree.")
endif()
project(Katabasis)
enable_testing()
set(CMAKE_AUTOMOC ON)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_CXX_STANDARD_REQUIRED true)
set(CMAKE_C_STANDARD_REQUIRED true)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_C_STANDARD 11)
if(QT_VERSION_MAJOR EQUAL 5)
find_package(Qt5 COMPONENTS Core Network REQUIRED)
elseif(Launcher_QT_VERSION_MAJOR EQUAL 6)
find_package(Qt6 COMPONENTS Core Network REQUIRED)
endif()
set( katabasis_PRIVATE
src/DeviceFlow.cpp
src/JsonResponse.cpp
src/JsonResponse.h
src/PollServer.cpp
src/Reply.cpp
)
set( katabasis_PUBLIC
include/katabasis/DeviceFlow.h
include/katabasis/Globals.h
include/katabasis/PollServer.h
include/katabasis/Reply.h
include/katabasis/RequestParameter.h
)
ecm_qt_declare_logging_category(katabasis_PRIVATE
HEADER KatabasisLogging.h # NOTE: this won't be in src/, but CMAKE_BINARY_DIR/src isn't included by default so this should be fine
IDENTIFIER katabasisCredentials
CATEGORY_NAME "katabasis.credentials"
DEFAULT_SEVERITY Warning
DESCRIPTION "Secrets and credentials from Katabasis"
EXPORT "Katabasis"
)
add_library( Katabasis STATIC ${katabasis_PRIVATE} ${katabasis_PUBLIC} )
target_link_libraries(Katabasis Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Network)
# needed for statically linked Katabasis in shared libs on x86_64
set_target_properties(Katabasis
PROPERTIES POSITION_INDEPENDENT_CODE TRUE
)
target_include_directories(Katabasis PUBLIC include PRIVATE src include/katabasis)

View File

@ -1,23 +0,0 @@
Copyright (c) 2012, Akos Polster
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -1,36 +0,0 @@
# Katabasis - MS-flavored OAuth for Qt, derived from the O2 library
This library's sole purpose is to make interacting with MSA and various MSA and XBox authenticated services less painful.
It may be possible to backport some of the changes to O2 in the future, but for the sake of going fast, all compatibility concerns have been ignored.
[You can find the original library's git repository here.](https://github.com/pipacs/o2)
Notes to contributors:
* Please follow the coding style of the existing source, where reasonable
* Code contributions are released under Simplified BSD License, as specified in LICENSE. Do not contribute if this license does not suit your code
* If you are interested in working on this, come to the Prism Launcher Discord server and talk first
## Installation
Clone the Github repository, integrate the it into your CMake build system.
The library is static only, dynamic linking and system-wide installation are out of scope and undesirable.
## Usage
At this stage, don't, unless you want to help with the library itself.
This is an experimental fork of the O2 library and is undergoing a big design/architecture shift in order to support different features:
* Multiple accounts
* Multi-stage authentication/authorization schemes
* Tighter control over token chains and their storage
* Talking to complex APIs and individually authorized microservices
* Token lifetime management, 'offline mode' and resilience in face of network failures
* Token and claims/entitlements validation
* Caching of some API results
* XBox magic
* Mojang magic
* Generally, magic that you would spend weeks on researching while getting confused by contradictory/incomplete documentation (if any is available)

View File

@ -1,108 +0,0 @@
## O2 library by Akos Polster and contributors
[The origin of this fork.](https://github.com/pipacs/o2)
> Copyright (c) 2012, Akos Polster
> All rights reserved.
>
> Redistribution and use in source and binary forms, with or without
> modification, are permitted provided that the following conditions are met:
>
> * Redistributions of source code must retain the above copyright notice, this
> list of conditions and the following disclaimer.
>
> * Redistributions in binary form must reproduce the above copyright notice,
> this list of conditions and the following disclaimer in the documentation
> and/or other materials provided with the distribution.
>
> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
> AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
> IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
> DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
> FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
> DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
> SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
> CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
> OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
> OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
## SimpleCrypt by Andre Somers
Cryptographic methods for Qt.
> Copyright (c) 2011, Andre Somers
> All rights reserved.
>
> Redistribution and use in source and binary forms, with or without
> modification, are permitted provided that the following conditions are met:
>
> * Redistributions of source code must retain the above copyright
> notice, this list of conditions and the following disclaimer.
> * Redistributions in binary form must reproduce the above copyright
> notice, this list of conditions and the following disclaimer in the
> documentation and/or other materials provided with the distribution.
> * Neither the name of the Rathenau Instituut, Andre Somers nor the
> names of its contributors may be used to endorse or promote products
> derived from this software without specific prior written permission.
>
> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
> ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
> WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
> DISCLAIMED. IN NO EVENT SHALL ANDRE SOMERS BE LIABLE FOR ANY
> DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
> (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
> LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
> ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
> (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
> SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
## Mandeep Sandhu <mandeepsandhu.chd@gmail.com>
Configurable settings storage, Twitter XAuth specialization, new demos, cleanups.
> "Hi Akos,
>
> I'm writing this mail to confirm that my contributions to the O2 library, available here <https://github.com/pipacs/o2>, can be freely distributed according to the project's license (as shown in the LICENSE file).
>
> Regards,
> -mandeep"
## Sergey Gavrushkin <https://github.com/ncux>
FreshBooks specialization
## Theofilos Intzoglou <https://github.com/parapente>
Hubic specialization
## Dimitar
SurveyMonkey specialization
## David Brooks <https://github.com/dbrnz>
CMake related fixes and improvements.
## Lukas Vogel <https://github.com/lukedirtwalker>
Spotify support
## Alan Garny <https://github.com/agarny>
Windows DLL build support
## MartinMikita <https://github.com/MartinMikita>
Bug fixes
## Larry Shaffer <https://github.com/dakcarto>
Versioning, shared lib, install target and header support
## Gilmanov Ildar <https://github.com/gilmanov-ildar>
Bug fixes, support for ```qml``` module
## Fabian Vogt <https://github.com/Vogtinator>
Bug fixes, support for building without Qt keywords enabled

View File

@ -1,33 +0,0 @@
#pragma once
#include <QDateTime>
#include <QMap>
#include <QString>
#include <QVariantMap>
namespace Katabasis {
enum class Activity {
Idle,
LoggingIn,
LoggingOut,
Refreshing,
FailedSoft, //!< soft failure. this generally means the user auth details haven't been invalidated
FailedHard, //!< hard failure. auth is invalid
FailedGone, //!< hard failure. auth is invalid, and the account no longer exists
Succeeded
};
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;
};
} // namespace Katabasis

View File

@ -1,149 +0,0 @@
#pragma once
#include <QLoggingCategory>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QPair>
#include "Bits.h"
#include "Reply.h"
#include "RequestParameter.h"
namespace Katabasis {
class ReplyServer;
class PollServer;
/// Simple OAuth2 Device Flow authenticator.
class DeviceFlow : public QObject {
Q_OBJECT
public:
Q_ENUMS(GrantFlow)
public:
struct Options {
QString userAgent = QStringLiteral("Katabasis/1.0");
QString responseType = QStringLiteral("code");
QString scope;
QString clientIdentifier;
QString clientSecret;
QUrl authorizationUrl;
QUrl accessTokenUrl;
};
public:
/// Are we authenticated?
bool linked();
/// Authentication token.
QString token();
/// Provider-specific extra tokens, available after a successful authentication
QVariantMap extraTokens();
public:
// TODO: put in `Options`
/// User-defined extra parameters to append to request URL
QVariantMap extraRequestParams();
void setExtraRequestParams(const QVariantMap& value);
// TODO: split up the class into multiple, each implementing one OAuth2 flow
/// Grant type (if non-standard)
QString grantType();
void setGrantType(const QString& value);
public:
/// Constructor.
/// @param parent Parent object.
explicit DeviceFlow(Options& opts, Token& token, QObject* parent = 0, QNetworkAccessManager* manager = 0);
/// Get refresh token.
QString refreshToken();
/// Get token expiration time
QDateTime expires();
public slots:
/// Authenticate.
void login();
/// De-authenticate.
void logout();
/// Refresh token.
bool refresh();
/// Handle situation where reply server has opted to close its connection
void serverHasClosed(bool paramsfound = false);
signals:
/// Emitted when client needs to open a web browser window, with the given URL.
void openBrowser(const QUrl& url);
/// Emitted when client can close the browser window.
void closeBrowser();
/// Emitted when client needs to show a verification uri and user code
void showVerificationUriAndCode(const QUrl& uri, const QString& code, int expiresIn);
/// Emitted when the internal state changes
void activityChanged(Activity activity);
public slots:
/// Handle verification response.
void onVerificationReceived(QMap<QString, QString>);
protected slots:
/// Handle completion of a Device Authorization Request
void onDeviceAuthReplyFinished();
/// Handle completion of a refresh request.
void onRefreshFinished();
/// Handle failure of a refresh request.
void onRefreshError(QNetworkReply::NetworkError error, QNetworkReply* reply);
protected:
/// Set refresh token.
void setRefreshToken(const QString& v);
/// Set token expiration time.
void setExpires(QDateTime v);
/// Start polling authorization server
void startPollServer(const QVariantMap& params, int expiresIn);
/// Set authentication token.
void setToken(const QString& v);
/// Set the linked state
void setLinked(bool v);
/// Set extra tokens found in OAuth response
void setExtraTokens(QVariantMap extraTokens);
/// Set local poll server
void setPollServer(PollServer* server);
PollServer* pollServer() const;
void updateActivity(Activity activity);
protected:
Options options_;
QVariantMap extraReqParams_;
QNetworkAccessManager* manager_ = nullptr;
ReplyList timedReplies_;
QString grantType_;
protected:
Token& token_;
private:
PollServer* pollServer_ = nullptr;
Activity activity_ = Activity::Idle;
};
} // namespace Katabasis

Some files were not shown because too many files have changed in this diff Show More