SkinServer updates (#83)

SkinServer update:
 * loadProfileData was unwrapped from Optional. Now it throws an exception.
 * getPreviewTextures is now a CompletableFuture so it won't lag GuiSkins
 * arguments for uploadSkin were condensed into a single object
 * Added a class wrapper for getting different http response types
 * HttpClient is now a singleton which lives in SkinServer.httpClient
This commit is contained in:
Matthew Messinger 2018-08-05 18:35:13 -04:00 committed by GitHub
parent 1369665fd6
commit f8ee05ca11
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 247 additions and 197 deletions

View file

@ -9,16 +9,11 @@ import com.google.common.collect.HashBiMap;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.mojang.authlib.GameProfile; import com.mojang.authlib.GameProfile;
import com.mojang.authlib.minecraft.MinecraftProfileTexture; import com.mojang.authlib.minecraft.MinecraftProfileTexture;
import com.mojang.authlib.minecraft.MinecraftProfileTexture.Type; import com.mojang.authlib.minecraft.MinecraftProfileTexture.Type;
import com.mojang.authlib.properties.Property; import com.mojang.authlib.properties.Property;
import com.mojang.authlib.yggdrasil.response.MinecraftTexturesPayload; import com.mojang.authlib.yggdrasil.response.MinecraftTexturesPayload;
import com.mojang.util.UUIDTypeAdapter;
import com.mumfrey.liteloader.core.LiteLoader; import com.mumfrey.liteloader.core.LiteLoader;
import com.mumfrey.liteloader.util.log.LiteLoaderLogger; import com.mumfrey.liteloader.util.log.LiteLoaderLogger;
import com.voxelmodpack.hdskins.gui.GuiSkins; import com.voxelmodpack.hdskins.gui.GuiSkins;
@ -39,6 +34,9 @@ import net.minecraft.client.resources.IResourceManagerReloadListener;
import net.minecraft.client.resources.SkinManager.SkinAvailableCallback; import net.minecraft.client.resources.SkinManager.SkinAvailableCallback;
import net.minecraft.util.ResourceLocation; import net.minecraft.util.ResourceLocation;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.logging.log4j.LogManager;
import java.awt.Graphics; import java.awt.Graphics;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
@ -53,6 +51,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -62,13 +61,11 @@ import javax.annotation.Nonnull;
public final class HDSkinManager implements IResourceManagerReloadListener { public final class HDSkinManager implements IResourceManagerReloadListener {
private static final ResourceLocation LOADING = new ResourceLocation("LOADING"); public static final ExecutorService skinUploadExecutor = Executors.newSingleThreadExecutor();
private static final Gson GSON = new GsonBuilder() public static final ExecutorService skinDownloadExecutor = Executors.newFixedThreadPool(8);
.registerTypeAdapter(UUID.class, new UUIDTypeAdapter()) public static final CloseableHttpClient httpClient = HttpClients.createSystem();
.create();
private static final ExecutorService skinDownloadExecutor = Executors.newFixedThreadPool(8); private static final ResourceLocation LOADING = new ResourceLocation("LOADING");
public static final ListeningExecutorService skinUploadExecutor = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
public static final HDSkinManager INSTANCE = new HDSkinManager(); public static final HDSkinManager INSTANCE = new HDSkinManager();
@ -133,7 +130,7 @@ public final class HDSkinManager implements IResourceManagerReloadListener {
Property textures = Iterables.getFirst(profile1.getProperties().get("textures"), null); Property textures = Iterables.getFirst(profile1.getProperties().get("textures"), null);
if (textures != null) { if (textures != null) {
String json = new String(Base64.getDecoder().decode(textures.getValue()), StandardCharsets.UTF_8); String json = new String(Base64.getDecoder().decode(textures.getValue()), StandardCharsets.UTF_8);
MinecraftTexturesPayload texturePayload = GSON.fromJson(json, MinecraftTexturesPayload.class); MinecraftTexturesPayload texturePayload = SkinServer.gson.fromJson(json, MinecraftTexturesPayload.class);
if (texturePayload != null) { if (texturePayload != null) {
// name is optional // name is optional
String name = texturePayload.getProfileName(); String name = texturePayload.getProfileName();
@ -216,10 +213,13 @@ public final class HDSkinManager implements IResourceManagerReloadListener {
private Map<Type, MinecraftProfileTexture> loadProfileData(GameProfile profile) { private Map<Type, MinecraftProfileTexture> loadProfileData(GameProfile profile) {
Map<Type, MinecraftProfileTexture> textures = Maps.newEnumMap(Type.class); Map<Type, MinecraftProfileTexture> textures = Maps.newEnumMap(Type.class);
for (SkinServer server : skinServers) { for (SkinServer server : skinServers) {
Optional<MinecraftTexturesPayload> profileData = server.loadProfileData(profile); try {
profileData.map(MinecraftTexturesPayload::getTextures).ifPresent(it -> it.forEach(textures::putIfAbsent)); server.loadProfileData(profile).getTextures().forEach(textures::putIfAbsent);
if (textures.size() == Type.values().length) { if (textures.size() == Type.values().length) {
break; break;
}
} catch (IOException e) {
LogManager.getLogger().trace(e);
} }
} }
@ -263,8 +263,8 @@ public final class HDSkinManager implements IResourceManagerReloadListener {
this.enabled = enabled; this.enabled = enabled;
} }
public static PreviewTextureManager getPreviewTextureManager(GameProfile profile) { public static CompletableFuture<PreviewTextureManager> getPreviewTextureManager(GameProfile profile) {
return new PreviewTextureManager(INSTANCE.getGatewayServer().getPreviewTextures(profile)); return INSTANCE.getGatewayServer().getPreviewTextures(profile).thenApply(PreviewTextureManager::new);
} }
public void addClearListener(ISkinCacheClearListener listener) { public void addClearListener(ISkinCacheClearListener listener) {

View file

@ -2,7 +2,7 @@ package com.voxelmodpack.hdskins;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.mojang.authlib.minecraft.MinecraftProfileTexture; import com.mojang.authlib.minecraft.MinecraftProfileTexture;
import net.minecraft.client.Minecraft; import com.mojang.authlib.yggdrasil.response.MinecraftTexturesPayload;
import net.minecraft.client.renderer.IImageBuffer; import net.minecraft.client.renderer.IImageBuffer;
import net.minecraft.client.resources.SkinManager; import net.minecraft.client.resources.SkinManager;
import net.minecraft.util.ResourceLocation; import net.minecraft.util.ResourceLocation;
@ -20,8 +20,8 @@ public class PreviewTextureManager {
private final Map<MinecraftProfileTexture.Type, MinecraftProfileTexture> textures; private final Map<MinecraftProfileTexture.Type, MinecraftProfileTexture> textures;
PreviewTextureManager(Map<MinecraftProfileTexture.Type, MinecraftProfileTexture> textures) { PreviewTextureManager(MinecraftTexturesPayload payload) {
this.textures = textures; this.textures = payload.getTextures();
} }
@Nullable @Nullable
@ -47,7 +47,8 @@ public class PreviewTextureManager {
} }
} }
} : null); } : null);
Minecraft.getMinecraft().getTextureManager().loadTexture(location, skinTexture);
TextureLoader.loadTexture(location, skinTexture);
return skinTexture; return skinTexture;
} }

View file

@ -8,7 +8,6 @@ import com.voxelmodpack.hdskins.DynamicTextureImage;
import com.voxelmodpack.hdskins.HDSkinManager; import com.voxelmodpack.hdskins.HDSkinManager;
import com.voxelmodpack.hdskins.ImageBufferDownloadHD; import com.voxelmodpack.hdskins.ImageBufferDownloadHD;
import com.voxelmodpack.hdskins.PreviewTexture; import com.voxelmodpack.hdskins.PreviewTexture;
import com.voxelmodpack.hdskins.PreviewTextureManager;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.texture.DynamicTexture; import net.minecraft.client.renderer.texture.DynamicTexture;
import net.minecraft.client.renderer.texture.TextureManager; import net.minecraft.client.renderer.texture.TextureManager;
@ -25,6 +24,7 @@ import java.awt.image.BufferedImage;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Map; import java.util.Map;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
@SuppressWarnings("EntityConstructor") @SuppressWarnings("EntityConstructor")
@ -41,11 +41,11 @@ public class EntityPlayerModel extends EntityLivingBase {
EntityEquipmentSlot.MAINHAND, ItemStack.EMPTY EntityEquipmentSlot.MAINHAND, ItemStack.EMPTY
)); ));
private PreviewTexture remoteSkinTexture; private volatile PreviewTexture remoteSkinTexture;
private ResourceLocation remoteSkinResource; private ResourceLocation remoteSkinResource;
protected ResourceLocation localSkinResource; protected ResourceLocation localSkinResource;
private DynamicTexture localSkinTexture; private DynamicTexture localSkinTexture;
private PreviewTexture remoteElytraTexture; private volatile PreviewTexture remoteElytraTexture;
private ResourceLocation remoteElytraResource; private ResourceLocation remoteElytraResource;
private ResourceLocation localElytraResource; private ResourceLocation localElytraResource;
private DynamicTexture localElytraTexture; private DynamicTexture localElytraTexture;
@ -77,10 +77,11 @@ public class EntityPlayerModel extends EntityLivingBase {
this.textureManager.deleteTexture(this.remoteElytraResource); this.textureManager.deleteTexture(this.remoteElytraResource);
} }
PreviewTextureManager ptm = HDSkinManager.getPreviewTextureManager(this.profile); HDSkinManager.getPreviewTextureManager(this.profile).thenAccept(ptm -> {
this.remoteSkinTexture = ptm.getPreviewTexture(this.remoteSkinResource, Type.SKIN, getBlankSkin(), listener);
this.remoteElytraTexture = ptm.getPreviewTexture(this.remoteElytraResource, Type.ELYTRA, getBlankElytra(), null);
});
this.remoteSkinTexture = ptm.getPreviewTexture(this.remoteSkinResource, Type.SKIN, getBlankSkin(), listener);
this.remoteElytraTexture = ptm.getPreviewTexture(this.remoteElytraResource, Type.ELYTRA, getBlankElytra(), null);
} }

View file

@ -12,6 +12,7 @@ import com.mojang.authlib.minecraft.MinecraftProfileTexture;
import com.mumfrey.liteloader.util.log.LiteLoaderLogger; import com.mumfrey.liteloader.util.log.LiteLoaderLogger;
import com.voxelmodpack.hdskins.HDSkinManager; import com.voxelmodpack.hdskins.HDSkinManager;
import com.voxelmodpack.hdskins.skins.SkinServer; import com.voxelmodpack.hdskins.skins.SkinServer;
import com.voxelmodpack.hdskins.skins.SkinUpload;
import com.voxelmodpack.hdskins.skins.SkinUploadResponse; import com.voxelmodpack.hdskins.skins.SkinUploadResponse;
import com.voxelmodpack.hdskins.upload.awt.ThreadOpenFilePNG; import com.voxelmodpack.hdskins.upload.awt.ThreadOpenFilePNG;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
@ -567,7 +568,7 @@ public class GuiSkins extends GuiScreen {
this.uploadingSkin = true; this.uploadingSkin = true;
this.skinUploadMessage = I18n.format("hdskins.request"); this.skinUploadMessage = I18n.format("hdskins.request");
HDSkinManager.INSTANCE.getGatewayServer() HDSkinManager.INSTANCE.getGatewayServer()
.uploadSkin(session, null, this.textureType, getMetadata()) .uploadSkin(session, new SkinUpload(this.textureType, null, getMetadata()))
.thenAccept(this::onUploadComplete) .thenAccept(this::onUploadComplete)
.exceptionally(this::onFailure); .exceptionally(this::onFailure);
} }
@ -577,7 +578,7 @@ public class GuiSkins extends GuiScreen {
this.skinUploadMessage = I18n.format("hdskins.upload"); this.skinUploadMessage = I18n.format("hdskins.upload");
URI path = skinFile == null ? null : skinFile.toURI(); URI path = skinFile == null ? null : skinFile.toURI();
HDSkinManager.INSTANCE.getGatewayServer() HDSkinManager.INSTANCE.getGatewayServer()
.uploadSkin(session, path, this.textureType, getMetadata()) .uploadSkin(session, new SkinUpload(this.textureType, path, getMetadata()))
.thenAccept(this::onUploadComplete) .thenAccept(this::onUploadComplete)
.exceptionally(this::onFailure); .exceptionally(this::onFailure);
} }
@ -591,7 +592,6 @@ public class GuiSkins extends GuiScreen {
this.btnUpload.enabled = true; this.btnUpload.enabled = true;
} }
private Void onFailure(Throwable t) { private Void onFailure(Throwable t) {
t = Throwables.getRootCause(t); t = Throwables.getRootCause(t);
LogManager.getLogger().warn("Upload failed", t); LogManager.getLogger().warn("Upload failed", t);

View file

@ -5,6 +5,7 @@ import com.google.common.collect.ImmutableMap.Builder;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.annotations.Expose; import com.google.gson.annotations.Expose;
import com.mojang.authlib.GameProfile; import com.mojang.authlib.GameProfile;
import com.mojang.authlib.minecraft.MinecraftProfileTexture;
import com.mojang.authlib.minecraft.MinecraftProfileTexture.Type; import com.mojang.authlib.minecraft.MinecraftProfileTexture.Type;
import com.mojang.authlib.yggdrasil.response.MinecraftTexturesPayload; import com.mojang.authlib.yggdrasil.response.MinecraftTexturesPayload;
import com.mojang.util.UUIDTypeAdapter; import com.mojang.util.UUIDTypeAdapter;
@ -16,9 +17,10 @@ import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import javax.annotation.Nullable;
@ServerType("bethlehem") @ServerType("bethlehem")
public class BethlehemSkinServer implements SkinServer { public class BethlehemSkinServer implements SkinServer {
@ -32,24 +34,24 @@ public class BethlehemSkinServer implements SkinServer {
} }
@Override @Override
public Optional<MinecraftTexturesPayload> loadProfileData(GameProfile profile) { public MinecraftTexturesPayload loadProfileData(GameProfile profile) throws IOException {
NetClient client = new NetClient("GET", getPath(profile)); try (MoreHttpResponses response = new NetClient("GET", getPath(profile)).send()) {
String json = client.getResponseText(); JsonObject s = response.json(JsonObject.class);
JsonObject s = gson.fromJson(json, JsonObject.class); if (s.has("success") && s.get("success").getAsBoolean()) {
s = s.get("data").getAsJsonObject();
if (s.has("success") && s.get("success").getAsBoolean()) { return gson.fromJson(s, MinecraftTexturesPayload.class);
s = s.get("data").getAsJsonObject(); }
throw new IOException(s.get("error").getAsString());
return Optional.ofNullable(gson.fromJson(s, MinecraftTexturesPayload.class));
} }
return Optional.empty();
} }
@Override @Override
public CompletableFuture<SkinUploadResponse> uploadSkin(Session session, URI image, Type type, Map<String, String> metadata) { public CompletableFuture<SkinUploadResponse> uploadSkin(Session session, SkinUpload skin) {
URI image = skin.getImage();
Map<String, String> metadata = skin.getMetadata();
MinecraftProfileTexture.Type type = skin.getType();
return CallableFutures.asyncFailableFuture(() -> { return CallableFutures.asyncFailableFuture(() -> {
SkinServer.verifyServerConnection(session, SERVER_ID); SkinServer.verifyServerConnection(session, SERVER_ID);
@ -59,15 +61,17 @@ public class BethlehemSkinServer implements SkinServer {
client.putFile(type.toString().toLowerCase(Locale.US), "image/png", image); client.putFile(type.toString().toLowerCase(Locale.US), "image/png", image);
} }
if (!client.send()) { try (MoreHttpResponses response = client.send()) {
throw new IOException(client.getResponseText()); if (!response.ok()) {
throw new IOException(response.text());
}
return new SkinUploadResponse(response.text());
} }
return new SkinUploadResponse(client.getResponseText());
}, HDSkinManager.skinUploadExecutor); }, HDSkinManager.skinUploadExecutor);
} }
protected Map<String, ?> createHeaders(Session session, Type type, URI image, Map<String, String> metadata) { protected Map<String, ?> createHeaders(Session session, Type type, @Nullable URI image, Map<String, String> metadata) {
Builder<String, Object> builder = ImmutableMap.<String, Object>builder() Builder<String, Object> builder = ImmutableMap.<String, Object>builder()
.put("accessToken", session.getToken()) .put("accessToken", session.getToken())
.put("user", session.getUsername()) .put("user", session.getUsername())

View file

@ -18,11 +18,9 @@ import java.io.IOException;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URI; import java.net.URI;
import java.net.URL; import java.net.URL;
import java.util.Collections;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -45,30 +43,25 @@ public class LegacySkinServer implements SkinServer {
} }
@Override @Override
public Map<MinecraftProfileTexture.Type, MinecraftProfileTexture> getPreviewTextures(GameProfile profile) { public CompletableFuture<MinecraftTexturesPayload> getPreviewTextures(GameProfile profile) {
if (Strings.isNullOrEmpty(this.gateway)) { if (Strings.isNullOrEmpty(this.gateway)) {
return Collections.emptyMap(); return CallableFutures.failedFuture(gatewayUnsupported());
} }
Map<MinecraftProfileTexture.Type, MinecraftProfileTexture> map = new EnumMap<>(MinecraftProfileTexture.Type.class); Map<MinecraftProfileTexture.Type, MinecraftProfileTexture> map = new EnumMap<>(MinecraftProfileTexture.Type.class);
for (MinecraftProfileTexture.Type type : MinecraftProfileTexture.Type.values()) { for (MinecraftProfileTexture.Type type : MinecraftProfileTexture.Type.values()) {
map.put(type, new MinecraftProfileTexture(getPath(gateway, type, profile), null)); map.put(type, new MinecraftProfileTexture(getPath(gateway, type, profile), null));
} }
return map; return CompletableFuture.completedFuture(TexturesPayloadBuilder.createTexturesPayload(profile, map));
} }
@Override @Override
public Optional<MinecraftTexturesPayload> loadProfileData(GameProfile profile) { public MinecraftTexturesPayload loadProfileData(GameProfile profile) throws IOException {
ImmutableMap.Builder<MinecraftProfileTexture.Type, MinecraftProfileTexture> builder = ImmutableMap.builder(); ImmutableMap.Builder<MinecraftProfileTexture.Type, MinecraftProfileTexture> builder = ImmutableMap.builder();
for (MinecraftProfileTexture.Type type : MinecraftProfileTexture.Type.values()) { for (MinecraftProfileTexture.Type type : MinecraftProfileTexture.Type.values()) {
String url = getPath(this.address, type, profile); String url = getPath(this.address, type, profile);
try { try {
HttpURLConnection urlConnection = (HttpURLConnection) new URL(url).openConnection(); builder.put(type, loadProfileTexture(profile, url));
if (urlConnection.getResponseCode() / 100 != 2) {
throw new IOException("Bad response code: " + urlConnection.getResponseCode());
}
builder.put(type, new MinecraftProfileTexture(url, null));
logger.debug("Found skin for {} at {}", profile.getName(), url);
} catch (IOException e) { } catch (IOException e) {
logger.trace("Couldn't find texture for {} at {}. Does it exist?", profile.getName(), url, e); logger.trace("Couldn't find texture for {} at {}. Does it exist?", profile.getName(), url, e);
} }
@ -76,21 +69,32 @@ public class LegacySkinServer implements SkinServer {
Map<MinecraftProfileTexture.Type, MinecraftProfileTexture> map = builder.build(); Map<MinecraftProfileTexture.Type, MinecraftProfileTexture> map = builder.build();
if (map.isEmpty()) { if (map.isEmpty()) {
logger.debug("No textures found for {} at {}", profile, this.address); throw new IOException(String.format("No textures found for %s at %s", profile, this.address));
return Optional.empty();
} }
return TexturesPayloadBuilder.createTexturesPayload(profile, map);
}
return Optional.of(TexturesPayloadBuilder.createTexturesPayload(profile, map)); private MinecraftProfileTexture loadProfileTexture(GameProfile profile, String url) throws IOException {
HttpURLConnection urlConnection = (HttpURLConnection) new URL(url).openConnection();
if (urlConnection.getResponseCode() / 100 != 2) {
throw new IOException("Bad response code: " + urlConnection.getResponseCode() + ". URL: " + url);
}
logger.debug("Found skin for {} at {}", profile.getName(), url);
return new MinecraftProfileTexture(url, null);
} }
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
@Override @Override
public CompletableFuture<SkinUploadResponse> uploadSkin(Session session, @Nullable URI image, MinecraftProfileTexture.Type type, Map<String, String> metadata) { public CompletableFuture<SkinUploadResponse> uploadSkin(Session session, SkinUpload skin) {
if (Strings.isNullOrEmpty(this.gateway)) { if (Strings.isNullOrEmpty(this.gateway)) {
return CallableFutures.failedFuture(new NullPointerException("gateway url is blank")); return CallableFutures.failedFuture(gatewayUnsupported());
} }
return CallableFutures.asyncFailableFuture(() -> { return CallableFutures.asyncFailableFuture(() -> {
URI image = skin.getImage();
MinecraftProfileTexture.Type type = skin.getType();
Map<String, String> metadata = skin.getMetadata();
SkinServer.verifyServerConnection(session, SERVER_ID); SkinServer.verifyServerConnection(session, SERVER_ID);
String model = metadata.getOrDefault("model", "default"); String model = metadata.getOrDefault("model", "default");
Map<String, ?> data = image == null ? getClearData(session, type) : getUploadData(session, type, model, image); Map<String, ?> data = image == null ? getClearData(session, type) : getUploadData(session, type, model, image);
@ -99,13 +103,18 @@ public class LegacySkinServer implements SkinServer {
if (response.startsWith("ERROR: ")) { if (response.startsWith("ERROR: ")) {
response = response.substring(7); response = response.substring(7);
} }
if (!response.equalsIgnoreCase("OK") && !response.endsWith("OK")) if (!response.equalsIgnoreCase("OK") && !response.endsWith("OK")) {
throw new IOException(response); throw new IOException(response);
}
return new SkinUploadResponse(response); return new SkinUploadResponse(response);
}, HDSkinManager.skinUploadExecutor); }, HDSkinManager.skinUploadExecutor);
} }
private UnsupportedOperationException gatewayUnsupported() {
return new UnsupportedOperationException("Server does not have a gateway.");
}
private static Map<String, ?> getData(Session session, MinecraftProfileTexture.Type type, String model, String param, Object val) { private static Map<String, ?> getData(Session session, MinecraftProfileTexture.Type type, String model, String param, Object val) {
return ImmutableMap.of( return ImmutableMap.of(
"user", session.getUsername(), "user", session.getUsername(),

View file

@ -0,0 +1,75 @@
package com.voxelmodpack.hdskins.skins;
import com.google.common.io.CharStreams;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.impl.client.CloseableHttpClient;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.stream.Stream;
/**
* Utility class for getting different response types from a http response.
*/
@FunctionalInterface
public interface MoreHttpResponses extends AutoCloseable {
CloseableHttpResponse getResponse();
default boolean ok() {
return getResponseCode() == HttpStatus.SC_OK;
}
default int getResponseCode() {
return getResponse().getStatusLine().getStatusCode();
}
default InputStream getInputStream() throws IOException {
return getResponse().getEntity().getContent();
}
default BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8));
}
default String text() throws IOException {
try (BufferedReader reader = getReader()) {
return CharStreams.toString(reader);
}
}
default Stream<String> lines() throws IOException {
try (BufferedReader reader = getReader()) {
return reader.lines();
}
}
default <T> T json(Class<T> type) throws IOException {
try (Reader reader = new InputStreamReader(getResponse().getEntity().getContent())) {
return SkinServer.gson.fromJson(reader, type);
}
}
default <T> T json(Type type) throws IOException {
try (Reader reader = new InputStreamReader(getResponse().getEntity().getContent())) {
return SkinServer.gson.fromJson(reader, type);
}
}
@Override
default void close() throws IOException {
this.getResponse().close();
}
static MoreHttpResponses execute(CloseableHttpClient client, HttpUriRequest request) throws IOException {
CloseableHttpResponse response = client.execute(request);
return () -> response;
}
}

View file

@ -1,21 +1,16 @@
package com.voxelmodpack.hdskins.skins; package com.voxelmodpack.hdskins.skins;
import java.io.BufferedReader; import com.voxelmodpack.hdskins.HDSkinManager;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.util.Map;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpEntity; import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder; import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.entity.ContentType; import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.MultipartEntityBuilder; import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.impl.client.HttpClients;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.util.Map;
/** /**
* Ew. Why so many builders? >.< * Ew. Why so many builders? >.<
@ -26,8 +21,6 @@ public class NetClient {
private Map<String, ?> headers; private Map<String, ?> headers;
private CloseableHttpResponse response = null;
public NetClient(String method, String uri) { public NetClient(String method, String uri) {
rqBuilder = RequestBuilder.create(method).setUri(uri); rqBuilder = RequestBuilder.create(method).setUri(uri);
} }
@ -47,56 +40,14 @@ public class NetClient {
return this; return this;
} }
public boolean send() { public MoreHttpResponses send() throws IOException {
HttpUriRequest request = rqBuilder.build(); HttpUriRequest request = rqBuilder.build();
for (Map.Entry<String, ?> parameter : headers.entrySet()) { for (Map.Entry<String, ?> parameter : headers.entrySet()) {
request.addHeader(parameter.getKey(), parameter.getValue().toString()); request.addHeader(parameter.getKey(), parameter.getValue().toString());
} }
try { return MoreHttpResponses.execute(HDSkinManager.httpClient, request);
response = HttpClients.createSystem().execute(request);
return getResponseCode() == HttpStatus.SC_OK;
} catch (IOException e) { }
return false;
} }
public int getResponseCode() {
if (response == null) {
send();
}
return response.getStatusLine().getStatusCode();
}
public String getResponseText() {
if (response == null) {
if (!send()) {
return "";
}
}
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
StringBuilder builder = new StringBuilder();
int ch;
while ((ch = reader.read()) != -1) {
builder.append((char)ch);
}
return builder.toString();
} catch (IOException e) {
} finally {
IOUtils.closeQuietly(reader);
}
return "";
}
} }

View file

@ -5,28 +5,22 @@ import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.mojang.authlib.GameProfile; import com.mojang.authlib.GameProfile;
import com.mojang.authlib.exceptions.AuthenticationException; import com.mojang.authlib.exceptions.AuthenticationException;
import com.mojang.authlib.minecraft.MinecraftProfileTexture;
import com.mojang.authlib.minecraft.MinecraftSessionService; import com.mojang.authlib.minecraft.MinecraftSessionService;
import com.mojang.authlib.yggdrasil.response.MinecraftTexturesPayload; import com.mojang.authlib.yggdrasil.response.MinecraftTexturesPayload;
import com.mojang.util.UUIDTypeAdapter; import com.mojang.util.UUIDTypeAdapter;
import com.mumfrey.liteloader.modconfig.Exposable; import com.mumfrey.liteloader.modconfig.Exposable;
import com.voxelmodpack.hdskins.HDSkinManager;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.util.Session; import net.minecraft.util.Session;
import java.net.URI; import java.io.IOException;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import javax.annotation.Nullable;
public interface SkinServer extends Exposable { public interface SkinServer extends Exposable {
static final Gson gson = new GsonBuilder() Gson gson = new GsonBuilder()
.registerTypeAdapter(UUID.class, new UUIDTypeAdapter()) .registerTypeAdapter(UUID.class, new UUIDTypeAdapter())
.create(); .create();
@ -34,16 +28,15 @@ public interface SkinServer extends Exposable {
"http://skins.voxelmodpack.com", "http://skins.voxelmodpack.com",
"http://skinmanager.voxelmodpack.com")); "http://skinmanager.voxelmodpack.com"));
Optional<MinecraftTexturesPayload> loadProfileData(GameProfile profile); MinecraftTexturesPayload loadProfileData(GameProfile profile) throws IOException;
default Map<MinecraftProfileTexture.Type, MinecraftProfileTexture> getPreviewTextures(GameProfile profile) { CompletableFuture<SkinUploadResponse> uploadSkin(Session session, SkinUpload upload);
return loadProfileData(profile).map(MinecraftTexturesPayload::getTextures).orElse(Collections.emptyMap());
default CompletableFuture<MinecraftTexturesPayload> getPreviewTextures(GameProfile profile) {
return CallableFutures.asyncFailableFuture(() -> loadProfileData(profile), HDSkinManager.skinDownloadExecutor);
} }
CompletableFuture<SkinUploadResponse> uploadSkin(Session session, @Nullable URI image, MinecraftProfileTexture.Type type, Map<String, String> metadata); static void verifyServerConnection(Session session, String serverId) throws AuthenticationException {
public static void verifyServerConnection(Session session, String serverId) throws AuthenticationException {
MinecraftSessionService service = Minecraft.getMinecraft().getSessionService(); MinecraftSessionService service = Minecraft.getMinecraft().getSessionService();
service.joinServer(session.getProfile(), session.getToken(), serverId); service.joinServer(session.getProfile(), session.getToken(), serverId);
} }

View file

@ -0,0 +1,34 @@
package com.voxelmodpack.hdskins.skins;
import com.mojang.authlib.minecraft.MinecraftProfileTexture;
import java.net.URI;
import java.util.Map;
import javax.annotation.Nullable;
public class SkinUpload {
private URI image;
private Map<String, String> metadata;
private MinecraftProfileTexture.Type type;
public SkinUpload(MinecraftProfileTexture.Type type, @Nullable URI image, Map<String, String> metadata) {
this.image = image;
this.metadata = metadata;
this.type = type;
}
@Nullable
public URI getImage() {
return image;
}
public Map<String, String> getMetadata() {
return metadata;
}
public MinecraftProfileTexture.Type getType() {
return type;
}
}

View file

@ -13,28 +13,19 @@ import net.minecraft.client.Minecraft;
import net.minecraft.util.Session; import net.minecraft.util.Session;
import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.http.HttpHeaders; import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair; import org.apache.http.NameValuePair;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder; import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.entity.ContentType; import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.MultipartEntityBuilder; import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair; import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URI; import java.net.URI;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@ -53,83 +44,81 @@ public class ValhallaSkinServer implements SkinServer {
} }
@Override @Override
public Optional<MinecraftTexturesPayload> loadProfileData(GameProfile profile) { public MinecraftTexturesPayload loadProfileData(GameProfile profile) throws IOException {
try (CloseableHttpClient client = HttpClients.createSystem(); try (MoreHttpResponses response = MoreHttpResponses.execute(HDSkinManager.httpClient, new HttpGet(getTexturesURI(profile)))) {
CloseableHttpResponse response = client.execute(new HttpGet(getTexturesURI(profile)))) {
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { if (response.ok()) {
return readJson(response, MinecraftTexturesPayload.class);
return Optional.of(readJson(response, MinecraftTexturesPayload.class));
} }
} catch (IOException e) { throw new IOException("Server sent non-ok response code: " + response.getResponseCode());
e.printStackTrace();
} }
return Optional.empty();
} }
@Override @Override
public CompletableFuture<SkinUploadResponse> uploadSkin(Session session, @Nullable URI image, MinecraftProfileTexture.Type type, Map<String, String> metadata) { public CompletableFuture<SkinUploadResponse> uploadSkin(Session session, SkinUpload skin) {
URI image = skin.getImage();
Map<String, String> metadata = skin.getMetadata();
MinecraftProfileTexture.Type type = skin.getType();
return CallableFutures.asyncFailableFuture(() -> { return CallableFutures.asyncFailableFuture(() -> {
try (CloseableHttpClient client = HttpClients.createSystem()) { authorize(session);
authorize(client, session);
try { try {
return upload(client, session, image, type, metadata); return upload(session, image, type, metadata);
} catch (IOException e) { } catch (IOException e) {
if (e.getMessage().equals("Authorization failed")) { if (e.getMessage().equals("Authorization failed")) {
accessToken = null; accessToken = null;
authorize(client, session); authorize(session);
return upload(client, session, image, type, metadata); return upload(session, image, type, metadata);
} }
throw e; throw e;
} }
}
}, HDSkinManager.skinUploadExecutor); }, HDSkinManager.skinUploadExecutor);
} }
private SkinUploadResponse upload(CloseableHttpClient client, Session session, @Nullable URI image, private SkinUploadResponse upload(Session session, @Nullable URI image,
MinecraftProfileTexture.Type type, Map<String, String> metadata) MinecraftProfileTexture.Type type, Map<String, String> metadata)
throws IOException { throws IOException {
GameProfile profile = session.getProfile(); GameProfile profile = session.getProfile();
if (image == null) { if (image == null) {
return resetSkin(client, profile, type); return resetSkin(profile, type);
} }
switch (image.getScheme()) { switch (image.getScheme()) {
case "file": case "file":
return uploadFile(client, new File(image), profile, type, metadata); return uploadFile(new File(image), profile, type, metadata);
case "http": case "http":
case "https": case "https":
return uploadUrl(client, image, profile, type, metadata); return uploadUrl(image, profile, type, metadata);
default: default:
throw new IOException("Unsupported URI scheme: " + image.getScheme()); throw new IOException("Unsupported URI scheme: " + image.getScheme());
} }
} }
private SkinUploadResponse resetSkin(CloseableHttpClient client, GameProfile profile, MinecraftProfileTexture.Type type) throws IOException { private SkinUploadResponse resetSkin(GameProfile profile, MinecraftProfileTexture.Type type) throws IOException {
return upload(client, RequestBuilder.delete() return upload(RequestBuilder.delete()
.setUri(buildUserTextureUri(profile, type)) .setUri(buildUserTextureUri(profile, type))
.addHeader(HttpHeaders.AUTHORIZATION, this.accessToken) .addHeader(HttpHeaders.AUTHORIZATION, this.accessToken)
.build()); .build());
} }
private SkinUploadResponse uploadFile(CloseableHttpClient client, File file, GameProfile profile, MinecraftProfileTexture.Type type, Map<String, String> metadata) throws IOException { private SkinUploadResponse uploadFile(File file, GameProfile profile, MinecraftProfileTexture.Type type, Map<String, String> metadata) throws IOException {
MultipartEntityBuilder b = MultipartEntityBuilder.create(); MultipartEntityBuilder b = MultipartEntityBuilder.create();
b.addBinaryBody("file", file, ContentType.create("image/png"), file.getName()); b.addBinaryBody("file", file, ContentType.create("image/png"), file.getName());
metadata.forEach(b::addTextBody); metadata.forEach(b::addTextBody);
return upload(client, RequestBuilder.put() return upload(RequestBuilder.put()
.setUri(buildUserTextureUri(profile, type)) .setUri(buildUserTextureUri(profile, type))
.addHeader(HttpHeaders.AUTHORIZATION, this.accessToken) .addHeader(HttpHeaders.AUTHORIZATION, this.accessToken)
.setEntity(b.build()) .setEntity(b.build())
.build()); .build());
} }
private SkinUploadResponse uploadUrl(CloseableHttpClient client, URI uri, GameProfile profile, MinecraftProfileTexture.Type type, Map<String, String> metadata) throws IOException { private SkinUploadResponse uploadUrl(URI uri, GameProfile profile, MinecraftProfileTexture.Type type, Map<String, String> metadata) throws IOException {
return upload(client, RequestBuilder.post() return upload(RequestBuilder.post()
.setUri(buildUserTextureUri(profile, type)) .setUri(buildUserTextureUri(profile, type))
.addHeader(HttpHeaders.AUTHORIZATION, this.accessToken) .addHeader(HttpHeaders.AUTHORIZATION, this.accessToken)
.addParameter("file", uri.toString()) .addParameter("file", uri.toString())
@ -139,20 +128,19 @@ public class ValhallaSkinServer implements SkinServer {
.build()); .build());
} }
private SkinUploadResponse upload(CloseableHttpClient client, HttpUriRequest request) throws IOException { private SkinUploadResponse upload(HttpUriRequest request) throws IOException {
try (CloseableHttpResponse response = client.execute(request)) { try (MoreHttpResponses response = MoreHttpResponses.execute(HDSkinManager.httpClient, request)) {
return readJson(response, SkinUploadResponse.class); return readJson(response, SkinUploadResponse.class);
} }
} }
private void authorize(Session session) throws IOException, AuthenticationException {
private void authorize(CloseableHttpClient client, Session session) throws IOException, AuthenticationException {
if (this.accessToken != null) { if (this.accessToken != null) {
return; return;
} }
GameProfile profile = session.getProfile(); GameProfile profile = session.getProfile();
String token = session.getToken(); String token = session.getToken();
AuthHandshake handshake = authHandshake(client, profile.getName()); AuthHandshake handshake = authHandshake(profile.getName());
if (handshake.offline) { if (handshake.offline) {
return; return;
@ -161,33 +149,27 @@ public class ValhallaSkinServer implements SkinServer {
// join the session server // join the session server
Minecraft.getMinecraft().getSessionService().joinServer(profile, token, handshake.serverId); Minecraft.getMinecraft().getSessionService().joinServer(profile, token, handshake.serverId);
AuthResponse response = authResponse(client, profile.getName(), handshake.verifyToken); AuthResponse response = authResponse(profile.getName(), handshake.verifyToken);
if (!response.userId.equals(profile.getId())) { if (!response.userId.equals(profile.getId())) {
throw new IOException("UUID mismatch!"); // probably won't ever throw throw new IOException("UUID mismatch!"); // probably won't ever throw
} }
this.accessToken = response.accessToken; this.accessToken = response.accessToken;
} }
private <T> T readJson(HttpResponse resp, Class<T> cl) throws IOException { private <T> T readJson(MoreHttpResponses resp, Class<T> cl) throws IOException {
String type = resp.getEntity().getContentType().getValue(); String type = resp.getResponse().getEntity().getContentType().getValue();
if (!"application/json".equals(type)) { if (!"application/json".equals(type)) {
try { throw new IOException("Server returned a non-json response!");
throw new IOException("Server returned a non-json response!");
} finally {
EntityUtils.consumeQuietly(resp.getEntity());
}
} }
try (Reader r = new InputStreamReader(resp.getEntity().getContent())) { if (resp.ok()) {
if (resp.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { return resp.json(cl);
// TODO specific error handling
throw new IOException(gson.fromJson(r, JsonObject.class).get("message").getAsString());
}
return gson.fromJson(r, cl);
} }
throw new IOException(resp.json(JsonObject.class).get("message").getAsString());
} }
private AuthHandshake authHandshake(CloseableHttpClient client, String name) throws IOException { private AuthHandshake authHandshake(String name) throws IOException {
try (CloseableHttpResponse resp = client.execute(RequestBuilder.post() try (MoreHttpResponses resp = MoreHttpResponses.execute(HDSkinManager.httpClient, RequestBuilder.post()
.setUri(getHandshakeURI()) .setUri(getHandshakeURI())
.addParameter("name", name) .addParameter("name", name)
.build())) { .build())) {
@ -195,8 +177,8 @@ public class ValhallaSkinServer implements SkinServer {
} }
} }
private AuthResponse authResponse(CloseableHttpClient client, String name, long verifyToken) throws IOException { private AuthResponse authResponse(String name, long verifyToken) throws IOException {
try (CloseableHttpResponse resp = client.execute(RequestBuilder.post() try (MoreHttpResponses resp = MoreHttpResponses.execute(HDSkinManager.httpClient, RequestBuilder.post()
.setUri(getResponseURI()) .setUri(getResponseURI())
.addParameter("name", name) .addParameter("name", name)
.addParameter("verifyToken", String.valueOf(verifyToken)) .addParameter("verifyToken", String.valueOf(verifyToken))