diff options
Diffstat (limited to 'meshmc/libraries/katabasis')
| -rw-r--r-- | meshmc/libraries/katabasis/.gitignore | 2 | ||||
| -rw-r--r-- | meshmc/libraries/katabasis/CMakeLists.txt | 53 | ||||
| -rw-r--r-- | meshmc/libraries/katabasis/LICENSE | 23 | ||||
| -rw-r--r-- | meshmc/libraries/katabasis/README.md | 36 | ||||
| -rw-r--r-- | meshmc/libraries/katabasis/acknowledgements.md | 110 | ||||
| -rw-r--r-- | meshmc/libraries/katabasis/include/katabasis/Bits.h | 57 | ||||
| -rw-r--r-- | meshmc/libraries/katabasis/include/katabasis/DeviceFlow.h | 178 | ||||
| -rw-r--r-- | meshmc/libraries/katabasis/include/katabasis/Globals.h | 82 | ||||
| -rw-r--r-- | meshmc/libraries/katabasis/include/katabasis/PollServer.h | 73 | ||||
| -rw-r--r-- | meshmc/libraries/katabasis/include/katabasis/Reply.h | 92 | ||||
| -rw-r--r-- | meshmc/libraries/katabasis/include/katabasis/RequestParameter.h | 42 | ||||
| -rw-r--r-- | meshmc/libraries/katabasis/src/DeviceFlow.cpp | 539 | ||||
| -rw-r--r-- | meshmc/libraries/katabasis/src/JsonResponse.cpp | 51 | ||||
| -rw-r--r-- | meshmc/libraries/katabasis/src/JsonResponse.h | 34 | ||||
| -rw-r--r-- | meshmc/libraries/katabasis/src/PollServer.cpp | 147 | ||||
| -rw-r--r-- | meshmc/libraries/katabasis/src/Reply.cpp | 96 |
16 files changed, 1615 insertions, 0 deletions
diff --git a/meshmc/libraries/katabasis/.gitignore b/meshmc/libraries/katabasis/.gitignore new file mode 100644 index 0000000000..35e189c5ef --- /dev/null +++ b/meshmc/libraries/katabasis/.gitignore @@ -0,0 +1,2 @@ +build/ +*.kdev4 diff --git a/meshmc/libraries/katabasis/CMakeLists.txt b/meshmc/libraries/katabasis/CMakeLists.txt new file mode 100644 index 0000000000..a42fb3057d --- /dev/null +++ b/meshmc/libraries/katabasis/CMakeLists.txt @@ -0,0 +1,53 @@ +cmake_minimum_required(VERSION 3.25) + +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() + +if (CMAKE_SYSTEM_NAME STREQUAL "Linux") + if(CMAKE_HOST_SYSTEM_VERSION MATCHES ".*[Mm]icrosoft.*" OR + CMAKE_HOST_SYSTEM_VERSION MATCHES ".*WSL.*" + ) + message(FATAL_ERROR "Building Katabasis is not supported in Linux-on-Windows distributions. Use a real Linux machine, not a fraudulent one.") + endif() +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) + +find_package(Qt6 COMPONENTS Core Network REQUIRED) + +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 +) + +add_library( Katabasis STATIC ${katabasis_PRIVATE} ${katabasis_PUBLIC} ) +target_link_libraries(Katabasis Qt6::Core Qt6::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) diff --git a/meshmc/libraries/katabasis/LICENSE b/meshmc/libraries/katabasis/LICENSE new file mode 100644 index 0000000000..9ac8d42fb0 --- /dev/null +++ b/meshmc/libraries/katabasis/LICENSE @@ -0,0 +1,23 @@ +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. diff --git a/meshmc/libraries/katabasis/README.md b/meshmc/libraries/katabasis/README.md new file mode 100644 index 0000000000..646a128670 --- /dev/null +++ b/meshmc/libraries/katabasis/README.md @@ -0,0 +1,36 @@ +# Katabasis - MS-flavoerd 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 MeshMC 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) diff --git a/meshmc/libraries/katabasis/acknowledgements.md b/meshmc/libraries/katabasis/acknowledgements.md new file mode 100644 index 0000000000..c1c8a3d49e --- /dev/null +++ b/meshmc/libraries/katabasis/acknowledgements.md @@ -0,0 +1,110 @@ +# 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 + diff --git a/meshmc/libraries/katabasis/include/katabasis/Bits.h b/meshmc/libraries/katabasis/include/katabasis/Bits.h new file mode 100644 index 0000000000..bd0aed8508 --- /dev/null +++ b/meshmc/libraries/katabasis/include/katabasis/Bits.h @@ -0,0 +1,57 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * 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, either version 3 of the License, or + * (at your option) any later version. + * + * 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 <QString> +#include <QDateTime> +#include <QMap> +#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 diff --git a/meshmc/libraries/katabasis/include/katabasis/DeviceFlow.h b/meshmc/libraries/katabasis/include/katabasis/DeviceFlow.h new file mode 100644 index 0000000000..37f39cc658 --- /dev/null +++ b/meshmc/libraries/katabasis/include/katabasis/DeviceFlow.h @@ -0,0 +1,178 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * 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, either version 3 of the License, or + * (at your option) any later version. + * + * 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 <QNetworkAccessManager> +#include <QNetworkRequest> +#include <QNetworkReply> +#include <QPair> + +#include "Reply.h" +#include "RequestParameter.h" +#include "Bits.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 diff --git a/meshmc/libraries/katabasis/include/katabasis/Globals.h b/meshmc/libraries/katabasis/include/katabasis/Globals.h new file mode 100644 index 0000000000..cf6fa39b21 --- /dev/null +++ b/meshmc/libraries/katabasis/include/katabasis/Globals.h @@ -0,0 +1,82 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * 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, either version 3 of the License, or + * (at your option) any later version. + * + * 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 + +namespace Katabasis +{ + + // Common constants + const char ENCRYPTION_KEY[] = "12345678"; + const char MIME_TYPE_XFORM[] = "application/x-www-form-urlencoded"; + const char MIME_TYPE_JSON[] = "application/json"; + + // OAuth 1/1.1 Request Parameters + const char OAUTH_CALLBACK[] = "oauth_callback"; + const char OAUTH_CONSUMER_KEY[] = "oauth_consumer_key"; + const char OAUTH_NONCE[] = "oauth_nonce"; + const char OAUTH_SIGNATURE[] = "oauth_signature"; + const char OAUTH_SIGNATURE_METHOD[] = "oauth_signature_method"; + const char OAUTH_TIMESTAMP[] = "oauth_timestamp"; + const char OAUTH_VERSION[] = "oauth_version"; + // OAuth 1/1.1 Response Parameters + const char OAUTH_TOKEN[] = "oauth_token"; + const char OAUTH_TOKEN_SECRET[] = "oauth_token_secret"; + const char OAUTH_CALLBACK_CONFIRMED[] = "oauth_callback_confirmed"; + const char OAUTH_VERFIER[] = "oauth_verifier"; + + // OAuth 2 Request Parameters + const char OAUTH2_RESPONSE_TYPE[] = "response_type"; + const char OAUTH2_CLIENT_ID[] = "client_id"; + const char OAUTH2_CLIENT_SECRET[] = "client_secret"; + const char OAUTH2_USERNAME[] = "username"; + const char OAUTH2_PASSWORD[] = "password"; + const char OAUTH2_REDIRECT_URI[] = "redirect_uri"; + const char OAUTH2_SCOPE[] = "scope"; + const char OAUTH2_GRANT_TYPE_CODE[] = "code"; + const char OAUTH2_GRANT_TYPE_TOKEN[] = "token"; + const char OAUTH2_GRANT_TYPE_PASSWORD[] = "password"; + const char OAUTH2_GRANT_TYPE_DEVICE[] = + "urn:ietf:params:oauth:grant-type:device_code"; + const char OAUTH2_GRANT_TYPE[] = "grant_type"; + const char OAUTH2_API_KEY[] = "api_key"; + const char OAUTH2_STATE[] = "state"; + const char OAUTH2_CODE[] = "code"; + + // OAuth 2 Response Parameters + const char OAUTH2_ACCESS_TOKEN[] = "access_token"; + const char OAUTH2_REFRESH_TOKEN[] = "refresh_token"; + const char OAUTH2_EXPIRES_IN[] = "expires_in"; + const char OAUTH2_DEVICE_CODE[] = "device_code"; + const char OAUTH2_USER_CODE[] = "user_code"; + const char OAUTH2_VERIFICATION_URI[] = "verification_uri"; + const char OAUTH2_VERIFICATION_URL[] = "verification_url"; // Google sign-in + const char OAUTH2_VERIFICATION_URI_COMPLETE[] = "verification_uri_complete"; + const char OAUTH2_INTERVAL[] = "interval"; + + // Parameter values + const char AUTHORIZATION_CODE[] = "authorization_code"; + + // Standard HTTP headers + const char HTTP_HTTP_HEADER[] = "HTTP"; + const char HTTP_AUTHORIZATION_HEADER[] = "Authorization"; + +} // namespace Katabasis diff --git a/meshmc/libraries/katabasis/include/katabasis/PollServer.h b/meshmc/libraries/katabasis/include/katabasis/PollServer.h new file mode 100644 index 0000000000..d070131f49 --- /dev/null +++ b/meshmc/libraries/katabasis/include/katabasis/PollServer.h @@ -0,0 +1,73 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * 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, either version 3 of the License, or + * (at your option) any later version. + * + * 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 <QByteArray> +#include <QMap> +#include <QNetworkRequest> +#include <QObject> +#include <QString> +#include <QTimer> + +class QNetworkAccessManager; + +namespace Katabasis +{ + + /// Poll an authorization server for token + class PollServer : public QObject + { + Q_OBJECT + + public: + explicit PollServer(QNetworkAccessManager* manager, + const QNetworkRequest& request, + const QByteArray& payload, int expiresIn, + QObject* parent = 0); + + /// Seconds to wait between polling requests + Q_PROPERTY(int interval READ interval WRITE setInterval) + int interval() const; + void setInterval(int interval); + + signals: + void verificationReceived(QMap<QString, QString>); + void serverClosed(bool); // whether it has found parameters + + public slots: + void startPolling(); + + protected slots: + void onPollTimeout(); + void onExpiration(); + void onReplyFinished(); + + protected: + QNetworkAccessManager* manager_; + const QNetworkRequest request_; + const QByteArray payload_; + const int expiresIn_; + QTimer expirationTimer; + QTimer pollTimer; + }; + +} // namespace Katabasis diff --git a/meshmc/libraries/katabasis/include/katabasis/Reply.h b/meshmc/libraries/katabasis/include/katabasis/Reply.h new file mode 100644 index 0000000000..dbfa939829 --- /dev/null +++ b/meshmc/libraries/katabasis/include/katabasis/Reply.h @@ -0,0 +1,92 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * 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, either version 3 of the License, or + * (at your option) any later version. + * + * 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 <QList> +#include <QTimer> +#include <QNetworkRequest> +#include <QNetworkReply> +#include <QNetworkAccessManager> +#include <QByteArray> + +namespace Katabasis +{ + + constexpr int defaultTimeout = 30 * 1000; + + /// A network request/reply pair that can time out. + class Reply : public QTimer + { + Q_OBJECT + + public: + Reply(QNetworkReply* reply, int timeOut = defaultTimeout, + QObject* parent = 0); + + signals: + void error(QNetworkReply::NetworkError); + + public slots: + /// When time out occurs, the QNetworkReply's error() signal is + /// triggered. + void onTimeOut(); + + public: + QNetworkReply* reply; + bool timedOut = false; + }; + + /// List of O2Replies. + class ReplyList + { + public: + ReplyList() + { + ignoreSslErrors_ = false; + } + + /// Destructor. + /// Deletes all O2Reply instances in the list. + virtual ~ReplyList(); + + /// Create a new O2Reply from a QNetworkReply, and add it to this list. + void add(QNetworkReply* reply, int timeOut = defaultTimeout); + + /// Add an O2Reply to the list, while taking ownership of it. + void add(Reply* reply); + + /// Remove item from the list that corresponds to a QNetworkReply. + void remove(QNetworkReply* reply); + + /// Find an O2Reply in the list, corresponding to a QNetworkReply. + /// @return Matching O2Reply or NULL. + Reply* find(QNetworkReply* reply); + + bool ignoreSslErrors(); + void setIgnoreSslErrors(bool ignoreSslErrors); + + protected: + QList<Reply*> replies_; + bool ignoreSslErrors_; + }; + +} // namespace Katabasis diff --git a/meshmc/libraries/katabasis/include/katabasis/RequestParameter.h b/meshmc/libraries/katabasis/include/katabasis/RequestParameter.h new file mode 100644 index 0000000000..e7ff1efda3 --- /dev/null +++ b/meshmc/libraries/katabasis/include/katabasis/RequestParameter.h @@ -0,0 +1,42 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * 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, either version 3 of the License, or + * (at your option) any later version. + * + * 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 + +namespace Katabasis +{ + + /// Request parameter (name-value pair) participating in authentication. + struct RequestParameter { + RequestParameter(const QByteArray& n, const QByteArray& v) + : name(n), value(v) + { + } + bool operator<(const RequestParameter& other) const + { + return (name == other.name) ? (value < other.value) + : (name < other.name); + } + QByteArray name; + QByteArray value; + }; + +} // namespace Katabasis diff --git a/meshmc/libraries/katabasis/src/DeviceFlow.cpp b/meshmc/libraries/katabasis/src/DeviceFlow.cpp new file mode 100644 index 0000000000..d03b3efd8f --- /dev/null +++ b/meshmc/libraries/katabasis/src/DeviceFlow.cpp @@ -0,0 +1,539 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * 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, either version 3 of the License, or + * (at your option) any later version. + * + * 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 <QList> +#include <QPair> +#include <QDebug> +#include <QTcpServer> +#include <QMap> +#include <QNetworkRequest> +#include <QNetworkReply> +#include <QNetworkAccessManager> +#include <QDateTime> +#include <QCryptographicHash> +#include <QTimer> +#include <QVariantMap> +#include <QUuid> +#include <QDataStream> + +#include <QUrlQuery> + +#include "katabasis/DeviceFlow.h" +#include "katabasis/PollServer.h" +#include "katabasis/Globals.h" + +#include "JsonResponse.h" + +namespace +{ + // ref: https://tools.ietf.org/html/rfc8628#section-3.2 + // Exception: Google sign-in uses "verification_url" instead of "*_uri" - + // we'll accept both. + bool hasMandatoryDeviceAuthParams(const QVariantMap& params) + { + if (!params.contains(Katabasis::OAUTH2_DEVICE_CODE)) + return false; + + if (!params.contains(Katabasis::OAUTH2_USER_CODE)) + return false; + + if (!(params.contains(Katabasis::OAUTH2_VERIFICATION_URI) || + params.contains(Katabasis::OAUTH2_VERIFICATION_URL))) + return false; + + if (!params.contains(Katabasis::OAUTH2_EXPIRES_IN)) + return false; + + return true; + } + + QByteArray + createQueryParameters(const QList<Katabasis::RequestParameter>& parameters) + { + QByteArray ret; + bool first = true; + for (auto& h : parameters) { + if (first) { + first = false; + } else { + ret.append("&"); + } + ret.append(QUrl::toPercentEncoding(h.name) + "=" + + QUrl::toPercentEncoding(h.value)); + } + return ret; + } +} // namespace + +namespace Katabasis +{ + + DeviceFlow::DeviceFlow(Options& opts, Token& token, QObject* parent, + QNetworkAccessManager* manager) + : QObject(parent), token_(token) + { + manager_ = manager ? manager : new QNetworkAccessManager(this); + qRegisterMetaType<QNetworkReply::NetworkError>( + "QNetworkReply::NetworkError"); + options_ = opts; + } + + bool DeviceFlow::linked() + { + return token_.validity != Validity::None; + } + void DeviceFlow::setLinked(bool v) + { + qDebug() << "DeviceFlow::setLinked:" << (v ? "true" : "false"); + token_.validity = v ? Validity::Certain : Validity::None; + } + + void DeviceFlow::updateActivity(Activity activity) + { + if (activity_ == activity) { + return; + } + + activity_ = activity; + switch (activity) { + case Katabasis::Activity::Idle: + case Katabasis::Activity::LoggingIn: + case Katabasis::Activity::LoggingOut: + case Katabasis::Activity::Refreshing: + // non-terminal states... + break; + case Katabasis::Activity::FailedSoft: + // terminal state, tokens did not change + break; + case Katabasis::Activity::FailedHard: + case Katabasis::Activity::FailedGone: + // terminal state, tokens are invalid + token_ = Token(); + break; + case Katabasis::Activity::Succeeded: + setLinked(true); + break; + } + emit activityChanged(activity_); + } + + QString DeviceFlow::token() + { + return token_.token; + } + void DeviceFlow::setToken(const QString& v) + { + token_.token = v; + } + + QVariantMap DeviceFlow::extraTokens() + { + return token_.extra; + } + + void DeviceFlow::setExtraTokens(QVariantMap extraTokens) + { + token_.extra = extraTokens; + } + + void DeviceFlow::setPollServer(PollServer* server) + { + if (pollServer_) + pollServer_->deleteLater(); + + pollServer_ = server; + } + + PollServer* DeviceFlow::pollServer() const + { + return pollServer_; + } + + QVariantMap DeviceFlow::extraRequestParams() + { + return extraReqParams_; + } + + void DeviceFlow::setExtraRequestParams(const QVariantMap& value) + { + extraReqParams_ = value; + } + + QString DeviceFlow::grantType() + { + if (!grantType_.isEmpty()) + return grantType_; + + return OAUTH2_GRANT_TYPE_DEVICE; + } + + void DeviceFlow::setGrantType(const QString& value) + { + grantType_ = value; + } + + // First get the URL and token to display to the user + void DeviceFlow::login() + { + qDebug() << "DeviceFlow::link"; + + updateActivity(Activity::LoggingIn); + setLinked(false); + setToken(""); + setExtraTokens(QVariantMap()); + setRefreshToken(QString()); + setExpires(QDateTime()); + + QList<RequestParameter> parameters; + parameters.append(RequestParameter(OAUTH2_CLIENT_ID, + options_.clientIdentifier.toUtf8())); + parameters.append( + RequestParameter(OAUTH2_SCOPE, options_.scope.toUtf8())); + QByteArray payload = createQueryParameters(parameters); + + QUrl url(options_.authorizationUrl); + QNetworkRequest deviceRequest(url); + deviceRequest.setHeader(QNetworkRequest::ContentTypeHeader, + "application/x-www-form-urlencoded"); + QNetworkReply* tokenReply = manager_->post(deviceRequest, payload); + + connect(tokenReply, &QNetworkReply::finished, this, + &DeviceFlow::onDeviceAuthReplyFinished, Qt::QueuedConnection); + } + + // Then, once we get them, present them to the user + void DeviceFlow::onDeviceAuthReplyFinished() + { + qDebug() << "DeviceFlow::onDeviceAuthReplyFinished"; + QNetworkReply* tokenReply = qobject_cast<QNetworkReply*>(sender()); + if (!tokenReply) { + qDebug() << "DeviceFlow::onDeviceAuthReplyFinished: reply is null"; + return; + } + if (tokenReply->error() == QNetworkReply::NoError) { + QByteArray replyData = tokenReply->readAll(); + + // Dump replyData + // SENSITIVE DATA in RelWithDebInfo or Debug builds + // qDebug() << "DeviceFlow::onDeviceAuthReplyFinished: replyData\n"; + // qDebug() << QString( replyData ); + + QVariantMap params = parseJsonResponse(replyData); + + // Dump tokens + qDebug() + << "DeviceFlow::onDeviceAuthReplyFinished: Tokens returned:\n"; + foreach (QString key, params.keys()) { + // SENSITIVE DATA in RelWithDebInfo or Debug builds, so it is + // truncated first + qDebug() << key << ": " << params.value(key).toString(); + } + + // Check for mandatory parameters + if (hasMandatoryDeviceAuthParams(params)) { + qDebug() << "DeviceFlow::onDeviceAuthReplyFinished: Device " + "auth request response"; + + const QString userCode = + params.take(OAUTH2_USER_CODE).toString(); + QUrl uri = params.take(OAUTH2_VERIFICATION_URI).toUrl(); + if (uri.isEmpty()) + uri = params.take(OAUTH2_VERIFICATION_URL).toUrl(); + + if (params.contains(OAUTH2_VERIFICATION_URI_COMPLETE)) + emit openBrowser( + params.take(OAUTH2_VERIFICATION_URI_COMPLETE).toUrl()); + + bool ok = false; + int expiresIn = params[OAUTH2_EXPIRES_IN].toInt(&ok); + if (!ok) { + qWarning() << "DeviceFlow::startPollServer: No expired_in " + "parameter"; + updateActivity(Activity::FailedHard); + return; + } + + emit showVerificationUriAndCode(uri, userCode, expiresIn); + + startPollServer(params, expiresIn); + } else { + qWarning() << "DeviceFlow::onDeviceAuthReplyFinished: " + "Mandatory parameters missing from response"; + updateActivity(Activity::FailedHard); + } + } + tokenReply->deleteLater(); + } + + // Spin up polling for the user completing the login flow out of band + void DeviceFlow::startPollServer(const QVariantMap& params, int expiresIn) + { + qDebug() + << "DeviceFlow::startPollServer: device_ and user_code expires in" + << expiresIn << "seconds"; + + QUrl url(options_.accessTokenUrl); + QNetworkRequest authRequest(url); + authRequest.setHeader(QNetworkRequest::ContentTypeHeader, + "application/x-www-form-urlencoded"); + + const QString deviceCode = params[OAUTH2_DEVICE_CODE].toString(); + const QString grantType = + grantType_.isEmpty() ? OAUTH2_GRANT_TYPE_DEVICE : grantType_; + + QList<RequestParameter> parameters; + parameters.append(RequestParameter(OAUTH2_CLIENT_ID, + options_.clientIdentifier.toUtf8())); + if (!options_.clientSecret.isEmpty()) { + parameters.append(RequestParameter(OAUTH2_CLIENT_SECRET, + options_.clientSecret.toUtf8())); + } + parameters.append(RequestParameter(OAUTH2_CODE, deviceCode.toUtf8())); + parameters.append( + RequestParameter(OAUTH2_GRANT_TYPE, grantType.toUtf8())); + QByteArray payload = createQueryParameters(parameters); + + PollServer* pollServer = + new PollServer(manager_, authRequest, payload, expiresIn, this); + if (params.contains(OAUTH2_INTERVAL)) { + bool ok = false; + int interval = params[OAUTH2_INTERVAL].toInt(&ok); + if (ok) { + pollServer->setInterval(interval); + } + } + connect(pollServer, &PollServer::verificationReceived, this, + &DeviceFlow::onVerificationReceived); + connect(pollServer, &PollServer::serverClosed, this, + &DeviceFlow::serverHasClosed); + setPollServer(pollServer); + pollServer->startPolling(); + } + + // Once the user completes the flow, update the internal state and report it + // to observers + void + DeviceFlow::onVerificationReceived(const QMap<QString, QString> response) + { + qDebug() + << "DeviceFlow::onVerificationReceived: Emitting closeBrowser()"; + emit closeBrowser(); + + if (response.contains("error")) { + qWarning() + << "DeviceFlow::onVerificationReceived: Verification failed:" + << response; + updateActivity(Activity::FailedHard); + return; + } + + // Check for mandatory tokens + if (response.contains(OAUTH2_ACCESS_TOKEN)) { + qDebug() << "DeviceFlow::onVerificationReceived: Access token " + "returned for implicit or device flow"; + setToken(response.value(OAUTH2_ACCESS_TOKEN)); + if (response.contains(OAUTH2_EXPIRES_IN)) { + bool ok = false; + int expiresIn = response.value(OAUTH2_EXPIRES_IN).toInt(&ok); + if (ok) { + qDebug() << "DeviceFlow::onVerificationReceived: Token " + "expires in" + << expiresIn << "seconds"; + setExpires( + QDateTime::currentDateTimeUtc().addSecs(expiresIn)); + } + } + if (response.contains(OAUTH2_REFRESH_TOKEN)) { + setRefreshToken(response.value(OAUTH2_REFRESH_TOKEN)); + } + updateActivity(Activity::Succeeded); + } else { + qWarning() << "DeviceFlow::onVerificationReceived: Access token " + "missing from response for implicit or device flow"; + updateActivity(Activity::FailedHard); + } + } + + // Or if the flow fails or the polling times out, update the internal state + // with error and report it to observers + void DeviceFlow::serverHasClosed(bool paramsfound) + { + if (!paramsfound) { + // server has probably timed out after receiving first response + updateActivity(Activity::FailedHard); + } + // poll server is not re-used for later auth requests + setPollServer(NULL); + } + + void DeviceFlow::logout() + { + qDebug() << "DeviceFlow::unlink"; + updateActivity(Activity::LoggingOut); + // FIXME: implement logout flows... if they exist + token_ = Token(); + updateActivity(Activity::FailedHard); + } + + QDateTime DeviceFlow::expires() + { + return token_.notAfter; + } + void DeviceFlow::setExpires(QDateTime v) + { + token_.notAfter = v; + } + + QString DeviceFlow::refreshToken() + { + return token_.refresh_token; + } + + void DeviceFlow::setRefreshToken(const QString& v) + { +#ifndef NDEBUG + qDebug() << "DeviceFlow::setRefreshToken" << v << "..."; +#endif + token_.refresh_token = v; + } + + namespace + { + QByteArray buildRequestBody(const QMap<QString, QString>& parameters) + { + QByteArray body; + bool first = true; + foreach (QString key, parameters.keys()) { + if (first) { + first = false; + } else { + body.append("&"); + } + QString value = parameters.value(key); + body.append(QUrl::toPercentEncoding(key) + + QString("=").toUtf8() + + QUrl::toPercentEncoding(value)); + } + return body; + } + } // namespace + + bool DeviceFlow::refresh() + { + qDebug() << "DeviceFlow::refresh: Token: ..." + << refreshToken().right(7); + + updateActivity(Activity::Refreshing); + + if (refreshToken().isEmpty()) { + qWarning() << "DeviceFlow::refresh: No refresh token"; + onRefreshError(QNetworkReply::AuthenticationRequiredError, nullptr); + return false; + } + if (options_.accessTokenUrl.isEmpty()) { + qWarning() << "DeviceFlow::refresh: Refresh token URL not set"; + onRefreshError(QNetworkReply::AuthenticationRequiredError, nullptr); + return false; + } + + QNetworkRequest refreshRequest(options_.accessTokenUrl); + refreshRequest.setHeader(QNetworkRequest::ContentTypeHeader, + MIME_TYPE_XFORM); + QMap<QString, QString> parameters; + parameters.insert(OAUTH2_CLIENT_ID, options_.clientIdentifier); + if (!options_.clientSecret.isEmpty()) { + parameters.insert(OAUTH2_CLIENT_SECRET, options_.clientSecret); + } + parameters.insert(OAUTH2_REFRESH_TOKEN, refreshToken()); + parameters.insert(OAUTH2_GRANT_TYPE, OAUTH2_REFRESH_TOKEN); + + QByteArray data = buildRequestBody(parameters); + QNetworkReply* refreshReply = manager_->post(refreshRequest, data); + timedReplies_.add(refreshReply); + connect(refreshReply, &QNetworkReply::finished, this, + &DeviceFlow::onRefreshFinished, Qt::QueuedConnection); + return true; + } + + void DeviceFlow::onRefreshFinished() + { + QNetworkReply* refreshReply = qobject_cast<QNetworkReply*>(sender()); + + auto networkError = refreshReply->error(); + if (networkError == QNetworkReply::NoError) { + QByteArray reply = refreshReply->readAll(); + QVariantMap tokens = parseJsonResponse(reply); + setToken(tokens.value(OAUTH2_ACCESS_TOKEN).toString()); + setExpires(QDateTime::currentDateTimeUtc().addSecs( + tokens.value(OAUTH2_EXPIRES_IN).toInt())); + QString refreshToken = + tokens.value(OAUTH2_REFRESH_TOKEN).toString(); + if (!refreshToken.isEmpty()) { + setRefreshToken(refreshToken); + } else { + qDebug() << "No new refresh token. Keep the old one."; + } + timedReplies_.remove(refreshReply); + refreshReply->deleteLater(); + updateActivity(Activity::Succeeded); + qDebug() << "New token expires in" << expires() << "seconds"; + } else { + // FIXME: differentiate the error more here + onRefreshError(networkError, refreshReply); + } + } + + void DeviceFlow::onRefreshError(QNetworkReply::NetworkError error, + QNetworkReply* refreshReply) + { + QString errorString = "No Reply"; + if (refreshReply) { + timedReplies_.remove(refreshReply); + errorString = refreshReply->errorString(); + } + + switch (error) { + // used for invalid credentials and similar errors. Fall through. + case QNetworkReply::AuthenticationRequiredError: + case QNetworkReply::ContentAccessDenied: + case QNetworkReply::ContentOperationNotPermittedError: + case QNetworkReply::ProtocolInvalidOperationError: + updateActivity(Activity::FailedHard); + break; + case QNetworkReply::ContentGoneError: { + updateActivity(Activity::FailedGone); + break; + } + case QNetworkReply::TimeoutError: + case QNetworkReply::OperationCanceledError: + case QNetworkReply::SslHandshakeFailedError: + default: + updateActivity(Activity::FailedSoft); + return; + } + if (refreshReply) { + refreshReply->deleteLater(); + } + qDebug() << "DeviceFlow::onRefreshFinished: Error" << (int)error + << " - " << errorString; + } + +} // namespace Katabasis diff --git a/meshmc/libraries/katabasis/src/JsonResponse.cpp b/meshmc/libraries/katabasis/src/JsonResponse.cpp new file mode 100644 index 0000000000..8daee82ebb --- /dev/null +++ b/meshmc/libraries/katabasis/src/JsonResponse.cpp @@ -0,0 +1,51 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * 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, either version 3 of the License, or + * (at your option) any later version. + * + * 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 "JsonResponse.h" + +#include <QByteArray> +#include <QDebug> +#include <QJsonDocument> +#include <QJsonObject> + +namespace Katabasis +{ + + QVariantMap parseJsonResponse(const QByteArray& data) + { + QJsonParseError err; + QJsonDocument doc = QJsonDocument::fromJson(data, &err); + if (err.error != QJsonParseError::NoError) { + qWarning() << "parseTokenResponse: Failed to parse token response " + "due to err:" + << err.errorString(); + return QVariantMap(); + } + + if (!doc.isObject()) { + qWarning() << "parseTokenResponse: Token response is not an object"; + return QVariantMap(); + } + + return doc.object().toVariantMap(); + } + +} // namespace Katabasis diff --git a/meshmc/libraries/katabasis/src/JsonResponse.h b/meshmc/libraries/katabasis/src/JsonResponse.h new file mode 100644 index 0000000000..0662c8ff61 --- /dev/null +++ b/meshmc/libraries/katabasis/src/JsonResponse.h @@ -0,0 +1,34 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * 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, either version 3 of the License, or + * (at your option) any later version. + * + * 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 <QVariantMap> + +class QByteArray; + +namespace Katabasis +{ + + /// Parse JSON data into a QVariantMap + QVariantMap parseJsonResponse(const QByteArray& data); + +} // namespace Katabasis diff --git a/meshmc/libraries/katabasis/src/PollServer.cpp b/meshmc/libraries/katabasis/src/PollServer.cpp new file mode 100644 index 0000000000..1c8556aa98 --- /dev/null +++ b/meshmc/libraries/katabasis/src/PollServer.cpp @@ -0,0 +1,147 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * 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, either version 3 of the License, or + * (at your option) any later version. + * + * 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 <QNetworkAccessManager> +#include <QNetworkReply> + +#include "katabasis/PollServer.h" +#include "JsonResponse.h" + +namespace +{ + QMap<QString, QString> toVerificationParams(const QVariantMap& map) + { + QMap<QString, QString> params; + for (QVariantMap::const_iterator i = map.constBegin(); + i != map.constEnd(); ++i) { + params[i.key()] = i.value().toString(); + } + return params; + } +} // namespace + +namespace Katabasis +{ + + PollServer::PollServer(QNetworkAccessManager* manager, + const QNetworkRequest& request, + const QByteArray& payload, int expiresIn, + QObject* parent) + : QObject(parent), manager_(manager), request_(request), + payload_(payload), expiresIn_(expiresIn) + { + expirationTimer.setTimerType(Qt::VeryCoarseTimer); + expirationTimer.setInterval(expiresIn * 1000); + expirationTimer.setSingleShot(true); + connect(&expirationTimer, SIGNAL(timeout()), this, + SLOT(onExpiration())); + expirationTimer.start(); + + pollTimer.setTimerType(Qt::VeryCoarseTimer); + pollTimer.setInterval(5 * 1000); + pollTimer.setSingleShot(true); + connect(&pollTimer, SIGNAL(timeout()), this, SLOT(onPollTimeout())); + } + + int PollServer::interval() const + { + return pollTimer.interval() / 1000; + } + + void PollServer::setInterval(int interval) + { + pollTimer.setInterval(interval * 1000); + } + + void PollServer::startPolling() + { + if (expirationTimer.isActive()) { + pollTimer.start(); + } + } + + void PollServer::onPollTimeout() + { + qDebug() << "PollServer::onPollTimeout: retrying"; + QNetworkReply* reply = manager_->post(request_, payload_); + connect(reply, SIGNAL(finished()), this, SLOT(onReplyFinished())); + } + + void PollServer::onExpiration() + { + pollTimer.stop(); + emit serverClosed(false); + } + + void PollServer::onReplyFinished() + { + QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender()); + + if (!reply) { + qDebug() << "PollServer::onReplyFinished: reply is null"; + return; + } + + QByteArray replyData = reply->readAll(); + QMap<QString, QString> params = + toVerificationParams(parseJsonResponse(replyData)); + + // Dump replyData + // SENSITIVE DATA in RelWithDebInfo or Debug builds + // qDebug() << "PollServer::onReplyFinished: replyData\n"; + // qDebug() << QString( replyData ); + + if (reply->error() == QNetworkReply::TimeoutError) { + // rfc8628#section-3.2 + // "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." + setInterval(interval() * 2); + pollTimer.start(); + } else { + QString error = params.value("error"); + if (error == "slow_down") { + // rfc8628#section-3.2 + // "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." + setInterval(interval() + 5); + pollTimer.start(); + } else if (error == "authorization_pending") { + // keep trying - rfc8628#section-3.2 + // "The authorization request is still pending as the end user + // hasn't yet completed the user-interaction steps + // (Section 3.3)." + pollTimer.start(); + } else { + expirationTimer.stop(); + emit serverClosed(true); + // let O2 handle the other cases + emit verificationReceived(params); + } + } + reply->deleteLater(); + } + +} // namespace Katabasis diff --git a/meshmc/libraries/katabasis/src/Reply.cpp b/meshmc/libraries/katabasis/src/Reply.cpp new file mode 100644 index 0000000000..c19113ac50 --- /dev/null +++ b/meshmc/libraries/katabasis/src/Reply.cpp @@ -0,0 +1,96 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * 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, either version 3 of the License, or + * (at your option) any later version. + * + * 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 <QTimer> +#include <QNetworkReply> + +#include "katabasis/Reply.h" + +namespace Katabasis +{ + + Reply::Reply(QNetworkReply* r, int timeOut, QObject* parent) + : QTimer(parent), reply(r) + { + setSingleShot(true); + connect(this, &Reply::timeout, this, &Reply::onTimeOut, + Qt::QueuedConnection); + start(timeOut); + } + + void Reply::onTimeOut() + { + timedOut = true; + reply->abort(); + } + + // ---------------------------- + + ReplyList::~ReplyList() + { + foreach (Reply* timedReply, replies_) { + delete timedReply; + } + } + + void ReplyList::add(QNetworkReply* reply, int timeOut) + { + if (reply && ignoreSslErrors()) { + reply->ignoreSslErrors(); + } + add(new Reply(reply, timeOut)); + } + + void ReplyList::add(Reply* reply) + { + replies_.append(reply); + } + + void ReplyList::remove(QNetworkReply* reply) + { + Reply* o2Reply = find(reply); + if (o2Reply) { + o2Reply->stop(); + (void)replies_.removeOne(o2Reply); + } + } + + Reply* ReplyList::find(QNetworkReply* reply) + { + foreach (Reply* timedReply, replies_) { + if (timedReply->reply == reply) { + return timedReply; + } + } + return 0; + } + + bool ReplyList::ignoreSslErrors() + { + return ignoreSslErrors_; + } + + void ReplyList::setIgnoreSslErrors(bool ignoreSslErrors) + { + ignoreSslErrors_ = ignoreSslErrors; + } + +} // namespace Katabasis |
