summaryrefslogtreecommitdiff
path: root/meshmc/launcher/minecraft/auth/Parsers.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'meshmc/launcher/minecraft/auth/Parsers.cpp')
-rw-r--r--meshmc/launcher/minecraft/auth/Parsers.cpp366
1 files changed, 366 insertions, 0 deletions
diff --git a/meshmc/launcher/minecraft/auth/Parsers.cpp b/meshmc/launcher/minecraft/auth/Parsers.cpp
new file mode 100644
index 0000000000..6a4690942c
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/Parsers.cpp
@@ -0,0 +1,366 @@
+/* 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 "Parsers.h"
+
+#include <QJsonDocument>
+#include <QJsonArray>
+#include <QDebug>
+
+namespace Parsers
+{
+
+ bool getDateTime(QJsonValue value, QDateTime& out)
+ {
+ if (!value.isString()) {
+ return false;
+ }
+ out = QDateTime::fromString(value.toString(), Qt::ISODate);
+ return out.isValid();
+ }
+
+ bool getString(QJsonValue value, QString& out)
+ {
+ if (!value.isString()) {
+ return false;
+ }
+ out = value.toString();
+ return true;
+ }
+
+ bool getNumber(QJsonValue value, double& out)
+ {
+ if (!value.isDouble()) {
+ return false;
+ }
+ out = value.toDouble();
+ return true;
+ }
+
+ bool getNumber(QJsonValue value, int64_t& out)
+ {
+ if (!value.isDouble()) {
+ return false;
+ }
+ out = (int64_t)value.toDouble();
+ return true;
+ }
+
+ bool getBool(QJsonValue value, bool& out)
+ {
+ if (!value.isBool()) {
+ return false;
+ }
+ out = value.toBool();
+ return true;
+ }
+
+ /*
+ {
+ "IssueInstant":"2020-12-07T19:52:08.4463796Z",
+ "NotAfter":"2020-12-21T19:52:08.4463796Z",
+ "Token":"token",
+ "DisplayClaims":{
+ "xui":[
+ {
+ "uhs":"userhash"
+ }
+ ]
+ }
+ }
+ */
+ // TODO: handle error responses ...
+ /*
+ {
+ "Identity":"0",
+ "XErr":2148916238,
+ "Message":"",
+ "Redirect":"https://start.ui.xboxlive.com/AddChildToFamily"
+ }
+ // 2148916233 = missing XBox account
+ // 2148916238 = child account not linked to a family
+ */
+
+ bool parseXTokenResponse(QByteArray& data, Katabasis::Token& output,
+ QString name)
+ {
+ qDebug() << "Parsing" << name << ":";
+#ifndef NDEBUG
+ qDebug() << data;
+#endif
+ QJsonParseError jsonError;
+ QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
+ if (jsonError.error) {
+ qWarning() << "Failed to parse response from "
+ "user.auth.xboxlive.com as JSON: "
+ << jsonError.errorString();
+ return false;
+ }
+
+ auto obj = doc.object();
+ if (!getDateTime(obj.value("IssueInstant"), output.issueInstant)) {
+ qWarning() << "User IssueInstant is not a timestamp";
+ return false;
+ }
+ if (!getDateTime(obj.value("NotAfter"), output.notAfter)) {
+ qWarning() << "User NotAfter is not a timestamp";
+ return false;
+ }
+ if (!getString(obj.value("Token"), output.token)) {
+ qWarning() << "User Token is not a string";
+ return false;
+ }
+ auto arrayVal = obj.value("DisplayClaims").toObject().value("xui");
+ if (!arrayVal.isArray()) {
+ qWarning() << "Missing xui claims array";
+ return false;
+ }
+ bool foundUHS = false;
+ for (auto item : arrayVal.toArray()) {
+ if (!item.isObject()) {
+ continue;
+ }
+ auto obj = item.toObject();
+ if (obj.contains("uhs")) {
+ foundUHS = true;
+ } else {
+ continue;
+ }
+ // consume all 'display claims' ... whatever that means
+ for (auto iter = obj.begin(); iter != obj.end(); iter++) {
+ QString claim;
+ if (!getString(obj.value(iter.key()), claim)) {
+ qWarning() << "display claim " << iter.key()
+ << " is not a string...";
+ return false;
+ }
+ output.extra[iter.key()] = claim;
+ }
+
+ break;
+ }
+ if (!foundUHS) {
+ qWarning() << "Missing uhs";
+ return false;
+ }
+ output.validity = Katabasis::Validity::Certain;
+ qDebug() << name << "is valid.";
+ return true;
+ }
+
+ bool parseMinecraftProfile(QByteArray& data, MinecraftProfile& output)
+ {
+ qDebug() << "Parsing Minecraft profile...";
+#ifndef NDEBUG
+ qDebug() << data;
+#endif
+
+ QJsonParseError jsonError;
+ QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
+ if (jsonError.error) {
+ qWarning() << "Failed to parse response from "
+ "user.auth.xboxlive.com as JSON: "
+ << jsonError.errorString();
+ return false;
+ }
+
+ auto obj = doc.object();
+ if (!getString(obj.value("id"), output.id)) {
+ qWarning() << "Minecraft profile id is not a string";
+ return false;
+ }
+
+ if (!getString(obj.value("name"), output.name)) {
+ qWarning() << "Minecraft profile name is not a string";
+ return false;
+ }
+
+ auto skinsArray = obj.value("skins").toArray();
+ for (auto skin : skinsArray) {
+ auto skinObj = skin.toObject();
+ Skin skinOut;
+ if (!getString(skinObj.value("id"), skinOut.id)) {
+ continue;
+ }
+ QString state;
+ if (!getString(skinObj.value("state"), state)) {
+ continue;
+ }
+ if (state != "ACTIVE") {
+ continue;
+ }
+ if (!getString(skinObj.value("url"), skinOut.url)) {
+ continue;
+ }
+ if (!getString(skinObj.value("variant"), skinOut.variant)) {
+ continue;
+ }
+ // we deal with only the active skin
+ output.skin = skinOut;
+ break;
+ }
+ auto capesArray = obj.value("capes").toArray();
+
+ QString currentCape;
+ for (auto cape : capesArray) {
+ auto capeObj = cape.toObject();
+ Cape capeOut;
+ if (!getString(capeObj.value("id"), capeOut.id)) {
+ continue;
+ }
+ QString state;
+ if (!getString(capeObj.value("state"), state)) {
+ continue;
+ }
+ if (state == "ACTIVE") {
+ currentCape = capeOut.id;
+ }
+ if (!getString(capeObj.value("url"), capeOut.url)) {
+ continue;
+ }
+ if (!getString(capeObj.value("alias"), capeOut.alias)) {
+ continue;
+ }
+
+ output.capes[capeOut.id] = capeOut;
+ }
+ output.currentCape = currentCape;
+ output.validity = Katabasis::Validity::Certain;
+ return true;
+ }
+
+ bool parseMinecraftEntitlements(QByteArray& data,
+ MinecraftEntitlement& output)
+ {
+ qDebug() << "Parsing Minecraft entitlements...";
+#ifndef NDEBUG
+ qDebug() << data;
+#endif
+
+ QJsonParseError jsonError;
+ QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
+ if (jsonError.error) {
+ qWarning() << "Failed to parse response from "
+ "user.auth.xboxlive.com as JSON: "
+ << jsonError.errorString();
+ return false;
+ }
+
+ auto obj = doc.object();
+ output.canPlayMinecraft = false;
+ output.ownsMinecraft = false;
+
+ auto itemsArray = obj.value("items").toArray();
+ for (auto item : itemsArray) {
+ auto itemObj = item.toObject();
+ QString name;
+ if (!getString(itemObj.value("name"), name)) {
+ continue;
+ }
+ if (name == "game_minecraft") {
+ output.canPlayMinecraft = true;
+ }
+ if (name == "product_minecraft") {
+ output.ownsMinecraft = true;
+ }
+ }
+ output.validity = Katabasis::Validity::Certain;
+ return true;
+ }
+
+ bool parseRolloutResponse(QByteArray& data, bool& result)
+ {
+ qDebug() << "Parsing Rollout response...";
+#ifndef NDEBUG
+ qDebug() << data;
+#endif
+
+ QJsonParseError jsonError;
+ QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
+ if (jsonError.error) {
+ qWarning() << "Failed to parse response from "
+ "https://api.minecraftservices.com/rollout/v1/"
+ "msamigration as JSON: "
+ << jsonError.errorString();
+ return false;
+ }
+
+ auto obj = doc.object();
+ QString feature;
+ if (!getString(obj.value("feature"), feature)) {
+ qWarning() << "Rollout feature is not a string";
+ return false;
+ }
+ if (feature != "msamigration") {
+ qWarning() << "Rollout feature is not what we expected "
+ "(msamigration), but is instead \""
+ << feature << "\"";
+ return false;
+ }
+ if (!getBool(obj.value("rollout"), result)) {
+ qWarning() << "Rollout feature is not a string";
+ return false;
+ }
+ return true;
+ }
+
+ bool parseMojangResponse(QByteArray& data, Katabasis::Token& output)
+ {
+ QJsonParseError jsonError;
+ qDebug() << "Parsing Mojang response...";
+#ifndef NDEBUG
+ qDebug() << data;
+#endif
+ QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
+ if (jsonError.error) {
+ qWarning() << "Failed to parse response from "
+ "api.minecraftservices.com/launcher/login as JSON: "
+ << jsonError.errorString();
+ return false;
+ }
+
+ auto obj = doc.object();
+ double expires_in = 0;
+ if (!getNumber(obj.value("expires_in"), expires_in)) {
+ qWarning() << "expires_in is not a valid number";
+ return false;
+ }
+ auto currentTime = QDateTime::currentDateTimeUtc();
+ output.issueInstant = currentTime;
+ output.notAfter = currentTime.addSecs(expires_in);
+
+ QString username;
+ if (!getString(obj.value("username"), username)) {
+ qWarning() << "username is not valid";
+ return false;
+ }
+
+ // TODO: it's a JWT... validate it?
+ if (!getString(obj.value("access_token"), output.token)) {
+ qWarning() << "access_token is not valid";
+ return false;
+ }
+ output.validity = Katabasis::Validity::Certain;
+ qDebug() << "Mojang response is valid.";
+ return true;
+ }
+
+} // namespace Parsers