[FEATURE][EXPERIMENTAL] Support slim skins

This commit is contained in:
Gravita 2021-07-27 04:25:04 +07:00
parent b0538abe63
commit 4dd30faf7e
8 changed files with 139 additions and 20 deletions

View file

@ -0,0 +1,44 @@
package pro.gravit.launchserver.auth.texture;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import pro.gravit.launcher.HTTPRequest;
import pro.gravit.launcher.Launcher;
import pro.gravit.launcher.profiles.Texture;
import java.io.IOException;
import java.net.URL;
import java.util.UUID;
public class JsonTextureProvider extends TextureProvider {
public String url;
private transient final Logger logger = LogManager.getLogger();
@Override
public void close() throws IOException {
//None
}
@Override
public Texture getCloakTexture(UUID uuid, String username, String client) throws IOException {
logger.warn("Ineffective get cloak texture for {}", username);
return getTextures(uuid, username, client).cloak;
}
@Override
public Texture getSkinTexture(UUID uuid, String username, String client) throws IOException {
logger.warn("Ineffective get skin texture for {}", username);
return getTextures(uuid, username, client).skin;
}
@Override
public SkinAndCloakTextures getTextures(UUID uuid, String username, String client) {
try {
var result = HTTPRequest.jsonRequest(null, "GET", new URL(RequestTextureProvider.getTextureURL(url, uuid, username, client)));
return Launcher.gsonManager.gson.fromJson(result, SkinAndCloakTextures.class);
} catch (IOException e) {
logger.error("JsonTextureProvider", e);
return new SkinAndCloakTextures(null, null);
}
}
}

View file

@ -43,7 +43,7 @@ private static Texture getTexture(String url, Path local, boolean cloak) throws
} }
} }
private static String getTextureURL(String url, UUID uuid, String username, String client) { public static String getTextureURL(String url, UUID uuid, String username, String client) {
return CommonHelper.replace(url, "username", IOHelper.urlEncode(username), return CommonHelper.replace(url, "username", IOHelper.urlEncode(username),
"uuid", IOHelper.urlEncode(uuid.toString()), "hash", IOHelper.urlEncode(Launcher.toHash(uuid)), "uuid", IOHelper.urlEncode(uuid.toString()), "hash", IOHelper.urlEncode(Launcher.toHash(uuid)),
"client", IOHelper.urlEncode(client == null ? "unknown" : client)); "client", IOHelper.urlEncode(client == null ? "unknown" : client));

View file

@ -17,6 +17,7 @@ public static void registerProviders() {
// Auth providers that doesn't do nothing :D // Auth providers that doesn't do nothing :D
providers.register("request", RequestTextureProvider.class); providers.register("request", RequestTextureProvider.class);
providers.register("json", JsonTextureProvider.class);
registredProv = true; registredProv = true;
} }
} }
@ -29,4 +30,34 @@ public static void registerProviders() {
public abstract Texture getSkinTexture(UUID uuid, String username, String client) throws IOException; public abstract Texture getSkinTexture(UUID uuid, String username, String client) throws IOException;
public static class SkinAndCloakTextures {
public final Texture skin;
public final Texture cloak;
public SkinAndCloakTextures(Texture skin, Texture cloak) {
this.skin = skin;
this.cloak = cloak;
}
}
public SkinAndCloakTextures getTextures(UUID uuid, String username, String client) {
Texture skin;
try {
skin = getSkinTexture(uuid, username, client);
} catch (IOException e) {
skin = null;
}
// Get cloak texture
Texture cloak;
try {
cloak = getCloakTexture(uuid, username, client);
} catch (IOException e) {
cloak = null;
}
return new SkinAndCloakTextures(skin, cloak);
}
} }

View file

@ -6,7 +6,6 @@
import pro.gravit.launcher.events.request.AuthRequestEvent; import pro.gravit.launcher.events.request.AuthRequestEvent;
import pro.gravit.launcher.profiles.ClientProfile; import pro.gravit.launcher.profiles.ClientProfile;
import pro.gravit.launcher.profiles.PlayerProfile; import pro.gravit.launcher.profiles.PlayerProfile;
import pro.gravit.launcher.profiles.Texture;
import pro.gravit.launcher.request.auth.AuthRequest; import pro.gravit.launcher.request.auth.AuthRequest;
import pro.gravit.launcher.request.auth.password.*; import pro.gravit.launcher.request.auth.password.*;
import pro.gravit.launchserver.LaunchServer; import pro.gravit.launchserver.LaunchServer;
@ -309,23 +308,10 @@ public PlayerProfile getPlayerProfile(AuthProviderPair pair, User user) {
private PlayerProfile getPlayerProfile(UUID uuid, String username, String client, TextureProvider textureProvider) { private PlayerProfile getPlayerProfile(UUID uuid, String username, String client, TextureProvider textureProvider) {
// Get skin texture // Get skin texture
Texture skin; TextureProvider.SkinAndCloakTextures textures = textureProvider.getTextures(uuid, username, client);
try {
skin = textureProvider.getSkinTexture(uuid, username, client);
} catch (IOException e) {
skin = null;
}
// Get cloak texture
Texture cloak;
try {
cloak = textureProvider.getCloakTexture(uuid, username, client);
} catch (IOException e) {
cloak = null;
}
// Return combined profile // Return combined profile
return new PlayerProfile(uuid, username, skin, cloak); return new PlayerProfile(uuid, username, textures.skin, textures.cloak);
} }
public AuthRequest.AuthPasswordInterface decryptPassword(AuthRequest.AuthPasswordInterface password) throws AuthException { public AuthRequest.AuthPasswordInterface decryptPassword(AuthRequest.AuthPasswordInterface password) throws AuthException {

View file

@ -25,9 +25,15 @@ public final class Launcher {
public static final String SKIN_DIGEST_PROPERTY = "skinDigest"; public static final String SKIN_DIGEST_PROPERTY = "skinDigest";
public static final String SKIN_METADATA_PROPERTY = "skinMetadata";
public static final String CLOAK_URL_PROPERTY = "cloakURL"; public static final String CLOAK_URL_PROPERTY = "cloakURL";
public static final String CLOAK_DIGEST_PROPERTY = "cloakDigest"; public static final String CLOAK_DIGEST_PROPERTY = "cloakDigest";
public static final String CLOAK_METADATA_PROPERTY = "cloakMetadata";
// Used to determine from clientside is launched from launcher // Used to determine from clientside is launched from launcher
public static final AtomicBoolean LAUNCHED = new AtomicBoolean(false); public static final AtomicBoolean LAUNCHED = new AtomicBoolean(false);
public static final int PROTOCOL_MAGIC_LEGACY = 0x724724_00 + 24; public static final int PROTOCOL_MAGIC_LEGACY = 0x724724_00 + 24;

View file

@ -11,6 +11,7 @@
import java.io.InputStream; import java.io.InputStream;
import java.net.URL; import java.net.URL;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
public final class Texture extends StreamObject { public final class Texture extends StreamObject {
@ -22,11 +23,14 @@ public final class Texture extends StreamObject {
public final byte[] digest; public final byte[] digest;
public final Map<String, String> metadata;
@Deprecated @Deprecated
public Texture(HInput input) throws IOException { public Texture(HInput input) throws IOException {
url = IOHelper.verifyURL(input.readASCII(2048)); url = IOHelper.verifyURL(input.readASCII(2048));
digest = input.readByteArray(-DIGEST_ALGO.bytes); digest = input.readByteArray(-DIGEST_ALGO.bytes);
metadata = null;
} }
public Texture(String url, boolean cloak) throws IOException { public Texture(String url, boolean cloak) throws IOException {
@ -43,6 +47,7 @@ public Texture(String url, boolean cloak) throws IOException {
// Get digest of texture // Get digest of texture
digest = SecurityHelper.digest(DIGEST_ALGO, new URL(url)); digest = SecurityHelper.digest(DIGEST_ALGO, new URL(url));
metadata = null; // May be auto-detect?
} }
public Texture(String url, Path local, boolean cloak) throws IOException { public Texture(String url, Path local, boolean cloak) throws IOException {
@ -51,12 +56,20 @@ public Texture(String url, Path local, boolean cloak) throws IOException {
IOHelper.readTexture(input, cloak); // Verify texture IOHelper.readTexture(input, cloak); // Verify texture
} }
this.digest = Objects.requireNonNull(SecurityHelper.digest(DIGEST_ALGO, local), "digest"); this.digest = Objects.requireNonNull(SecurityHelper.digest(DIGEST_ALGO, local), "digest");
this.metadata = null;
} }
public Texture(String url, byte[] digest) { public Texture(String url, byte[] digest) {
this.url = IOHelper.verifyURL(url); this.url = IOHelper.verifyURL(url);
this.digest = Objects.requireNonNull(digest, "digest"); this.digest = Objects.requireNonNull(digest, "digest");
this.metadata = null;
}
public Texture(String url, byte[] digest, Map<String, String> metadata) {
this.url = url;
this.digest = digest;
this.metadata = metadata;
} }
@Override @Override

View file

@ -2,6 +2,7 @@
import java.util.Collections; import java.util.Collections;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.Map;
import java.util.Set; import java.util.Set;
public class MinecraftProfileTexture { public class MinecraftProfileTexture {
@ -10,6 +11,7 @@ public class MinecraftProfileTexture {
// Instance // Instance
private final String url; private final String url;
private final String hash; private final String hash;
private final Map<String, String> metadata;
public MinecraftProfileTexture(String url) { public MinecraftProfileTexture(String url) {
this(url, baseName(url)); this(url, baseName(url));
@ -18,6 +20,13 @@ public MinecraftProfileTexture(String url) {
public MinecraftProfileTexture(String url, String hash) { public MinecraftProfileTexture(String url, String hash) {
this.url = url; this.url = url;
this.hash = hash; this.hash = hash;
this.metadata = null;
}
public MinecraftProfileTexture(String url, String hash, Map<String, String> metadata) {
this.url = url;
this.hash = hash;
this.metadata = metadata;
} }
private static String baseName(String url) { private static String baseName(String url) {
@ -37,7 +46,10 @@ public String getHash() {
} }
public String getMetadata(String key) { public String getMetadata(String key) {
return null; if (metadata == null) {
return null;
}
return metadata.get(key);
} }
public String getUrl() { public String getUrl() {

View file

@ -1,9 +1,11 @@
package com.mojang.authlib.yggdrasil; package com.mojang.authlib.yggdrasil;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.gson.Gson;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
import com.google.gson.reflect.TypeToken;
import com.mojang.authlib.AuthenticationService; import com.mojang.authlib.AuthenticationService;
import com.mojang.authlib.GameProfile; import com.mojang.authlib.GameProfile;
import com.mojang.authlib.exceptions.AuthenticationException; import com.mojang.authlib.exceptions.AuthenticationException;
@ -21,6 +23,7 @@
import pro.gravit.utils.helper.LogHelper; import pro.gravit.utils.helper.LogHelper;
import pro.gravit.utils.helper.SecurityHelper; import pro.gravit.utils.helper.SecurityHelper;
import java.lang.reflect.Type;
import java.net.InetAddress; import java.net.InetAddress;
import java.util.Base64; import java.util.Base64;
import java.util.EnumMap; import java.util.EnumMap;
@ -29,6 +32,7 @@
public class YggdrasilMinecraftSessionService extends BaseMinecraftSessionService { public class YggdrasilMinecraftSessionService extends BaseMinecraftSessionService {
public static final boolean NO_TEXTURES = Boolean.getBoolean("launcher.com.mojang.authlib.noTextures"); public static final boolean NO_TEXTURES = Boolean.getBoolean("launcher.com.mojang.authlib.noTextures");
private static final Gson gson = new Gson();
public YggdrasilMinecraftSessionService(AuthenticationService service) { public YggdrasilMinecraftSessionService(AuthenticationService service) {
super(service); super(service);
@ -53,6 +57,10 @@ public static void fillTextureProperties(GameProfile profile, PlayerProfile pp)
if (pp.skin != null) { if (pp.skin != null) {
properties.put(Launcher.SKIN_URL_PROPERTY, new Property(Launcher.SKIN_URL_PROPERTY, pp.skin.url, "")); properties.put(Launcher.SKIN_URL_PROPERTY, new Property(Launcher.SKIN_URL_PROPERTY, pp.skin.url, ""));
properties.put(Launcher.SKIN_DIGEST_PROPERTY, new Property(Launcher.SKIN_DIGEST_PROPERTY, SecurityHelper.toHex(pp.skin.digest), "")); properties.put(Launcher.SKIN_DIGEST_PROPERTY, new Property(Launcher.SKIN_DIGEST_PROPERTY, SecurityHelper.toHex(pp.skin.digest), ""));
if (pp.skin.metadata != null) {
String metadata = serializeMetadataMap(pp.skin.metadata);
properties.put(Launcher.SKIN_METADATA_PROPERTY, new Property(Launcher.SKIN_METADATA_PROPERTY, metadata, ""));
}
if (debug) { if (debug) {
LogHelper.debug("fillTextureProperties, Has skin texture for username '%s'", profile.getName()); LogHelper.debug("fillTextureProperties, Has skin texture for username '%s'", profile.getName());
} }
@ -60,12 +68,29 @@ public static void fillTextureProperties(GameProfile profile, PlayerProfile pp)
if (pp.cloak != null) { if (pp.cloak != null) {
properties.put(Launcher.CLOAK_URL_PROPERTY, new Property(Launcher.CLOAK_URL_PROPERTY, pp.cloak.url, "")); properties.put(Launcher.CLOAK_URL_PROPERTY, new Property(Launcher.CLOAK_URL_PROPERTY, pp.cloak.url, ""));
properties.put(Launcher.CLOAK_DIGEST_PROPERTY, new Property(Launcher.CLOAK_DIGEST_PROPERTY, SecurityHelper.toHex(pp.cloak.digest), "")); properties.put(Launcher.CLOAK_DIGEST_PROPERTY, new Property(Launcher.CLOAK_DIGEST_PROPERTY, SecurityHelper.toHex(pp.cloak.digest), ""));
if (pp.cloak.metadata != null) {
String metadata = serializeMetadataMap(pp.cloak.metadata);
properties.put(Launcher.CLOAK_METADATA_PROPERTY, new Property(Launcher.SKIN_METADATA_PROPERTY, metadata, ""));
}
if (debug) { if (debug) {
LogHelper.debug("fillTextureProperties, Has cloak texture for username '%s'", profile.getName()); LogHelper.debug("fillTextureProperties, Has cloak texture for username '%s'", profile.getName());
} }
} }
} }
private static String serializeMetadataMap(Map<String, String> map) {
if (map == null) {
return null;
}
return gson.toJson(map);
}
private static Map<String, String> deserializeMetadataMap(String value) {
Type typeOfMap = new TypeToken<Map<String, String>>() {
}.getType();
return gson.fromJson(value, typeOfMap);
}
private static void getTexturesMojang(Map<MinecraftProfileTexture.Type, MinecraftProfileTexture> textures, String texturesBase64, GameProfile profile) { private static void getTexturesMojang(Map<MinecraftProfileTexture.Type, MinecraftProfileTexture> textures, String texturesBase64, GameProfile profile) {
// Decode textures payload // Decode textures payload
JsonObject texturesJSON; JsonObject texturesJSON;
@ -148,14 +173,16 @@ public Map<MinecraftProfileTexture.Type, MinecraftProfileTexture> getTextures(Ga
// Add skin URL to textures map // Add skin URL to textures map
Property skinURL = Iterables.getFirst(profile.getProperties().get(Launcher.SKIN_URL_PROPERTY), null); Property skinURL = Iterables.getFirst(profile.getProperties().get(Launcher.SKIN_URL_PROPERTY), null);
Property skinDigest = Iterables.getFirst(profile.getProperties().get(Launcher.SKIN_DIGEST_PROPERTY), null); Property skinDigest = Iterables.getFirst(profile.getProperties().get(Launcher.SKIN_DIGEST_PROPERTY), null);
Property skinMetadata = Iterables.getFirst(profile.getProperties().get(Launcher.SKIN_METADATA_PROPERTY), null);
if (skinURL != null && skinDigest != null) if (skinURL != null && skinDigest != null)
textures.put(MinecraftProfileTexture.Type.SKIN, new MinecraftProfileTexture(skinURL.getValue(), skinDigest.getValue())); textures.put(MinecraftProfileTexture.Type.SKIN, new MinecraftProfileTexture(skinURL.getValue(), skinDigest.getValue(), skinMetadata == null ? null : deserializeMetadataMap(skinMetadata.getValue())));
// Add cloak URL to textures map // Add cloak URL to textures map
Property cloakURL = Iterables.getFirst(profile.getProperties().get(Launcher.CLOAK_URL_PROPERTY), null); Property cloakURL = Iterables.getFirst(profile.getProperties().get(Launcher.CLOAK_URL_PROPERTY), null);
Property cloakDigest = Iterables.getFirst(profile.getProperties().get(Launcher.CLOAK_DIGEST_PROPERTY), null); Property cloakDigest = Iterables.getFirst(profile.getProperties().get(Launcher.CLOAK_DIGEST_PROPERTY), null);
Property cloakMetadata = Iterables.getFirst(profile.getProperties().get(Launcher.CLOAK_METADATA_PROPERTY), null);
if (cloakURL != null && cloakDigest != null) if (cloakURL != null && cloakDigest != null)
textures.put(MinecraftProfileTexture.Type.CAPE, new MinecraftProfileTexture(cloakURL.getValue(), cloakDigest.getValue())); textures.put(MinecraftProfileTexture.Type.CAPE, new MinecraftProfileTexture(cloakURL.getValue(), cloakDigest.getValue(), cloakMetadata == null ? null : deserializeMetadataMap(cloakMetadata.getValue())));
// Try to find missing textures in textures payload (now always true because launcher is not passing elytra skins) // Try to find missing textures in textures payload (now always true because launcher is not passing elytra skins)
if (textures.size() != MinecraftProfileTexture.PROFILE_TEXTURE_COUNT) { if (textures.size() != MinecraftProfileTexture.PROFILE_TEXTURE_COUNT) {