diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/HDSkinManager.java b/src/hdskins/java/com/voxelmodpack/hdskins/HDSkinManager.java index 6430aa7f..b9df3067 100644 --- a/src/hdskins/java/com/voxelmodpack/hdskins/HDSkinManager.java +++ b/src/hdskins/java/com/voxelmodpack/hdskins/HDSkinManager.java @@ -4,8 +4,6 @@ import com.google.common.base.Preconditions; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; -import com.google.common.collect.BiMap; -import com.google.common.collect.HashBiMap; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; @@ -13,6 +11,7 @@ import com.google.common.collect.Maps; import com.google.common.collect.Streams; import com.minelittlepony.gui.IconicButton; import com.mojang.authlib.GameProfile; +import com.mojang.authlib.exceptions.AuthenticationException; import com.mojang.authlib.minecraft.MinecraftProfileTexture; import com.mojang.authlib.minecraft.MinecraftProfileTexture.Type; import com.mojang.authlib.properties.Property; @@ -20,15 +19,12 @@ import com.mojang.authlib.yggdrasil.response.MinecraftTexturesPayload; import com.mumfrey.liteloader.core.LiteLoader; import com.mumfrey.liteloader.util.log.LiteLoaderLogger; import com.voxelmodpack.hdskins.ducks.INetworkPlayerInfo; +import com.voxelmodpack.hdskins.gui.Feature; import com.voxelmodpack.hdskins.gui.GuiSkins; import com.voxelmodpack.hdskins.resources.SkinResourceManager; import com.voxelmodpack.hdskins.resources.TextureLoader; import com.voxelmodpack.hdskins.resources.texture.ImageBufferDownloadHD; -import com.voxelmodpack.hdskins.server.BethlehemSkinServer; -import com.voxelmodpack.hdskins.server.LegacySkinServer; -import com.voxelmodpack.hdskins.server.ServerType; import com.voxelmodpack.hdskins.server.SkinServer; -import com.voxelmodpack.hdskins.server.ValhallaSkinServer; import com.voxelmodpack.hdskins.util.CallableFutures; import com.voxelmodpack.hdskins.util.MoreStreams; import com.voxelmodpack.hdskins.util.PlayerUtil; @@ -58,7 +54,6 @@ import java.awt.Graphics; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; -import java.lang.reflect.Modifier; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Collections; @@ -88,7 +83,6 @@ public final class HDSkinManager implements IResourceManagerReloadListener { private List clearListeners = Lists.newArrayList(); - private BiMap> skinServerTypes = HashBiMap.create(2); private List skinServers = Lists.newArrayList(); private LoadingCache>> skins = CacheBuilder.newBuilder() @@ -103,11 +97,6 @@ public final class HDSkinManager implements IResourceManagerReloadListener { private Function, GuiSkins> skinsGuiFunc = GuiSkins::new; private HDSkinManager() { - - // register default skin server types - addSkinServerType(LegacySkinServer.class); - addSkinServerType(ValhallaSkinServer.class); - addSkinServerType(BethlehemSkinServer.class); } public void setSkinsGui(Function, GuiSkins> skinsGuiFunc) { @@ -138,11 +127,13 @@ public final class HDSkinManager implements IResourceManagerReloadListener { for (SkinServer server : skinServers) { try { - server.loadProfileData(profile).getTextures().forEach(textureMap::putIfAbsent); - if (textureMap.size() == Type.values().length) { - break; + if (!server.supportsFeature(Feature.SYNTHETIC)) { + server.loadProfileData(profile).getTextures().forEach(textureMap::putIfAbsent); + if (textureMap.size() == Type.values().length) { + break; + } } - } catch (IOException e) { + } catch (IOException | AuthenticationException e) { logger.trace(e); } @@ -231,23 +222,6 @@ public final class HDSkinManager implements IResourceManagerReloadListener { return map; } - private void addSkinServerType(Class type) { - Preconditions.checkArgument(!type.isInterface(), "type cannot be an interface"); - Preconditions.checkArgument(!Modifier.isAbstract(type.getModifiers()), "type cannot be abstract"); - - ServerType st = type.getAnnotation(ServerType.class); - - if (st == null) { - throw new IllegalArgumentException("class is not annotated with @ServerType"); - } - - this.skinServerTypes.put(st.value(), type); - } - - public Class getSkinServerClass(String type) { - return this.skinServerTypes.get(type); - } - void addSkinServer(SkinServer skinServer) { this.skinServers.add(skinServer); } diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/LiteModHDSkins.java b/src/hdskins/java/com/voxelmodpack/hdskins/LiteModHDSkins.java index 43a63b0d..5a025e68 100644 --- a/src/hdskins/java/com/voxelmodpack/hdskins/LiteModHDSkins.java +++ b/src/hdskins/java/com/voxelmodpack/hdskins/LiteModHDSkins.java @@ -1,5 +1,6 @@ package com.voxelmodpack.hdskins; +import com.google.common.collect.Lists; import com.google.gson.GsonBuilder; import com.google.gson.annotations.Expose; import com.mumfrey.liteloader.Configurable; @@ -14,8 +15,7 @@ import com.mumfrey.liteloader.util.ModUtilities; import com.voxelmodpack.hdskins.gui.EntityPlayerModel; import com.voxelmodpack.hdskins.gui.HDSkinsConfigPanel; import com.voxelmodpack.hdskins.gui.RenderPlayerModel; -import com.voxelmodpack.hdskins.server.SkinServer; -import com.voxelmodpack.hdskins.server.SkinServerSerializer; +import com.voxelmodpack.hdskins.server.*; import com.voxelmodpack.hdskins.upload.GLWindow; import net.minecraft.client.Minecraft; @@ -35,7 +35,10 @@ public class LiteModHDSkins implements InitCompleteListener, ViewportListener, C } @Expose - public List skin_servers = SkinServer.defaultServers; + public List skin_servers = Lists.newArrayList( + new ValhallaSkinServer("https://skins.minelittlepony-mod.com"), + new YggdrasilSkinServer() + ); @Expose public boolean experimentalSkinDrop = false; @@ -54,7 +57,7 @@ public class LiteModHDSkins implements InitCompleteListener, ViewportListener, C @Override public String getVersion() { - return "4.0.0"; + return "4.0.1"; } public void writeConfig() { diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/SkinUploader.java b/src/hdskins/java/com/voxelmodpack/hdskins/SkinUploader.java index 8445e02b..60b144df 100644 --- a/src/hdskins/java/com/voxelmodpack/hdskins/SkinUploader.java +++ b/src/hdskins/java/com/voxelmodpack/hdskins/SkinUploader.java @@ -1,6 +1,7 @@ package com.voxelmodpack.hdskins; import net.minecraft.client.Minecraft; +import net.minecraft.client.audio.PositionedSoundRecord; import net.minecraft.init.Items; import net.minecraft.inventory.EntityEquipmentSlot; import net.minecraft.item.ItemStack; @@ -10,10 +11,9 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import com.google.common.base.Throwables; -import com.google.common.collect.Iterables; +import com.google.common.collect.Iterators; import com.mojang.authlib.GameProfile; -import com.mojang.authlib.exceptions.AuthenticationException; -import com.mojang.authlib.exceptions.AuthenticationUnavailableException; +import com.mojang.authlib.exceptions.*; import com.mojang.authlib.minecraft.MinecraftProfileTexture; import com.mojang.authlib.minecraft.MinecraftProfileTexture.Type; import com.voxelmodpack.hdskins.gui.EntityPlayerModel; @@ -33,7 +33,6 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.function.Predicate; public class SkinUploader implements Closeable { @@ -55,11 +54,12 @@ public class SkinUploader implements Closeable { private Type skinType; - private Map skinMetadata = new HashMap(); + private Map skinMetadata = new HashMap<>(); private volatile boolean fetchingSkin = false; private volatile boolean throttlingNeck = false; private volatile boolean offline = false; + private volatile boolean pending = false; private volatile boolean sendingSkin = false; @@ -78,10 +78,6 @@ public class SkinUploader implements Closeable { private final Minecraft mc = Minecraft.getMinecraft(); - private static Iterator cycle(List list, Predicate filter) { - return Iterables.cycle(Iterables.filter(list, filter::test)).iterator(); - } - public SkinUploader(List servers, EntityPlayerModel local, EntityPlayerModel remote, ISkinUploadHandler listener) { localPlayer = local; @@ -91,7 +87,7 @@ public class SkinUploader implements Closeable { skinMetadata.put("model", "default"); this.listener = listener; - skinServers = cycle(servers, SkinServer::verifyGateway); + skinServers = Iterators.cycle(servers); cycleGateway(); } @@ -190,17 +186,14 @@ public class SkinUploader implements Closeable { sendingSkin = true; status = statusMsg; - return gateway.uploadSkin(new SkinUpload(mc.getSession(), skinType, localSkin == null ? null : localSkin.toURI(), skinMetadata)).handle((response, throwable) -> { - if (throwable == null) { - logger.info("Upload completed with: %s", response); - setError(null); - } else { - setError(Throwables.getRootCause(throwable).toString()); + return CompletableFuture.runAsync(() -> { + try { + gateway.performSkinUpload(new SkinUpload(mc.getSession(), skinType, localSkin == null ? null : localSkin.toURI(), skinMetadata)); + setError(""); + } catch (IOException | AuthenticationException e) { + handleException(e); } - - fetchRemote(); - return null; - }); + }, HDSkinManager.skinUploadExecutor).thenRunAsync(this::fetchRemote); } public CompletableFuture downloadSkin() { @@ -210,42 +203,59 @@ public class SkinUploader implements Closeable { } protected void fetchRemote() { + boolean wasPending = pending; + pending = false; fetchingSkin = true; throttlingNeck = false; offline = false; remotePlayer.reloadRemoteSkin(this, (type, location, profileTexture) -> { fetchingSkin = false; + if (type == skinType) { + fetchingSkin = false; + if (wasPending) { + Minecraft.getMinecraft().getSoundHandler().playSound(PositionedSoundRecord.getMasterRecord(net.minecraft.init.SoundEvents.ENTITY_VILLAGER_YES, 1)); + } + } listener.onSetRemoteSkin(type, location, profileTexture); - }).handle((a, throwable) -> { + }).handleAsync((a, throwable) -> { fetchingSkin = false; if (throwable != null) { - throwable = throwable.getCause(); - - if (throwable instanceof AuthenticationUnavailableException) { - offline = true; - } else if (throwable instanceof AuthenticationException) { - throttlingNeck = true; - } else if (throwable instanceof HttpException) { - HttpException ex = (HttpException)throwable; - - logger.error(ex.getReasonPhrase(), ex); - - int code = ex.getStatusCode(); - - if (code >= 500) { - setError(String.format("A fatal server error has ocurred (check logs for details): \n%s", ex.getReasonPhrase())); - } else if (code >= 400 && code != 403 && code != 404) { - setError(ex.getReasonPhrase()); - } - } else { - logger.error("Unhandled exception", throwable); - setError(throwable.toString()); - } + handleException(throwable.getCause()); + } else { + retries = 1; } return a; - }); + }, Minecraft.getMinecraft()::addScheduledTask); + } + + private void handleException(Throwable throwable) { + throwable = Throwables.getRootCause(throwable); + + fetchingSkin = false; + + if (throwable instanceof AuthenticationUnavailableException) { + offline = true; + } else if (throwable instanceof InvalidCredentialsException) { + setError("hdskins.error.session"); + } else if (throwable instanceof AuthenticationException) { + throttlingNeck = true; + } else if (throwable instanceof HttpException) { + HttpException ex = (HttpException)throwable; + + int code = ex.getStatusCode(); + + if (code >= 500) { + logger.error(ex.getReasonPhrase(), ex); + setError("A fatal server error has ocurred (check logs for details): \n" + ex.getReasonPhrase()); + } else if (code >= 400 && code != 403 && code != 404) { + setError(ex.getReasonPhrase()); + } + } else { + logger.error("Unhandled exception", throwable); + setError(throwable.toString()); + } } @Override @@ -282,11 +292,19 @@ public class SkinUploader implements Closeable { retries++; fetchRemote(); } + } else if (pending) { + fetchRemote(); } } public CompletableFuture loadTextures(GameProfile profile) { - return gateway.getPreviewTextures(profile).thenApply(PreviewTextureManager::new); + return CompletableFuture.supplyAsync(() -> { + try { + return new PreviewTextureManager(gateway.getPreviewTextures(profile)); + } catch (IOException | AuthenticationException e) { + throw new RuntimeException(e); + } + }, HDSkinManager.skinDownloadExecutor); // run on main thread } public interface ISkinUploadHandler { diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/gui/Feature.java b/src/hdskins/java/com/voxelmodpack/hdskins/gui/Feature.java index 0f77d1ab..d058ff1b 100644 --- a/src/hdskins/java/com/voxelmodpack/hdskins/gui/Feature.java +++ b/src/hdskins/java/com/voxelmodpack/hdskins/gui/Feature.java @@ -1,9 +1,16 @@ package com.voxelmodpack.hdskins.gui; /** - * Represents the possible features that a skin server can implement. + * Represents the possible features that a skin net can implement. */ public enum Feature { + /** + * Whether this skin server is usable in-game. + * + * Synthetic skin servers will not be queried for skins when in-game, + * but can be still previewed, or accept textures to upload/download. + */ + SYNTHETIC, /** * Whether a server has write access. * i.e. If the server allows for users to upload a new skin. diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/server/BethlehemSkinServer.java b/src/hdskins/java/com/voxelmodpack/hdskins/server/BethlehemSkinServer.java index c3ca7a73..25d13818 100644 --- a/src/hdskins/java/com/voxelmodpack/hdskins/server/BethlehemSkinServer.java +++ b/src/hdskins/java/com/voxelmodpack/hdskins/server/BethlehemSkinServer.java @@ -16,6 +16,7 @@ import java.io.IOException; import java.util.Locale; import java.util.Map; +@Deprecated @ServerType("bethlehem") public class BethlehemSkinServer implements SkinServer { @@ -40,7 +41,7 @@ public class BethlehemSkinServer implements SkinServer { } @Override - public SkinUploadResponse performSkinUpload(SkinUpload upload) throws IOException, AuthenticationException { + public void performSkinUpload(SkinUpload upload) throws IOException, AuthenticationException { SkinServer.verifyServerConnection(upload.getSession(), SERVER_ID); NetClient client = new NetClient("POST", address); @@ -53,10 +54,8 @@ public class BethlehemSkinServer implements SkinServer { try (MoreHttpResponses response = client.send()) { if (!response.ok()) { - throw new HttpException(response.getResponse()); + throw response.exception(); } - - return new SkinUploadResponse(response.text()); } } diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/server/LegacySkinServer.java b/src/hdskins/java/com/voxelmodpack/hdskins/server/LegacySkinServer.java index cc9f3382..b430f190 100644 --- a/src/hdskins/java/com/voxelmodpack/hdskins/server/LegacySkinServer.java +++ b/src/hdskins/java/com/voxelmodpack/hdskins/server/LegacySkinServer.java @@ -32,6 +32,7 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; import javax.annotation.Nullable; +@Deprecated @ServerType("legacy") public class LegacySkinServer implements SkinServer { @@ -51,21 +52,19 @@ public class LegacySkinServer implements SkinServer { } @Override - public CompletableFuture getPreviewTextures(GameProfile profile) { - return CallableFutures.asyncFailableFuture(() -> { - SkinServer.verifyServerConnection(Minecraft.getMinecraft().getSession(), SERVER_ID); + public MinecraftTexturesPayload getPreviewTextures(GameProfile profile) throws IOException, AuthenticationException { + SkinServer.verifyServerConnection(Minecraft.getMinecraft().getSession(), SERVER_ID); - if (Strings.isNullOrEmpty(gateway)) { - throw gatewayUnsupported(); - } + if (Strings.isNullOrEmpty(gateway)) { + throw gatewayUnsupported(); + } - Map map = new EnumMap<>(MinecraftProfileTexture.Type.class); - for (MinecraftProfileTexture.Type type : MinecraftProfileTexture.Type.values()) { - map.put(type, new MinecraftProfileTexture(getPath(gateway, type, profile), null)); - } + Map map = new EnumMap<>(MinecraftProfileTexture.Type.class); + for (MinecraftProfileTexture.Type type : MinecraftProfileTexture.Type.values()) { + map.put(type, new MinecraftProfileTexture(getPath(gateway, type, profile), null)); + } - return TexturesPayloadBuilder.createTexturesPayload(profile, map); - }, HDSkinManager.skinDownloadExecutor); + return TexturesPayloadBuilder.createTexturesPayload(profile, map); } @Override @@ -109,7 +108,7 @@ public class LegacySkinServer implements SkinServer { } @Override - public SkinUploadResponse performSkinUpload(SkinUpload upload) throws IOException, AuthenticationException { + public void performSkinUpload(SkinUpload upload) throws IOException, AuthenticationException { if (Strings.isNullOrEmpty(gateway)) { throw gatewayUnsupported(); } @@ -134,8 +133,6 @@ public class LegacySkinServer implements SkinServer { if (!response.equalsIgnoreCase("OK") && !response.endsWith("OK")) { throw new HttpException(response, resp.getResponseCode(), null); } - - return new SkinUploadResponse(response); } private UnsupportedOperationException gatewayUnsupported() { @@ -161,11 +158,6 @@ public class LegacySkinServer implements SkinServer { return String.format("%s/%s/%s.png?%s", address, path, uuid, Long.toString(new Date().getTime() / 1000)); } - @Override - public boolean verifyGateway() { - return !Strings.isNullOrEmpty(gateway); - } - @Override public boolean supportsFeature(Feature feature) { switch (feature) { diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/server/SkinServer.java b/src/hdskins/java/com/voxelmodpack/hdskins/server/SkinServer.java index ac99dd03..41225714 100644 --- a/src/hdskins/java/com/voxelmodpack/hdskins/server/SkinServer.java +++ b/src/hdskins/java/com/voxelmodpack/hdskins/server/SkinServer.java @@ -1,6 +1,5 @@ package com.voxelmodpack.hdskins.server; -import com.google.common.collect.Lists; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.mojang.authlib.GameProfile; @@ -9,16 +8,12 @@ import com.mojang.authlib.minecraft.MinecraftSessionService; import com.mojang.authlib.yggdrasil.response.MinecraftTexturesPayload; import com.mojang.util.UUIDTypeAdapter; import com.mumfrey.liteloader.modconfig.Exposable; -import com.voxelmodpack.hdskins.HDSkinManager; import com.voxelmodpack.hdskins.gui.Feature; -import com.voxelmodpack.hdskins.util.CallableFutures; import net.minecraft.client.Minecraft; import net.minecraft.util.Session; import java.io.IOException; -import java.util.List; import java.util.UUID; -import java.util.concurrent.CompletableFuture; public interface SkinServer extends Exposable { @@ -26,11 +21,6 @@ public interface SkinServer extends Exposable { .registerTypeAdapter(UUID.class, new UUIDTypeAdapter()) .create(); - List defaultServers = Lists.newArrayList(new LegacySkinServer( - "http://skins.voxelmodpack.com", - "http://skinmanager.voxelmodpack.com") - ); - /** * Returns true for any features that this skin server supports. */ @@ -43,7 +33,7 @@ public interface SkinServer extends Exposable { * * @throws IOException If any authentication or network error occurs. */ - MinecraftTexturesPayload loadProfileData(GameProfile profile) throws IOException; + MinecraftTexturesPayload loadProfileData(GameProfile profile) throws IOException, AuthenticationException; /** * Synchronously uploads a skin to this server. @@ -55,36 +45,18 @@ public interface SkinServer extends Exposable { * @throws IOException If any authentication or network error occurs. * @throws AuthenticationException */ - SkinUploadResponse performSkinUpload(SkinUpload upload) throws IOException, AuthenticationException; - - /** - * Asynchronously uploads a skin to the server. - * - * Returns an incomplete future for chaining other actions to be performed after this method completes. - * Actions are dispatched to the default skinUploadExecutor - * - * @param upload The payload to send. - */ - default CompletableFuture uploadSkin(SkinUpload upload) { - return CallableFutures.asyncFailableFuture(() -> performSkinUpload(upload), HDSkinManager.skinUploadExecutor); - } + void performSkinUpload(SkinUpload upload) throws IOException, AuthenticationException; /** * Asynchronously loads texture information for the provided profile. * * Returns an incomplete future for chaining other actions to be performed after this method completes. * Actions are dispatched to the default skinDownloadExecutor + * @throws AuthenticationException + * @throws IOException */ - default CompletableFuture getPreviewTextures(GameProfile profile) { - return CallableFutures.asyncFailableFuture(() -> loadProfileData(profile), HDSkinManager.skinDownloadExecutor); - } - - /** - * Called to validate this skin server's state. - * Any servers with an invalid gateway format will not be loaded and generate an exception. - */ - default boolean verifyGateway() { - return true; + default MinecraftTexturesPayload getPreviewTextures(GameProfile profile) throws IOException, AuthenticationException { + return loadProfileData(profile); } /** diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/server/SkinServerSerializer.java b/src/hdskins/java/com/voxelmodpack/hdskins/server/SkinServerSerializer.java index 01779cbf..6029b9b4 100644 --- a/src/hdskins/java/com/voxelmodpack/hdskins/server/SkinServerSerializer.java +++ b/src/hdskins/java/com/voxelmodpack/hdskins/server/SkinServerSerializer.java @@ -1,5 +1,8 @@ package com.voxelmodpack.hdskins.server; +import com.google.common.base.Preconditions; +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; @@ -10,10 +13,36 @@ import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; import com.voxelmodpack.hdskins.HDSkinManager; +import java.lang.reflect.Modifier; import java.lang.reflect.Type; + public class SkinServerSerializer implements JsonSerializer, JsonDeserializer { + public static final SkinServerSerializer instance = new SkinServerSerializer(); + + private final BiMap> types = HashBiMap.create(2); + + public SkinServerSerializer() { + // register default skin server types + addSkinServerType(ValhallaSkinServer.class); + addSkinServerType(YggdrasilSkinServer.class); + addSkinServerType(LegacySkinServer.class); + } + + public void addSkinServerType(Class type) { + Preconditions.checkArgument(!type.isInterface(), "type cannot be an interface"); + Preconditions.checkArgument(!Modifier.isAbstract(type.getModifiers()), "type cannot be abstract"); + + ServerType st = type.getAnnotation(ServerType.class); + + if (st == null) { + throw new IllegalArgumentException("class is not annotated with @ServerType"); + } + + types.put(st.value(), type); + } + @Override public JsonElement serialize(SkinServer src, Type typeOfSrc, JsonSerializationContext context) { ServerType serverType = src.getClass().getAnnotation(ServerType.class); @@ -32,6 +61,6 @@ public class SkinServerSerializer implements JsonSerializer, JsonDes public SkinServer deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { String type = json.getAsJsonObject().get("type").getAsString(); - return context.deserialize(json, HDSkinManager.INSTANCE.getSkinServerClass(type)); + return context.deserialize(json, types.get(type)); } } diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/server/TexturePayload.java b/src/hdskins/java/com/voxelmodpack/hdskins/server/TexturePayload.java new file mode 100644 index 00000000..7bfc164d --- /dev/null +++ b/src/hdskins/java/com/voxelmodpack/hdskins/server/TexturePayload.java @@ -0,0 +1,53 @@ +package com.voxelmodpack.hdskins.server; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.minecraft.MinecraftProfileTexture; + +public class TexturePayload { + + private long timestamp; + + private UUID profileId; + + private String profileName; + + private boolean isPublic; + + private Map textures; + + TexturePayload() { } + + public TexturePayload(GameProfile profile, Map textures) { + profileId = profile.getId(); + profileName = profile.getName(); + timestamp = System.currentTimeMillis(); + + isPublic = true; + + this.textures = new HashMap<>(textures); + } + + public long getTimestamp() { + return timestamp; + } + + public UUID getProfileId() { + return profileId; + } + + public String getProfileName() { + return profileName; + } + + public boolean isPublic() { + return isPublic; + } + + public Map getTextures() { + return textures; + } +} diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/server/ValhallaSkinServer.java b/src/hdskins/java/com/voxelmodpack/hdskins/server/ValhallaSkinServer.java index 5d82e102..f5b8c319 100644 --- a/src/hdskins/java/com/voxelmodpack/hdskins/server/ValhallaSkinServer.java +++ b/src/hdskins/java/com/voxelmodpack/hdskins/server/ValhallaSkinServer.java @@ -28,6 +28,7 @@ import java.util.UUID; @ServerType("valhalla") public class ValhallaSkinServer implements SkinServer { + private static final String API_PREFIX = "/api/v1"; @Expose private final String address; @@ -38,8 +39,12 @@ public class ValhallaSkinServer implements SkinServer { this.address = address; } + private String getApiPrefix() { + return address + API_PREFIX; + } + @Override - public MinecraftTexturesPayload loadProfileData(GameProfile profile) throws IOException { + public MinecraftTexturesPayload loadProfileData(GameProfile profile) throws IOException, AuthenticationException { try (MoreHttpResponses response = MoreHttpResponses.execute(HDSkinManager.httpClient, new HttpGet(getTexturesURI(profile)))) { if (response.ok()) { @@ -51,43 +56,46 @@ public class ValhallaSkinServer implements SkinServer { } @Override - public SkinUploadResponse performSkinUpload(SkinUpload upload) throws IOException, AuthenticationException { + public void performSkinUpload(SkinUpload upload) throws IOException, AuthenticationException { try { - return uploadPlayerSkin(upload); + uploadPlayerSkin(upload); } catch (IOException e) { if (e.getMessage().equals("Authorization failed")) { accessToken = null; - return uploadPlayerSkin(upload); + uploadPlayerSkin(upload); } throw e; } } - private SkinUploadResponse uploadPlayerSkin(SkinUpload upload) throws IOException, AuthenticationException { + private void uploadPlayerSkin(SkinUpload upload) throws IOException, AuthenticationException { authorize(upload.getSession()); switch (upload.getSchemaAction()) { case "none": - return resetSkin(upload); + resetSkin(upload); + break; case "file": - return uploadFile(upload); + uploadFile(upload); + break; case "http": case "https": - return uploadUrl(upload); + uploadUrl(upload); + break; default: throw new IOException("Unsupported URI scheme: " + upload.getSchemaAction()); } } - private SkinUploadResponse resetSkin(SkinUpload upload) throws IOException { - return upload(RequestBuilder.delete() + private void resetSkin(SkinUpload upload) throws IOException { + upload(RequestBuilder.delete() .setUri(buildUserTextureUri(upload.getSession().getProfile(), upload.getType())) .addHeader(HttpHeaders.AUTHORIZATION, this.accessToken) .build()); } - private SkinUploadResponse uploadFile(SkinUpload upload) throws IOException { + private void uploadFile(SkinUpload upload) throws IOException { final File file = new File(upload.getImage()); MultipartEntityBuilder b = MultipartEntityBuilder.create() @@ -95,15 +103,15 @@ public class ValhallaSkinServer implements SkinServer { upload.getMetadata().forEach(b::addTextBody); - return upload(RequestBuilder.put() + upload(RequestBuilder.put() .setUri(buildUserTextureUri(upload.getSession().getProfile(), upload.getType())) .addHeader(HttpHeaders.AUTHORIZATION, this.accessToken) .setEntity(b.build()) .build()); } - private SkinUploadResponse uploadUrl(SkinUpload upload) throws IOException { - return upload(RequestBuilder.post() + private void uploadUrl(SkinUpload upload) throws IOException { + upload(RequestBuilder.post() .setUri(buildUserTextureUri(upload.getSession().getProfile(), upload.getType())) .addHeader(HttpHeaders.AUTHORIZATION, this.accessToken) .addParameter("file", upload.getImage().toString()) @@ -111,9 +119,11 @@ public class ValhallaSkinServer implements SkinServer { .build()); } - private SkinUploadResponse upload(HttpUriRequest request) throws IOException { + private void upload(HttpUriRequest request) throws IOException { try (MoreHttpResponses response = MoreHttpResponses.execute(HDSkinManager.httpClient, request)) { - return response.unwrapAsJson(SkinUploadResponse.class); + if (!response.ok()) { + throw response.exception(); + } } } @@ -122,7 +132,6 @@ public class ValhallaSkinServer implements SkinServer { return; } GameProfile profile = session.getProfile(); - String token = session.getToken(); AuthHandshake handshake = authHandshake(profile.getName()); if (handshake.offline) { @@ -130,7 +139,7 @@ public class ValhallaSkinServer implements SkinServer { } // join the session server - Minecraft.getMinecraft().getSessionService().joinServer(profile, token, handshake.serverId); + Minecraft.getMinecraft().getSessionService().joinServer(profile, session.getToken(), handshake.serverId); AuthResponse response = authResponse(profile.getName(), handshake.verifyToken); if (!response.userId.equals(profile.getId())) { @@ -161,20 +170,20 @@ public class ValhallaSkinServer implements SkinServer { private URI buildUserTextureUri(GameProfile profile, MinecraftProfileTexture.Type textureType) { String user = UUIDTypeAdapter.fromUUID(profile.getId()); String skinType = textureType.name().toLowerCase(Locale.US); - return URI.create(String.format("%s/user/%s/%s", this.address, user, skinType)); + return URI.create(String.format("%s/user/%s/%s", this.getApiPrefix(), user, skinType)); } private URI getTexturesURI(GameProfile profile) { Preconditions.checkNotNull(profile.getId(), "profile id required for skins"); - return URI.create(String.format("%s/user/%s", this.address, UUIDTypeAdapter.fromUUID(profile.getId()))); + return URI.create(String.format("%s/user/%s", this.getApiPrefix(), UUIDTypeAdapter.fromUUID(profile.getId()))); } private URI getHandshakeURI() { - return URI.create(String.format("%s/auth/handshake", this.address)); + return URI.create(String.format("%s/auth/handshake", this.getApiPrefix())); } private URI getResponseURI() { - return URI.create(String.format("%s/auth/response", this.address)); + return URI.create(String.format("%s/auth/response", this.getApiPrefix())); } @Override diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/server/YggdrasilSkinServer.java b/src/hdskins/java/com/voxelmodpack/hdskins/server/YggdrasilSkinServer.java new file mode 100644 index 00000000..bea947c8 --- /dev/null +++ b/src/hdskins/java/com/voxelmodpack/hdskins/server/YggdrasilSkinServer.java @@ -0,0 +1,184 @@ +package com.voxelmodpack.hdskins.server; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.annotation.Nonnull; + +import com.google.common.collect.Sets; +import com.google.gson.Gson; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.exceptions.AuthenticationException; +import com.mojang.authlib.minecraft.*; +import com.mojang.authlib.yggdrasil.response.MinecraftTexturesPayload; +import com.mojang.util.UUIDTypeAdapter; +import com.voxelmodpack.hdskins.HDSkinManager; +import com.voxelmodpack.hdskins.gui.Feature; +import com.voxelmodpack.hdskins.util.*; + +import net.minecraft.client.Minecraft; +import net.minecraft.util.Session; +import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.entity.mime.MultipartEntityBuilder; + +@ServerType("mojang") +public class YggdrasilSkinServer implements SkinServer { + + static final SkinServer INSTANCE = new YggdrasilSkinServer(); + + private static final Set FEATURES = Sets.newHashSet( + Feature.SYNTHETIC, + Feature.UPLOAD_USER_SKIN, + Feature.DOWNLOAD_USER_SKIN, + Feature.DELETE_USER_SKIN, + Feature.MODEL_VARIANTS, + Feature.MODEL_TYPES); + + private transient final String address = "https://api.mojang.com"; + private transient final String verify = "https://authserver.mojang.com/validate"; + + private transient final boolean requireSecure = true; + + @Override + public boolean supportsFeature(Feature feature) { + return FEATURES.contains(feature); + } + + @Override + public MinecraftTexturesPayload loadProfileData(GameProfile profile) throws IOException, AuthenticationException { + + Map textures = new HashMap<>(); + + Minecraft client = Minecraft.getMinecraft(); + MinecraftSessionService session = client.getSessionService(); + + profile.getProperties().clear(); + GameProfile newProfile = session.fillProfileProperties(profile, requireSecure); + + if (newProfile == profile) { + throw new AuthenticationException("Mojang API error occured. You may be throttled."); + } + profile = newProfile; + + try { + textures.putAll(session.getTextures(profile, requireSecure)); + } catch (InsecureTextureException e) { + HDSkinManager.logger.error(e); + } + + return TexturesPayloadBuilder.createTexturesPayload(profile, textures); + } + + @Override + public void performSkinUpload(SkinUpload upload) throws IOException, AuthenticationException { + authorize(upload.getSession()); + + switch (upload.getSchemaAction()) { + case "none": + send(appendHeaders(upload, RequestBuilder.delete())); + break; + default: + send(prepareUpload(upload, RequestBuilder.put())); + } + + Minecraft client = Minecraft.getMinecraft(); + client.getProfileProperties().clear(); + } + + private RequestBuilder prepareUpload(SkinUpload upload, RequestBuilder request) throws IOException { + request = appendHeaders(upload, request); + switch (upload.getSchemaAction()) { + case "file": + final File file = new File(upload.getImage()); + + MultipartEntityBuilder b = MultipartEntityBuilder.create() + .addBinaryBody("file", file, ContentType.create("image/png"), file.getName()); + + mapMetadata(upload.getMetadata()).forEach(b::addTextBody); + + return request.setEntity(b.build()); + case "http": + case "https": + return request + .addParameter("file", upload.getImage().toString()) + .addParameters(MoreHttpResponses.mapAsParameters(mapMetadata(upload.getMetadata()))); + default: + throw new IOException("Unsupported URI scheme: " + upload.getSchemaAction()); + } + } + + private RequestBuilder appendHeaders(SkinUpload upload, RequestBuilder request) { + return request + .setUri(URI.create(String.format("%s/user/profile/%s/%s", address, + UUIDTypeAdapter.fromUUID(upload.getSession().getProfile().getId()), + upload.getType()))) + .addHeader("authorization", "Bearer " + upload.getSession().getToken()); + } + + private Map mapMetadata(Map metadata) { + return metadata.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, + entry -> { + String value = entry.getValue(); + if ("model".contentEquals(entry.getKey()) && "default".contentEquals(value)) { + return "classic"; + } + return value; + }) + ); + } + + private void authorize(Session session) throws IOException { + RequestBuilder request = RequestBuilder.post().setUri(verify); + request.setEntity(new TokenRequest(session).toEntity()); + + send(request); + } + + private void send(RequestBuilder request) throws IOException { + try (MoreHttpResponses response = MoreHttpResponses.execute(HDSkinManager.httpClient, request.build())) { + if (!response.ok()) { + throw new IOException(response.json(ErrorResponse.class, "Server error wasn't in json: {}").toString()); + } + } + } + + @Override + public String toString() { + return new IndentedToStringStyle.Builder(this) + .append("address", address) + .append("secured", requireSecure) + .toString(); + } + + static class TokenRequest { + static final Gson GSON = new Gson(); + + @Nonnull + private final String accessToken; + + TokenRequest(Session session) { + accessToken = session.getToken(); + } + + public StringEntity toEntity() throws IOException { + return new StringEntity(GSON.toJson(this), ContentType.APPLICATION_JSON); + } + } + + class ErrorResponse { + String error; + String errorMessage; + + @Override + public String toString() { + return String.format("%s: %s", error, errorMessage); + } + } +} diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/util/MoreHttpResponses.java b/src/hdskins/java/com/voxelmodpack/hdskins/util/MoreHttpResponses.java index 43bac9bd..6baadb9f 100644 --- a/src/hdskins/java/com/voxelmodpack/hdskins/util/MoreHttpResponses.java +++ b/src/hdskins/java/com/voxelmodpack/hdskins/util/MoreHttpResponses.java @@ -2,7 +2,9 @@ package com.voxelmodpack.hdskins.util; import com.google.common.io.ByteStreams; import com.google.common.io.CharStreams; -import com.google.gson.JsonObject; +import com.google.gson.*; +import com.mojang.util.UUIDTypeAdapter; +import com.voxelmodpack.hdskins.HDSkinManager; import com.voxelmodpack.hdskins.server.SkinServer; import org.apache.http.Header; @@ -11,6 +13,7 @@ import org.apache.http.HttpStatus; import org.apache.http.NameValuePair; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.entity.ContentType; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.message.BasicNameValuePair; @@ -20,8 +23,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.stream.Stream; /** @@ -29,6 +31,9 @@ import java.util.stream.Stream; */ @FunctionalInterface public interface MoreHttpResponses extends AutoCloseable { + Gson GSON = new GsonBuilder() + .registerTypeAdapter(UUID.class, new UUIDTypeAdapter()) + .create(); CloseableHttpResponse getResponse(); @@ -36,6 +41,10 @@ public interface MoreHttpResponses extends AutoCloseable { return getResponseCode() == HttpStatus.SC_OK; } + default boolean json() { + return "application/json".contentEquals(contentType().getMimeType()); + } + default int getResponseCode() { return getResponse().getStatusLine().getStatusCode(); } @@ -44,6 +53,12 @@ public interface MoreHttpResponses extends AutoCloseable { return Optional.ofNullable(getResponse().getEntity()); } + default ContentType contentType() { + return getEntity() + .map(ContentType::get) + .orElse(ContentType.DEFAULT_TEXT); + } + default String getContentType() { return getEntity().map(HttpEntity::getContentType).map(Header::getValue).orElse("text/plain"); } @@ -86,6 +101,22 @@ public interface MoreHttpResponses extends AutoCloseable { } } + default T json(Class type, String errorMessage) throws IOException { + return json((Type)type, errorMessage); + } + + default T json(Type type, String errorMessage) throws IOException { + if (!json()) { + String text = text(); + HDSkinManager.logger.error(errorMessage, text); + throw new IOException(text); + } + + try (BufferedReader reader = getReader()) { + return GSON.fromJson(reader, type); + } + } + default T unwrapAsJson(Type type) throws IOException { if (!"application/json".equals(getContentType())) { throw new IOException("Server returned a non-json response!"); @@ -95,7 +126,11 @@ public interface MoreHttpResponses extends AutoCloseable { return json(type); } - throw new IOException(json(JsonObject.class).get("message").getAsString()); + throw exception(); + } + + default IOException exception() throws IOException { + return new IOException(json(JsonObject.class, "Server error wasn't in json: {}").get("message").getAsString()); } @Override diff --git a/src/hdskins/resources/assets/hdskins/skins/servers.json b/src/hdskins/resources/assets/hdskins/skins/servers.json new file mode 100644 index 00000000..181f6d40 --- /dev/null +++ b/src/hdskins/resources/assets/hdskins/skins/servers.json @@ -0,0 +1,11 @@ +{ + "overwrite": false, + "insert": "END", + "servers": [ + { + "type": "valhalla", + "address": "https://skins.minelittlepony-mod.com" + }, + { "type": "mojang" } + ] +} \ No newline at end of file diff --git a/src/main/java/com/minelittlepony/MineLittlePony.java b/src/main/java/com/minelittlepony/MineLittlePony.java index 98e6cffc..b8cdbf40 100644 --- a/src/main/java/com/minelittlepony/MineLittlePony.java +++ b/src/main/java/com/minelittlepony/MineLittlePony.java @@ -36,10 +36,6 @@ public class MineLittlePony { public static final String MOD_NAME = "Mine Little Pony"; public static final String MOD_VERSION = "@VERSION@"; - private static final String MINELP_VALHALLA_SERVER = "http://skins.minelittlepony-mod.com"; - private static final String MINELP_LEGACY_SERVER = "http://minelpskins.voxelmodpack.com"; - private static final String MINELP_LEGACY_GATEWAY = "http://minelpskinmanager.voxelmodpack.com"; - private static final KeyBinding SETTINGS_GUI = new KeyBinding("Settings", Keyboard.KEY_F9, "Mine Little Pony"); private static MineLittlePony instance; @@ -69,10 +65,6 @@ public class MineLittlePony { MetadataSerializer ms = Minecraft.getMinecraft().getResourcePackRepository().rprMetadataSerializer; ms.registerMetadataSectionType(new PonyDataSerialiser(), IPonyData.class); - - // This also makes it the default gateway server. - SkinServer.defaultServers.add(new LegacySkinServer(MINELP_LEGACY_SERVER, MINELP_LEGACY_GATEWAY)); - SkinServer.defaultServers.add(0, new ValhallaSkinServer(MINELP_VALHALLA_SERVER)); } /**