aboutsummaryrefslogtreecommitdiff
path: root/launcher/minecraft/auth/Parsers.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'launcher/minecraft/auth/Parsers.cpp')
-rw-r--r--launcher/minecraft/auth/Parsers.cpp175
1 files changed, 175 insertions, 0 deletions
diff --git a/launcher/minecraft/auth/Parsers.cpp b/launcher/minecraft/auth/Parsers.cpp
index 2dd36562..47473899 100644
--- a/launcher/minecraft/auth/Parsers.cpp
+++ b/launcher/minecraft/auth/Parsers.cpp
@@ -1,4 +1,5 @@
#include "Parsers.h"
+#include "Json.h"
#include <QJsonDocument>
#include <QJsonArray>
@@ -212,6 +213,180 @@ bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) {
return true;
}
+namespace {
+ // these skin URLs are for the MHF_Steve and MHF_Alex accounts (made by a Mojang employee)
+ // they are needed because the session server doesn't return skin urls for default skins
+ static const QString SKIN_URL_STEVE = "http://textures.minecraft.net/texture/1a4af718455d4aab528e7a61f86fa25e6a369d1768dcb13f7df319a713eb810b";
+ static const QString SKIN_URL_ALEX = "http://textures.minecraft.net/texture/83cee5ca6afcdb171285aa00e8049c297b2dbeba0efb8ff970a5677a1b644032";
+
+ bool isDefaultModelSteve(QString uuid) {
+ // need to calculate *Java* hashCode of UUID
+ // if number is even, skin/model is steve, otherwise it is alex
+
+ // just in case dashes are in the id
+ uuid.remove('-');
+
+ if (uuid.size() != 32) {
+ return true;
+ }
+
+ // qulonglong is guaranteed to be 64 bits
+ // we need to use unsigned numbers to guarantee truncation below
+ qulonglong most = uuid.left(16).toULongLong(nullptr, 16);
+ qulonglong least = uuid.right(16).toULongLong(nullptr, 16);
+ qulonglong xored = most ^ least;
+ return ((static_cast<quint32>(xored >> 32)) ^ static_cast<quint32>(xored)) % 2 == 0;
+ }
+}
+
+/**
+Uses session server for skin/cape lookup instead of profile,
+because locked Mojang accounts cannot access profile endpoint
+(https://api.minecraftservices.com/minecraft/profile/)
+
+ref: https://wiki.vg/Mojang_API#UUID_to_Profile_and_Skin.2FCape
+
+{
+ "id": "<profile identifier>",
+ "name": "<player name>",
+ "properties": [
+ {
+ "name": "textures",
+ "value": "<base64 string>"
+ }
+ ]
+}
+
+decoded base64 "value":
+{
+ "timestamp": <java time in ms>,
+ "profileId": "<profile uuid>",
+ "profileName": "<player name>",
+ "textures": {
+ "SKIN": {
+ "url": "<player skin URL>"
+ },
+ "CAPE": {
+ "url": "<player cape URL>"
+ }
+ }
+}
+*/
+
+bool parseMinecraftProfileMojang(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 as JSON: " << jsonError.errorString();
+ return false;
+ }
+
+ auto obj = Json::requireObject(doc, "mojang minecraft profile");
+ 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 propsArray = obj.value("properties").toArray();
+ QByteArray texturePayload;
+ for( auto p : propsArray) {
+ auto pObj = p.toObject();
+ auto name = pObj.value("name");
+ if (!name.isString() || name.toString() != "textures") {
+ continue;
+ }
+
+ auto value = pObj.value("value");
+ if (value.isString()) {
+#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
+ texturePayload = QByteArray::fromBase64(value.toString().toUtf8(), QByteArray::AbortOnBase64DecodingErrors);
+#else
+ texturePayload = QByteArray::fromBase64(value.toString().toUtf8());
+#endif
+ }
+
+ if (!texturePayload.isEmpty()) {
+ break;
+ }
+ }
+
+ if (texturePayload.isNull()) {
+ qWarning() << "No texture payload data";
+ return false;
+ }
+
+ doc = QJsonDocument::fromJson(texturePayload, &jsonError);
+ if(jsonError.error) {
+ qWarning() << "Failed to parse response as JSON: " << jsonError.errorString();
+ return false;
+ }
+
+ obj = Json::requireObject(doc, "session texture payload");
+ auto textures = obj.value("textures");
+ if (!textures.isObject()) {
+ qWarning() << "No textures array in response";
+ return false;
+ }
+
+ Skin skinOut;
+ // fill in default skin info ourselves, as this endpoint doesn't provide it
+ bool steve = isDefaultModelSteve(output.id);
+ skinOut.variant = steve ? "classic" : "slim";
+ skinOut.url = steve ? SKIN_URL_STEVE : SKIN_URL_ALEX;
+ // sadly we can't figure this out, but I don't think it really matters...
+ skinOut.id = "00000000-0000-0000-0000-000000000000";
+ Cape capeOut;
+ auto tObj = textures.toObject();
+ for (auto idx = tObj.constBegin(); idx != tObj.constEnd(); ++idx) {
+ if (idx->isObject()) {
+ if (idx.key() == "SKIN") {
+ auto skin = idx->toObject();
+ if (!getString(skin.value("url"), skinOut.url)) {
+ qWarning() << "Skin url is not a string";
+ return false;
+ }
+
+ auto maybeMeta = skin.find("metadata");
+ if (maybeMeta != skin.end() && maybeMeta->isObject()) {
+ auto meta = maybeMeta->toObject();
+ // might not be present
+ getString(meta.value("model"), skinOut.variant);
+ }
+ }
+ else if (idx.key() == "CAPE") {
+ auto cape = idx->toObject();
+ if (!getString(cape.value("url"), capeOut.url)) {
+ qWarning() << "Cape url is not a string";
+ return false;
+ }
+
+ // we don't know the cape ID as it is not returned from the session server
+ // so just fake it - changing capes is probably locked anyway :(
+ capeOut.alias = "cape";
+ }
+ }
+ }
+
+ output.skin = skinOut;
+ if (capeOut.alias == "cape") {
+ output.capes = QMap<QString, Cape>({{capeOut.alias, capeOut}});
+ output.currentCape = capeOut.alias;
+ }
+
+ output.validity = Katabasis::Validity::Certain;
+ return true;
+}
+
bool parseMinecraftEntitlements(QByteArray & data, MinecraftEntitlement &output) {
qDebug() << "Parsing Minecraft entitlements...";
#ifndef NDEBUG