#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 parseForcedMigrationResponse(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/msamigrationforced 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 != "msamigrationforced") { qWarning() << "Rollout feature is not what we expected (msamigrationforced), but is instead \"" << feature << "\""; return false; } if(!getBool(obj.value("rollout"), result)) { qWarning() << "Rollout feature is not a string"; return false; } 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; } }