Add YggdrasilClient

This commit is contained in:
yushijinhun 2018-12-30 14:25:12 +08:00
parent 5531c8a06e
commit 3572ff8eb5
No known key found for this signature in database
GPG key ID: 5BC167F73EA558E4
7 changed files with 214 additions and 62 deletions

View file

@ -1,19 +1,13 @@
package moe.yushi.authlibinjector.httpd; package moe.yushi.authlibinjector.httpd;
import static fi.iki.elonen.NanoHTTPD.newFixedLengthResponse; import static fi.iki.elonen.NanoHTTPD.newFixedLengthResponse;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.singleton;
import static java.util.Optional.empty; import static java.util.Optional.empty;
import static java.util.Optional.of; import static java.util.Optional.of;
import static java.util.Optional.ofNullable; import static java.util.Optional.ofNullable;
import static moe.yushi.authlibinjector.util.IOUtils.CONTENT_TYPE_JSON;
import static moe.yushi.authlibinjector.util.IOUtils.asString; import static moe.yushi.authlibinjector.util.IOUtils.asString;
import static moe.yushi.authlibinjector.util.IOUtils.getURL; import static moe.yushi.authlibinjector.util.IOUtils.getURL;
import static moe.yushi.authlibinjector.util.IOUtils.newUncheckedIOException; import static moe.yushi.authlibinjector.util.IOUtils.newUncheckedIOException;
import static moe.yushi.authlibinjector.util.IOUtils.postURL;
import static moe.yushi.authlibinjector.util.JsonUtils.asJsonArray;
import static moe.yushi.authlibinjector.util.JsonUtils.asJsonObject; import static moe.yushi.authlibinjector.util.JsonUtils.asJsonObject;
import static moe.yushi.authlibinjector.util.JsonUtils.asJsonString;
import static moe.yushi.authlibinjector.util.JsonUtils.parseJson; import static moe.yushi.authlibinjector.util.JsonUtils.parseJson;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
@ -21,7 +15,6 @@ import java.io.IOException;
import java.io.UncheckedIOException; import java.io.UncheckedIOException;
import java.util.Base64; import java.util.Base64;
import java.util.Optional; import java.util.Optional;
import java.util.logging.Level;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -29,19 +22,20 @@ import fi.iki.elonen.NanoHTTPD.IHTTPSession;
import fi.iki.elonen.NanoHTTPD.Response; import fi.iki.elonen.NanoHTTPD.Response;
import fi.iki.elonen.NanoHTTPD.Response.Status; import fi.iki.elonen.NanoHTTPD.Response.Status;
import moe.yushi.authlibinjector.YggdrasilConfiguration; import moe.yushi.authlibinjector.YggdrasilConfiguration;
import moe.yushi.authlibinjector.internal.org.json.simple.JSONArray;
import moe.yushi.authlibinjector.internal.org.json.simple.JSONObject; import moe.yushi.authlibinjector.internal.org.json.simple.JSONObject;
import moe.yushi.authlibinjector.util.JsonUtils; import moe.yushi.authlibinjector.util.JsonUtils;
import moe.yushi.authlibinjector.util.Logging; import moe.yushi.authlibinjector.util.Logging;
import moe.yushi.authlibinjector.yggdrasil.CustomYggdrasilAPIProvider;
import moe.yushi.authlibinjector.yggdrasil.YggdrasilClient;
public class LegacySkinAPIFilter implements URLFilter { public class LegacySkinAPIFilter implements URLFilter {
private static final Pattern PATH_SKINS = Pattern.compile("^/MinecraftSkins/(?<username>[^/]+)\\.png$"); private static final Pattern PATH_SKINS = Pattern.compile("^/MinecraftSkins/(?<username>[^/]+)\\.png$");
private YggdrasilConfiguration configuration; private YggdrasilClient upstream;
public LegacySkinAPIFilter(YggdrasilConfiguration configuration) { public LegacySkinAPIFilter(YggdrasilConfiguration configuration) {
this.configuration = configuration; this.upstream = new YggdrasilClient(new CustomYggdrasilAPIProvider(configuration));
} }
@Override @Override
@ -60,13 +54,13 @@ public class LegacySkinAPIFilter implements URLFilter {
Optional<String> skinUrl; Optional<String> skinUrl;
try { try {
skinUrl = queryCharacterUUID(username) skinUrl = upstream.queryUUID(username)
.flatMap(uuid -> queryCharacterProperty(uuid, "textures")) .flatMap(uuid -> upstream.queryProfile(uuid, false))
.map(encoded -> asString(Base64.getDecoder().decode(encoded))) .flatMap(profile -> Optional.ofNullable(profile.properties.get("textures")))
.map(property -> asString(Base64.getDecoder().decode(property.value)))
.flatMap(texturesPayload -> obtainTextureUrl(texturesPayload, "SKIN")); .flatMap(texturesPayload -> obtainTextureUrl(texturesPayload, "SKIN"));
} catch (UncheckedIOException e) { } catch (UncheckedIOException e) {
Logging.HTTPD.log(Level.WARNING, "Failed to fetch skin for " + username, e); throw newUncheckedIOException("Failed to fetch skin metadata for " + username, e);
return of(newFixedLengthResponse(Status.INTERNAL_ERROR, null, null));
} }
if (skinUrl.isPresent()) { if (skinUrl.isPresent()) {
@ -76,8 +70,7 @@ public class LegacySkinAPIFilter implements URLFilter {
try { try {
data = getURL(url); data = getURL(url);
} catch (IOException e) { } catch (IOException e) {
Logging.HTTPD.log(Level.WARNING, "Failed to retrieve skin from " + url, e); throw newUncheckedIOException("Failed to retrieve skin from " + url, e);
return of(newFixedLengthResponse(Status.INTERNAL_ERROR, null, null));
} }
Logging.HTTPD.info("Retrieved skin for " + username + " from " + url + ", " + data.length + " bytes"); Logging.HTTPD.info("Retrieved skin for " + username + " from " + url + ", " + data.length + " bytes");
return of(newFixedLengthResponse(Status.OK, "image/png", new ByteArrayInputStream(data), data.length)); return of(newFixedLengthResponse(Status.OK, "image/png", new ByteArrayInputStream(data), data.length));
@ -88,51 +81,6 @@ public class LegacySkinAPIFilter implements URLFilter {
} }
} }
private Optional<String> queryCharacterUUID(String username) throws UncheckedIOException {
String responseText;
try {
responseText = asString(postURL(
configuration.getApiRoot() + "api/profiles/minecraft",
CONTENT_TYPE_JSON,
JSONArray.toJSONString(singleton(username)).getBytes(UTF_8)));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
Logging.HTTPD.fine("Query UUID of username " + username + ", response: " + responseText);
JSONArray response = asJsonArray(parseJson(responseText));
if (response.size() == 0) {
return empty();
} else if (response.size() == 1) {
JSONObject profile = asJsonObject(response.get(0));
return of(asJsonString(profile.get("id")));
} else {
throw newUncheckedIOException("Invalid JSON: Unexpected response length");
}
}
private Optional<String> queryCharacterProperty(String uuid, String propertyName) throws UncheckedIOException {
String responseText;
try {
responseText = asString(getURL(
configuration.getApiRoot() + "sessionserver/session/minecraft/profile/" + uuid));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
if (responseText.isEmpty()) {
Logging.HTTPD.fine("Query profile of " + uuid + ", not found");
return empty();
}
Logging.HTTPD.fine("Query profile of " + uuid + ", response: " + responseText);
JSONObject response = asJsonObject(parseJson(responseText));
return asJsonArray(response.get("properties")).stream()
.map(JsonUtils::asJsonObject)
.filter(property -> asJsonString(property.get("name")).equals(propertyName))
.findFirst()
.map(property -> asJsonString(property.get("value")));
}
private Optional<String> obtainTextureUrl(String texturesPayload, String textureType) throws UncheckedIOException { private Optional<String> obtainTextureUrl(String texturesPayload, String textureType) throws UncheckedIOException {
JSONObject payload = asJsonObject(parseJson(texturesPayload)); JSONObject payload = asJsonObject(parseJson(texturesPayload));
JSONObject textures = asJsonObject(payload.get("textures")); JSONObject textures = asJsonObject(payload.get("textures"));

View file

@ -0,0 +1,20 @@
package moe.yushi.authlibinjector.util;
import java.util.UUID;
public final class UUIDUtils {
public static String toUnsignedUUID(UUID uuid) {
return uuid.toString().replace("-", "");
}
public static UUID fromUnsignedUUID(String uuid) {
if (uuid.length() == 32) {
return UUID.fromString(uuid.substring(0, 8) + "-" + uuid.substring(8, 12) + "-" + uuid.substring(12, 16) + "-" + uuid.substring(16, 20) + "-" + uuid.substring(20, 32));
} else {
throw new IllegalArgumentException("Invalid UUID: " + uuid);
}
}
private UUIDUtils() {
}
}

View file

@ -0,0 +1,31 @@
package moe.yushi.authlibinjector.yggdrasil;
import static moe.yushi.authlibinjector.util.UUIDUtils.toUnsignedUUID;
import java.util.UUID;
import moe.yushi.authlibinjector.YggdrasilConfiguration;
public class CustomYggdrasilAPIProvider implements YggdrasilAPIProvider {
private String apiRoot;
public CustomYggdrasilAPIProvider(YggdrasilConfiguration configuration) {
this.apiRoot = configuration.getApiRoot();
}
@Override
public String queryUUIDsByNames() {
return apiRoot + "api/profiles/minecraft";
}
@Override
public String queryProfile(UUID uuid) {
return apiRoot + "sessionserver/session/minecraft/profile/" + toUnsignedUUID(uuid);
}
@Override
public String toString() {
return apiRoot;
}
}

View file

@ -0,0 +1,16 @@
package moe.yushi.authlibinjector.yggdrasil;
import java.util.Map;
import java.util.UUID;
public class GameProfile {
public static class PropertyValue {
public String value;
public String signature;
}
public UUID id;
public String name;
public Map<String, PropertyValue> properties;
}

View file

@ -0,0 +1,23 @@
package moe.yushi.authlibinjector.yggdrasil;
import static moe.yushi.authlibinjector.util.UUIDUtils.toUnsignedUUID;
import java.util.UUID;
public class MojangYggdrasilAPIProvider implements YggdrasilAPIProvider {
@Override
public String queryUUIDsByNames() {
return "https://api.mojang.com/profiles/minecraft";
}
@Override
public String queryProfile(UUID uuid) {
return "https://sessionserver.mojang.com/session/minecraft/profile/" + toUnsignedUUID(uuid);
}
@Override
public String toString() {
return "Mojang";
}
}

View file

@ -0,0 +1,8 @@
package moe.yushi.authlibinjector.yggdrasil;
import java.util.UUID;
public interface YggdrasilAPIProvider {
String queryUUIDsByNames();
String queryProfile(UUID uuid);
}

View file

@ -0,0 +1,106 @@
package moe.yushi.authlibinjector.yggdrasil;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.singleton;
import static moe.yushi.authlibinjector.util.IOUtils.CONTENT_TYPE_JSON;
import static moe.yushi.authlibinjector.util.IOUtils.asString;
import static moe.yushi.authlibinjector.util.IOUtils.getURL;
import static moe.yushi.authlibinjector.util.IOUtils.newUncheckedIOException;
import static moe.yushi.authlibinjector.util.IOUtils.postURL;
import static moe.yushi.authlibinjector.util.JsonUtils.asJsonArray;
import static moe.yushi.authlibinjector.util.JsonUtils.asJsonObject;
import static moe.yushi.authlibinjector.util.JsonUtils.asJsonString;
import static moe.yushi.authlibinjector.util.JsonUtils.parseJson;
import static moe.yushi.authlibinjector.util.UUIDUtils.fromUnsignedUUID;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import moe.yushi.authlibinjector.internal.org.json.simple.JSONArray;
import moe.yushi.authlibinjector.internal.org.json.simple.JSONObject;
import moe.yushi.authlibinjector.util.Logging;
import moe.yushi.authlibinjector.yggdrasil.GameProfile.PropertyValue;
public class YggdrasilClient {
private YggdrasilAPIProvider apiProvider;
public YggdrasilClient(YggdrasilAPIProvider apiProvider) {
this.apiProvider = apiProvider;
}
public Map<String, UUID> queryUUIDs(Set<String> names) throws UncheckedIOException {
String responseText;
try {
responseText = asString(postURL(
apiProvider.queryUUIDsByNames(), CONTENT_TYPE_JSON,
JSONArray.toJSONString(names).getBytes(UTF_8)));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
Logging.HTTPD.fine("Query UUIDs of " + names + " at [" + apiProvider + "], response: " + responseText);
Map<String, UUID> result = new LinkedHashMap<>();
for (Object rawProfile : asJsonArray(parseJson(responseText))) {
JSONObject profile = asJsonObject(rawProfile);
result.put(
asJsonString(profile.get("name")),
parseUnsignedUUID(asJsonString(profile.get("id"))));
}
return result;
}
public Optional<UUID> queryUUID(String name) throws UncheckedIOException {
return Optional.ofNullable(queryUUIDs(singleton(name)).get(name));
}
public Optional<GameProfile> queryProfile(UUID uuid, boolean withSignature) throws UncheckedIOException {
String url = apiProvider.queryProfile(uuid);
if (withSignature) {
url += "?unsigned=false";
}
String responseText;
try {
responseText = asString(getURL(url));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
if (responseText.isEmpty()) {
Logging.HTTPD.fine("Query profile of [" + uuid + "] at [" + apiProvider + "], not found");
return Optional.empty();
}
Logging.HTTPD.fine("Query profile of [" + uuid + "] at [" + apiProvider + "], response: " + responseText);
return Optional.of(parseGameProfile(asJsonObject(parseJson(responseText))));
}
private GameProfile parseGameProfile(JSONObject json) {
GameProfile profile = new GameProfile();
profile.id = parseUnsignedUUID(asJsonString(json.get("id")));
profile.name = asJsonString(json.get("name"));
profile.properties = new LinkedHashMap<>();
for (Object rawProperty : asJsonArray(json.get("properties"))) {
JSONObject property = (JSONObject) rawProperty;
PropertyValue entry = new PropertyValue();
entry.value = asJsonString(property.get("value"));
if (property.containsKey("signature")) {
entry.signature = asJsonString(property.get("signature"));
}
profile.properties.put(asJsonString(property.get("name")), entry);
}
return profile;
}
private UUID parseUnsignedUUID(String uuid) throws UncheckedIOException {
try {
return fromUnsignedUUID(uuid);
} catch (IllegalArgumentException e) {
throw newUncheckedIOException(e.getMessage());
}
}
}