Backport skin server changes and remove the defaults for old legacy skin servers

This commit is contained in:
Sollace 2022-11-11 23:40:39 +01:00
parent 1cddac4ea4
commit 1e55a7a14a
14 changed files with 455 additions and 177 deletions

View file

@ -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<ISkinCacheClearListener> clearListeners = Lists.newArrayList();
private BiMap<String, Class<? extends SkinServer>> skinServerTypes = HashBiMap.create(2);
private List<SkinServer> skinServers = Lists.newArrayList();
private LoadingCache<GameProfile, CompletableFuture<Map<Type, MinecraftProfileTexture>>> skins = CacheBuilder.newBuilder()
@ -103,11 +97,6 @@ public final class HDSkinManager implements IResourceManagerReloadListener {
private Function<List<SkinServer>, GuiSkins> skinsGuiFunc = GuiSkins::new;
private HDSkinManager() {
// register default skin server types
addSkinServerType(LegacySkinServer.class);
addSkinServerType(ValhallaSkinServer.class);
addSkinServerType(BethlehemSkinServer.class);
}
public void setSkinsGui(Function<List<SkinServer>, 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<? extends SkinServer> 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<? extends SkinServer> getSkinServerClass(String type) {
return this.skinServerTypes.get(type);
}
void addSkinServer(SkinServer skinServer) {
this.skinServers.add(skinServer);
}

View file

@ -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<SkinServer> skin_servers = SkinServer.defaultServers;
public List<SkinServer> 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() {

View file

@ -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<String, String> skinMetadata = new HashMap<String, String>();
private Map<String, String> 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 <T> Iterator<T> cycle(List<T> list, Predicate<T> filter) {
return Iterables.cycle(Iterables.filter(list, filter::test)).iterator();
}
public SkinUploader(List<SkinServer> 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<MoreHttpResponses> 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<PreviewTextureManager> 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 {

View file

@ -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.

View file

@ -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());
}
}

View file

@ -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<MinecraftTexturesPayload> 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<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));
}
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 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) {

View file

@ -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<SkinServer> 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<SkinUploadResponse> 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<MinecraftTexturesPayload> 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);
}
/**

View file

@ -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<SkinServer>, JsonDeserializer<SkinServer> {
public static final SkinServerSerializer instance = new SkinServerSerializer();
private final BiMap<String, Class<? extends SkinServer>> types = HashBiMap.create(2);
public SkinServerSerializer() {
// register default skin server types
addSkinServerType(ValhallaSkinServer.class);
addSkinServerType(YggdrasilSkinServer.class);
addSkinServerType(LegacySkinServer.class);
}
public void addSkinServerType(Class<? extends SkinServer> 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<SkinServer>, 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));
}
}

View file

@ -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<String, MinecraftProfileTexture> textures;
TexturePayload() { }
public TexturePayload(GameProfile profile, Map<String, MinecraftProfileTexture> 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<String, MinecraftProfileTexture> getTextures() {
return textures;
}
}

View file

@ -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

View file

@ -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<Feature> 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<MinecraftProfileTexture.Type, MinecraftProfileTexture> 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<String, String> mapMetadata(Map<String, String> 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);
}
}
}

View file

@ -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> T json(Class<T> type, String errorMessage) throws IOException {
return json((Type)type, errorMessage);
}
default <T> 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> 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

View file

@ -0,0 +1,11 @@
{
"overwrite": false,
"insert": "END",
"servers": [
{
"type": "valhalla",
"address": "https://skins.minelittlepony-mod.com"
},
{ "type": "mojang" }
]
}

View file

@ -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));
}
/**