Add Valhalla implementation

This commit is contained in:
Matthew Messinger 2018-07-08 03:35:11 -04:00
parent 89ecc9f916
commit 77b8357f03
4 changed files with 207 additions and 10 deletions

View file

@ -39,11 +39,18 @@ sourceSets {
ext.refMap = 'hdskins.mixin.refmap.json' ext.refMap = 'hdskins.mixin.refmap.json'
} }
main { main {
compileClasspath += hdskins.output compileClasspath += hdskins.output + hdskins.compileClasspath
ext.refMap = 'minelp.mixin.refmap.json' ext.refMap = 'minelp.mixin.refmap.json'
} }
} }
dependencies {
// use the same version as httpclient
hdskinsCompile('org.apache.httpcomponents:httpmime:4.3.2'){
transitive = false
}
}
litemod.json { litemod.json {
mcversion = '1.12.r2' mcversion = '1.12.r2'
author = 'Verdana, Rene_Z, Mumfrey, Killjoy1221' author = 'Verdana, Rene_Z, Mumfrey, Killjoy1221'
@ -64,6 +71,9 @@ litemod.json {
jar { jar {
from sourceSets.hdskins.output from sourceSets.hdskins.output
from litemod from litemod
// TODO relocate. LiteLoader excludes apache libs from classloading
from {configurations.hdskinsCompile.collect{it.isDirectory() ? it : zipTree(it)}}
} }
sourceJar { sourceJar {
// add hdskins sources // add hdskins sources

View file

@ -223,7 +223,6 @@ public final class HDSkinManager implements IResourceManagerReloadListener {
this.enabled = enabled; this.enabled = enabled;
} }
@Nullable
public static PreviewTexture getPreviewTexture(ResourceLocation skinResource, GameProfile profile, Type type, ResourceLocation def, @Nullable final SkinAvailableCallback callback) { public static PreviewTexture getPreviewTexture(ResourceLocation skinResource, GameProfile profile, Type type, ResourceLocation def, @Nullable final SkinAvailableCallback callback) {
TextureManager textureManager = Minecraft.getMinecraft().getTextureManager(); TextureManager textureManager = Minecraft.getMinecraft().getTextureManager();
MinecraftProfileTexture url = INSTANCE.getGatewayServer().getPreviewTexture(type, profile).orElse(null); MinecraftProfileTexture url = INSTANCE.getGatewayServer().getPreviewTexture(type, profile).orElse(null);

View file

@ -1,21 +1,55 @@
package com.voxelmodpack.hdskins.skins; package com.voxelmodpack.hdskins.skins;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.base.Preconditions;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.mojang.authlib.GameProfile; import com.mojang.authlib.GameProfile;
import com.mojang.authlib.exceptions.AuthenticationException;
import com.mojang.authlib.minecraft.MinecraftProfileTexture; import com.mojang.authlib.minecraft.MinecraftProfileTexture;
import com.mojang.authlib.yggdrasil.response.MinecraftTexturesPayload; import com.mojang.authlib.yggdrasil.response.MinecraftTexturesPayload;
import com.mojang.util.UUIDTypeAdapter;
import com.voxelmodpack.hdskins.HDSkinManager;
import net.minecraft.client.Minecraft;
import net.minecraft.util.Session; import net.minecraft.util.Session;
import org.apache.http.HttpHeaders;
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 java.nio.file.Path; import java.io.File;
import java.io.IOException;
import java.io.InputStream;
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.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import javax.annotation.Nullable; import javax.annotation.Nullable;
public class ValhallaSkinServer implements SkinServer { public class ValhallaSkinServer implements SkinServer {
@SuppressWarnings("unused") @SuppressWarnings("unused")
private final String baseURL; private final String baseURL;
private final Gson gson = new GsonBuilder()
.registerTypeAdapter(UUID.class, new UUIDTypeAdapter())
.create();
private String accessToken;
public ValhallaSkinServer(String baseURL) { public ValhallaSkinServer(String baseURL) {
this.baseURL = baseURL; this.baseURL = baseURL;
@ -23,23 +57,175 @@ public class ValhallaSkinServer implements SkinServer {
@Override @Override
public Optional<MinecraftTexturesPayload> loadProfileData(GameProfile profile) { public Optional<MinecraftTexturesPayload> loadProfileData(GameProfile profile) {
try (CloseableHttpClient client = HttpClients.createSystem();
CloseableHttpResponse response = client.execute(new HttpGet(getTexturesURI(profile)))) {
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
return Optional.of(readJson(response.getEntity().getContent(), MinecraftTexturesPayload.class));
}
} catch (IOException e) {
e.printStackTrace();
}
return Optional.empty(); return Optional.empty();
} }
@Override @Override
public Optional<MinecraftProfileTexture> getPreviewTexture(MinecraftProfileTexture.Type type, GameProfile profile) { public CompletableFuture<SkinUploadResponse> uploadSkin(Session session, @Nullable URI image,
return null; MinecraftProfileTexture.Type type, Map<String, String> metadata) {
return CallableFutures.asyncFailableFuture(() -> {
try (CloseableHttpClient client = HttpClients.createSystem()) {
authorize(client, session);
GameProfile profile = session.getProfile();
if (image == null) {
return resetSkin(client, profile, type);
}
switch (image.getScheme()) {
case "file":
return uploadFile(client, new File(image), profile, type, metadata);
case "http":
case "https":
return uploadUrl(client, image, profile, type, metadata);
default:
throw new IOException("Unsupported URI scheme: " + image.getScheme());
}
}
}, HDSkinManager.skinUploadExecutor);
} }
@Override private SkinUploadResponse resetSkin(CloseableHttpClient client, GameProfile profile, MinecraftProfileTexture.Type type) throws IOException {
public ListenableFuture<SkinUploadResponse> uploadSkin(Session session, @Nullable Path image, MinecraftProfileTexture.Type type, boolean thinArmType) { return upload(client, RequestBuilder.delete()
return null; .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 {
MultipartEntityBuilder b = MultipartEntityBuilder.create();
b.addBinaryBody("file", file, ContentType.create("image/png"), file.getName());
metadata.forEach(b::addTextBody);
return upload(client, 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 {
return upload(client, RequestBuilder.post()
.setUri(buildUserTextureUri(profile, type))
.addHeader(HttpHeaders.AUTHORIZATION, this.accessToken)
.addParameter("file", uri.toString())
.addParameters(metadata.entrySet().stream()
.map(entry -> new BasicNameValuePair(entry.getKey(), entry.getValue()))
.toArray(NameValuePair[]::new))
.build());
}
private SkinUploadResponse upload(CloseableHttpClient client, HttpUriRequest request) throws IOException {
try (CloseableHttpResponse response = client.execute(request)) {
int code = response.getStatusLine().getStatusCode();
JsonObject json = readJson(response.getEntity().getContent(), JsonObject.class);
return new SkinUploadResponse(code == HttpStatus.SC_OK, json.get("message").getAsString());
}
}
private void authorize(CloseableHttpClient client, Session session) throws IOException, AuthenticationException {
if (accessToken != null) {
return;
}
GameProfile profile = session.getProfile();
String token = session.getToken();
AuthHandshake handshake = authHandshake(client, profile.getName());
if (handshake.offline) {
return;
}
// join the session server
Minecraft.getMinecraft().getSessionService().joinServer(profile, token, handshake.serverId);
AuthResponse response = authResponse(client, 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(InputStream in, Class<T> cl) throws IOException {
try (Reader r = new InputStreamReader(in)) {
return gson.fromJson(r, cl);
}
}
private AuthHandshake authHandshake(CloseableHttpClient client, String name) throws IOException {
try (CloseableHttpResponse resp = client.execute(RequestBuilder.post()
.setUri(getHandshakeURI())
.addParameter("name", name)
.build())) {
return readJson(resp.getEntity().getContent(), AuthHandshake.class);
}
}
private AuthResponse authResponse(CloseableHttpClient client, String name, long verifyToken) throws IOException {
try (CloseableHttpResponse resp = client.execute(RequestBuilder.post()
.setUri(getResponseURI())
.addParameter("name", name)
.addParameter("verifyToken", String.valueOf(verifyToken))
.build())) {
return readJson(resp.getEntity().getContent(), AuthResponse.class);
}
}
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.baseURL, 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.baseURL, UUIDTypeAdapter.fromUUID(profile.getId())));
}
private URI getHandshakeURI() {
return URI.create(String.format("%s/auth/handshake", this.baseURL));
}
private URI getResponseURI() {
return URI.create(String.format("%s/auth/response", this.baseURL));
} }
static ValhallaSkinServer from(String server) { static ValhallaSkinServer from(String server) {
Matcher matcher = Pattern.compile("^valhalla:(.*)$").matcher(server); Matcher matcher = Pattern.compile("^valhalla:(.*)$").matcher(server);
if (matcher.find()) if (matcher.find()) {
return new ValhallaSkinServer(matcher.group(1)); return new ValhallaSkinServer(matcher.group(1));
}
throw new IllegalArgumentException(); throw new IllegalArgumentException();
} }
@SuppressWarnings("WeakerAccess")
static class AuthHandshake {
boolean offline;
String serverId;
long verifyToken;
}
@SuppressWarnings("WeakerAccess")
static class AuthResponse {
String accessToken;
UUID userId;
}
} }

View file

@ -18,7 +18,9 @@ import javax.annotation.Nullable;
* Uploader for Multipart form data * Uploader for Multipart form data
* *
* @author Adam Mummery-Smith * @author Adam Mummery-Smith
* @deprecated Use httpmime multipart upload
*/ */
@Deprecated
public class ThreadMultipartPostUpload { public class ThreadMultipartPostUpload {
protected final Map<String, ?> sourceData; protected final Map<String, ?> sourceData;