summaryrefslogtreecommitdiff
path: root/meshmc/libraries/katabasis
diff options
context:
space:
mode:
authorMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:45:07 +0300
committerMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:45:07 +0300
commit31b9a8949ed0a288143e23bf739f2eb64fdc63be (patch)
tree8a984fa143c38fccad461a77792d6864f3e82cd3 /meshmc/libraries/katabasis
parent934382c8a1ce738589dee9ee0f14e1cec812770e (diff)
parentfad6a1066616b69d7f5fef01178efdf014c59537 (diff)
downloadProject-Tick-31b9a8949ed0a288143e23bf739f2eb64fdc63be.tar.gz
Project-Tick-31b9a8949ed0a288143e23bf739f2eb64fdc63be.zip
Add 'meshmc/' from commit 'fad6a1066616b69d7f5fef01178efdf014c59537'
git-subtree-dir: meshmc git-subtree-mainline: 934382c8a1ce738589dee9ee0f14e1cec812770e git-subtree-split: fad6a1066616b69d7f5fef01178efdf014c59537
Diffstat (limited to 'meshmc/libraries/katabasis')
-rw-r--r--meshmc/libraries/katabasis/.gitignore2
-rw-r--r--meshmc/libraries/katabasis/CMakeLists.txt53
-rw-r--r--meshmc/libraries/katabasis/LICENSE23
-rw-r--r--meshmc/libraries/katabasis/README.md36
-rw-r--r--meshmc/libraries/katabasis/acknowledgements.md110
-rw-r--r--meshmc/libraries/katabasis/include/katabasis/Bits.h57
-rw-r--r--meshmc/libraries/katabasis/include/katabasis/DeviceFlow.h178
-rw-r--r--meshmc/libraries/katabasis/include/katabasis/Globals.h82
-rw-r--r--meshmc/libraries/katabasis/include/katabasis/PollServer.h73
-rw-r--r--meshmc/libraries/katabasis/include/katabasis/Reply.h92
-rw-r--r--meshmc/libraries/katabasis/include/katabasis/RequestParameter.h42
-rw-r--r--meshmc/libraries/katabasis/src/DeviceFlow.cpp539
-rw-r--r--meshmc/libraries/katabasis/src/JsonResponse.cpp51
-rw-r--r--meshmc/libraries/katabasis/src/JsonResponse.h34
-rw-r--r--meshmc/libraries/katabasis/src/PollServer.cpp147
-rw-r--r--meshmc/libraries/katabasis/src/Reply.cpp96
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