/* 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 . */ #include "Parsers.h" #include #include #include 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