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.Lists;
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.minecraft.MinecraftProfileTexture;
import com.mojang.authlib.minecraft.MinecraftProfileTexture.Type;
import com.mojang.authlib.properties.Property;
import com.mojang.authlib.yggdrasil.response.MinecraftTexturesPayload;
import com.mojang.util.UUIDTypeAdapter;
import com.mumfrey.liteloader.core.LiteLoader;
import com.mumfrey.liteloader.util.log.LiteLoaderLogger;
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.util.ResourceLocation;
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.image.BufferedImage;
@ -53,6 +51,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@ -62,13 +61,11 @@ import javax.annotation.Nonnull;
public final class HDSkinManager implements IResourceManagerReloadListener {
private static final ResourceLocation LOADING = new ResourceLocation("LOADING");
private static final Gson GSON = new GsonBuilder()
.registerTypeAdapter(UUID.class, new UUIDTypeAdapter())
.create();
public static final ExecutorService skinUploadExecutor = Executors.newSingleThreadExecutor();
public static final ExecutorService skinDownloadExecutor = Executors.newFixedThreadPool(8);
public static final CloseableHttpClient httpClient = HttpClients.createSystem();
private static final ExecutorService skinDownloadExecutor = Executors.newFixedThreadPool(8);
public static final ListeningExecutorService skinUploadExecutor = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
private static final ResourceLocation LOADING = new ResourceLocation("LOADING");
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);
if (textures != null) {
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) {
// name is optional
String name = texturePayload.getProfileName();
@ -216,11 +213,14 @@ public final class HDSkinManager implements IResourceManagerReloadListener {
private Map<Type, MinecraftProfileTexture> loadProfileData(GameProfile profile) {
Map<Type, MinecraftProfileTexture> textures = Maps.newEnumMap(Type.class);
for (SkinServer server : skinServers) {
Optional<MinecraftTexturesPayload> profileData = server.loadProfileData(profile);
profileData.map(MinecraftTexturesPayload::getTextures).ifPresent(it -> it.forEach(textures::putIfAbsent));
try {
server.loadProfileData(profile).getTextures().forEach(textures::putIfAbsent);
if (textures.size() == Type.values().length) {
break;
}
} catch (IOException e) {
LogManager.getLogger().trace(e);
}
}
return textures;
@ -263,8 +263,8 @@ public final class HDSkinManager implements IResourceManagerReloadListener {
this.enabled = enabled;
}
public static PreviewTextureManager getPreviewTextureManager(GameProfile profile) {
return new PreviewTextureManager(INSTANCE.getGatewayServer().getPreviewTextures(profile));
public static CompletableFuture<PreviewTextureManager> getPreviewTextureManager(GameProfile profile) {
return INSTANCE.getGatewayServer().getPreviewTextures(profile).thenApply(PreviewTextureManager::new);
}
public void addClearListener(ISkinCacheClearListener listener) {

View file

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

View file

@ -8,7 +8,6 @@ import com.voxelmodpack.hdskins.DynamicTextureImage;
import com.voxelmodpack.hdskins.HDSkinManager;
import com.voxelmodpack.hdskins.ImageBufferDownloadHD;
import com.voxelmodpack.hdskins.PreviewTexture;
import com.voxelmodpack.hdskins.PreviewTextureManager;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.texture.DynamicTexture;
import net.minecraft.client.renderer.texture.TextureManager;
@ -25,6 +24,7 @@ import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import javax.imageio.ImageIO;
@SuppressWarnings("EntityConstructor")
@ -41,11 +41,11 @@ public class EntityPlayerModel extends EntityLivingBase {
EntityEquipmentSlot.MAINHAND, ItemStack.EMPTY
));
private PreviewTexture remoteSkinTexture;
private volatile PreviewTexture remoteSkinTexture;
private ResourceLocation remoteSkinResource;
protected ResourceLocation localSkinResource;
private DynamicTexture localSkinTexture;
private PreviewTexture remoteElytraTexture;
private volatile PreviewTexture remoteElytraTexture;
private ResourceLocation remoteElytraResource;
private ResourceLocation localElytraResource;
private DynamicTexture localElytraTexture;
@ -77,10 +77,11 @@ public class EntityPlayerModel extends EntityLivingBase {
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);
});
}

View file

@ -12,6 +12,7 @@ import com.mojang.authlib.minecraft.MinecraftProfileTexture;
import com.mumfrey.liteloader.util.log.LiteLoaderLogger;
import com.voxelmodpack.hdskins.HDSkinManager;
import com.voxelmodpack.hdskins.skins.SkinServer;
import com.voxelmodpack.hdskins.skins.SkinUpload;
import com.voxelmodpack.hdskins.skins.SkinUploadResponse;
import com.voxelmodpack.hdskins.upload.awt.ThreadOpenFilePNG;
import net.minecraft.client.Minecraft;
@ -567,7 +568,7 @@ public class GuiSkins extends GuiScreen {
this.uploadingSkin = true;
this.skinUploadMessage = I18n.format("hdskins.request");
HDSkinManager.INSTANCE.getGatewayServer()
.uploadSkin(session, null, this.textureType, getMetadata())
.uploadSkin(session, new SkinUpload(this.textureType, null, getMetadata()))
.thenAccept(this::onUploadComplete)
.exceptionally(this::onFailure);
}
@ -577,7 +578,7 @@ public class GuiSkins extends GuiScreen {
this.skinUploadMessage = I18n.format("hdskins.upload");
URI path = skinFile == null ? null : skinFile.toURI();
HDSkinManager.INSTANCE.getGatewayServer()
.uploadSkin(session, path, this.textureType, getMetadata())
.uploadSkin(session, new SkinUpload(this.textureType, path, getMetadata()))
.thenAccept(this::onUploadComplete)
.exceptionally(this::onFailure);
}
@ -591,7 +592,6 @@ public class GuiSkins extends GuiScreen {
this.btnUpload.enabled = true;
}
private Void onFailure(Throwable t) {
t = Throwables.getRootCause(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.annotations.Expose;
import com.mojang.authlib.GameProfile;
import com.mojang.authlib.minecraft.MinecraftProfileTexture;
import com.mojang.authlib.minecraft.MinecraftProfileTexture.Type;
import com.mojang.authlib.yggdrasil.response.MinecraftTexturesPayload;
import com.mojang.util.UUIDTypeAdapter;
@ -16,9 +17,10 @@ import java.io.IOException;
import java.net.URI;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import javax.annotation.Nullable;
@ServerType("bethlehem")
public class BethlehemSkinServer implements SkinServer {
@ -32,24 +34,24 @@ public class BethlehemSkinServer implements SkinServer {
}
@Override
public Optional<MinecraftTexturesPayload> loadProfileData(GameProfile profile) {
NetClient client = new NetClient("GET", getPath(profile));
public MinecraftTexturesPayload loadProfileData(GameProfile profile) throws IOException {
try (MoreHttpResponses response = new NetClient("GET", getPath(profile)).send()) {
String json = client.getResponseText();
JsonObject s = gson.fromJson(json, JsonObject.class);
JsonObject s = response.json(JsonObject.class);
if (s.has("success") && s.get("success").getAsBoolean()) {
s = s.get("data").getAsJsonObject();
return Optional.ofNullable(gson.fromJson(s, MinecraftTexturesPayload.class));
return gson.fromJson(s, MinecraftTexturesPayload.class);
}
throw new IOException(s.get("error").getAsString());
}
return Optional.empty();
}
@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(() -> {
SkinServer.verifyServerConnection(session, SERVER_ID);
@ -59,15 +61,17 @@ public class BethlehemSkinServer implements SkinServer {
client.putFile(type.toString().toLowerCase(Locale.US), "image/png", image);
}
if (!client.send()) {
throw new IOException(client.getResponseText());
try (MoreHttpResponses response = client.send()) {
if (!response.ok()) {
throw new IOException(response.text());
}
return new SkinUploadResponse(response.text());
}
return new SkinUploadResponse(client.getResponseText());
}, 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()
.put("accessToken", session.getToken())
.put("user", session.getUsername())

View file

@ -18,11 +18,9 @@ import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.util.Collections;
import java.util.EnumMap;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import javax.annotation.Nullable;
@ -45,30 +43,25 @@ public class LegacySkinServer implements SkinServer {
}
@Override
public Map<MinecraftProfileTexture.Type, MinecraftProfileTexture> getPreviewTextures(GameProfile profile) {
public CompletableFuture<MinecraftTexturesPayload> getPreviewTextures(GameProfile profile) {
if (Strings.isNullOrEmpty(this.gateway)) {
return Collections.emptyMap();
return CallableFutures.failedFuture(gatewayUnsupported());
}
Map<MinecraftProfileTexture.Type, MinecraftProfileTexture> map = new EnumMap<>(MinecraftProfileTexture.Type.class);
for (MinecraftProfileTexture.Type type : MinecraftProfileTexture.Type.values()) {
map.put(type, new MinecraftProfileTexture(getPath(gateway, type, profile), null));
}
return map;
return CompletableFuture.completedFuture(TexturesPayloadBuilder.createTexturesPayload(profile, map));
}
@Override
public Optional<MinecraftTexturesPayload> loadProfileData(GameProfile profile) {
public MinecraftTexturesPayload loadProfileData(GameProfile profile) throws IOException {
ImmutableMap.Builder<MinecraftProfileTexture.Type, MinecraftProfileTexture> builder = ImmutableMap.builder();
for (MinecraftProfileTexture.Type type : MinecraftProfileTexture.Type.values()) {
String url = getPath(this.address, type, profile);
try {
HttpURLConnection urlConnection = (HttpURLConnection) new URL(url).openConnection();
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);
builder.put(type, loadProfileTexture(profile, url));
} catch (IOException 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();
if (map.isEmpty()) {
logger.debug("No textures found for {} at {}", profile, this.address);
return Optional.empty();
throw new IOException(String.format("No textures found for %s at %s", profile, this.address));
}
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")
@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)) {
return CallableFutures.failedFuture(new NullPointerException("gateway url is blank"));
return CallableFutures.failedFuture(gatewayUnsupported());
}
return CallableFutures.asyncFailableFuture(() -> {
URI image = skin.getImage();
MinecraftProfileTexture.Type type = skin.getType();
Map<String, String> metadata = skin.getMetadata();
SkinServer.verifyServerConnection(session, SERVER_ID);
String model = metadata.getOrDefault("model", "default");
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: ")) {
response = response.substring(7);
}
if (!response.equalsIgnoreCase("OK") && !response.endsWith("OK"))
if (!response.equalsIgnoreCase("OK") && !response.endsWith("OK")) {
throw new IOException(response);
}
return new SkinUploadResponse(response);
}, 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) {
return ImmutableMap.of(
"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;
import java.io.BufferedReader;
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 com.voxelmodpack.hdskins.HDSkinManager;
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.RequestBuilder;
import org.apache.http.entity.ContentType;
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? >.<
@ -26,8 +21,6 @@ public class NetClient {
private Map<String, ?> headers;
private CloseableHttpResponse response = null;
public NetClient(String method, String uri) {
rqBuilder = RequestBuilder.create(method).setUri(uri);
}
@ -47,56 +40,14 @@ public class NetClient {
return this;
}
public boolean send() {
public MoreHttpResponses send() throws IOException {
HttpUriRequest request = rqBuilder.build();
for (Map.Entry<String, ?> parameter : headers.entrySet()) {
request.addHeader(parameter.getKey(), parameter.getValue().toString());
}
try {
response = HttpClients.createSystem().execute(request);
return getResponseCode() == HttpStatus.SC_OK;
} catch (IOException e) { }
return false;
return MoreHttpResponses.execute(HDSkinManager.httpClient, request);
}
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.mojang.authlib.GameProfile;
import com.mojang.authlib.exceptions.AuthenticationException;
import com.mojang.authlib.minecraft.MinecraftProfileTexture;
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 net.minecraft.client.Minecraft;
import net.minecraft.util.Session;
import java.net.URI;
import java.util.Collections;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import javax.annotation.Nullable;
public interface SkinServer extends Exposable {
static final Gson gson = new GsonBuilder()
Gson gson = new GsonBuilder()
.registerTypeAdapter(UUID.class, new UUIDTypeAdapter())
.create();
@ -34,16 +28,15 @@ public interface SkinServer extends Exposable {
"http://skins.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) {
return loadProfileData(profile).map(MinecraftTexturesPayload::getTextures).orElse(Collections.emptyMap());
CompletableFuture<SkinUploadResponse> uploadSkin(Session session, SkinUpload upload);
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);
public static void verifyServerConnection(Session session, String serverId) throws AuthenticationException {
static void verifyServerConnection(Session session, String serverId) throws AuthenticationException {
MinecraftSessionService service = Minecraft.getMinecraft().getSessionService();
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 org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
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.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.entity.ContentType;
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.util.EntityUtils;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URI;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@ -53,83 +44,81 @@ public class ValhallaSkinServer implements SkinServer {
}
@Override
public Optional<MinecraftTexturesPayload> loadProfileData(GameProfile profile) {
public MinecraftTexturesPayload loadProfileData(GameProfile profile) throws IOException {
try (CloseableHttpClient client = HttpClients.createSystem();
CloseableHttpResponse response = client.execute(new HttpGet(getTexturesURI(profile)))) {
try (MoreHttpResponses response = MoreHttpResponses.execute(HDSkinManager.httpClient, new HttpGet(getTexturesURI(profile)))) {
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
return Optional.of(readJson(response, MinecraftTexturesPayload.class));
if (response.ok()) {
return readJson(response, MinecraftTexturesPayload.class);
}
} catch (IOException e) {
e.printStackTrace();
throw new IOException("Server sent non-ok response code: " + response.getResponseCode());
}
return Optional.empty();
}
@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(() -> {
try (CloseableHttpClient client = HttpClients.createSystem()) {
authorize(client, session);
authorize(session);
try {
return upload(client, session, image, type, metadata);
return upload(session, image, type, metadata);
} catch (IOException e) {
if (e.getMessage().equals("Authorization failed")) {
accessToken = null;
authorize(client, session);
return upload(client, session, image, type, metadata);
authorize(session);
return upload(session, image, type, metadata);
}
throw e;
}
}
}, 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)
throws IOException {
GameProfile profile = session.getProfile();
if (image == null) {
return resetSkin(client, profile, type);
return resetSkin(profile, type);
}
switch (image.getScheme()) {
case "file":
return uploadFile(client, new File(image), profile, type, metadata);
return uploadFile(new File(image), profile, type, metadata);
case "http":
case "https":
return uploadUrl(client, image, profile, type, metadata);
return uploadUrl(image, profile, type, metadata);
default:
throw new IOException("Unsupported URI scheme: " + image.getScheme());
}
}
private SkinUploadResponse resetSkin(CloseableHttpClient client, GameProfile profile, MinecraftProfileTexture.Type type) throws IOException {
return upload(client, RequestBuilder.delete()
private SkinUploadResponse resetSkin(GameProfile profile, MinecraftProfileTexture.Type type) throws IOException {
return upload(RequestBuilder.delete()
.setUri(buildUserTextureUri(profile, type))
.addHeader(HttpHeaders.AUTHORIZATION, this.accessToken)
.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();
b.addBinaryBody("file", file, ContentType.create("image/png"), file.getName());
metadata.forEach(b::addTextBody);
return upload(client, RequestBuilder.put()
return upload(RequestBuilder.put()
.setUri(buildUserTextureUri(profile, type))
.addHeader(HttpHeaders.AUTHORIZATION, this.accessToken)
.setEntity(b.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))
.addHeader(HttpHeaders.AUTHORIZATION, this.accessToken)
.addParameter("file", uri.toString())
@ -139,20 +128,19 @@ public class ValhallaSkinServer implements SkinServer {
.build());
}
private SkinUploadResponse upload(CloseableHttpClient client, HttpUriRequest request) throws IOException {
try (CloseableHttpResponse response = client.execute(request)) {
private SkinUploadResponse upload(HttpUriRequest request) throws IOException {
try (MoreHttpResponses response = MoreHttpResponses.execute(HDSkinManager.httpClient, request)) {
return readJson(response, SkinUploadResponse.class);
}
}
private void authorize(CloseableHttpClient client, Session session) throws IOException, AuthenticationException {
private void authorize(Session session) throws IOException, AuthenticationException {
if (this.accessToken != null) {
return;
}
GameProfile profile = session.getProfile();
String token = session.getToken();
AuthHandshake handshake = authHandshake(client, profile.getName());
AuthHandshake handshake = authHandshake(profile.getName());
if (handshake.offline) {
return;
@ -161,33 +149,27 @@ public class ValhallaSkinServer implements SkinServer {
// join the session server
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())) {
throw new IOException("UUID mismatch!"); // probably won't ever throw
}
this.accessToken = response.accessToken;
}
private <T> T readJson(HttpResponse resp, Class<T> cl) throws IOException {
String type = resp.getEntity().getContentType().getValue();
private <T> T readJson(MoreHttpResponses resp, Class<T> cl) throws IOException {
String type = resp.getResponse().getEntity().getContentType().getValue();
if (!"application/json".equals(type)) {
try {
throw new IOException("Server returned a non-json response!");
} finally {
EntityUtils.consumeQuietly(resp.getEntity());
}
if (resp.ok()) {
return resp.json(cl);
}
try (Reader r = new InputStreamReader(resp.getEntity().getContent())) {
if (resp.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
// 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 {
try (CloseableHttpResponse resp = client.execute(RequestBuilder.post()
private AuthHandshake authHandshake(String name) throws IOException {
try (MoreHttpResponses resp = MoreHttpResponses.execute(HDSkinManager.httpClient, RequestBuilder.post()
.setUri(getHandshakeURI())
.addParameter("name", name)
.build())) {
@ -195,8 +177,8 @@ public class ValhallaSkinServer implements SkinServer {
}
}
private AuthResponse authResponse(CloseableHttpClient client, String name, long verifyToken) throws IOException {
try (CloseableHttpResponse resp = client.execute(RequestBuilder.post()
private AuthResponse authResponse(String name, long verifyToken) throws IOException {
try (MoreHttpResponses resp = MoreHttpResponses.execute(HDSkinManager.httpClient, RequestBuilder.post()
.setUri(getResponseURI())
.addParameter("name", name)
.addParameter("verifyToken", String.valueOf(verifyToken))