forked from MirrorHub/authlib-injector
Add YggdrasilClient
This commit is contained in:
parent
5531c8a06e
commit
3572ff8eb5
7 changed files with 214 additions and 62 deletions
|
@ -1,19 +1,13 @@
|
|||
package moe.yushi.authlibinjector.httpd;
|
||||
|
||||
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.of;
|
||||
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.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 java.io.ByteArrayInputStream;
|
||||
|
@ -21,7 +15,6 @@ import java.io.IOException;
|
|||
import java.io.UncheckedIOException;
|
||||
import java.util.Base64;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
import java.util.regex.Matcher;
|
||||
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.Status;
|
||||
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.util.JsonUtils;
|
||||
import moe.yushi.authlibinjector.util.Logging;
|
||||
import moe.yushi.authlibinjector.yggdrasil.CustomYggdrasilAPIProvider;
|
||||
import moe.yushi.authlibinjector.yggdrasil.YggdrasilClient;
|
||||
|
||||
public class LegacySkinAPIFilter implements URLFilter {
|
||||
|
||||
private static final Pattern PATH_SKINS = Pattern.compile("^/MinecraftSkins/(?<username>[^/]+)\\.png$");
|
||||
|
||||
private YggdrasilConfiguration configuration;
|
||||
private YggdrasilClient upstream;
|
||||
|
||||
public LegacySkinAPIFilter(YggdrasilConfiguration configuration) {
|
||||
this.configuration = configuration;
|
||||
this.upstream = new YggdrasilClient(new CustomYggdrasilAPIProvider(configuration));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -60,13 +54,13 @@ public class LegacySkinAPIFilter implements URLFilter {
|
|||
|
||||
Optional<String> skinUrl;
|
||||
try {
|
||||
skinUrl = queryCharacterUUID(username)
|
||||
.flatMap(uuid -> queryCharacterProperty(uuid, "textures"))
|
||||
.map(encoded -> asString(Base64.getDecoder().decode(encoded)))
|
||||
skinUrl = upstream.queryUUID(username)
|
||||
.flatMap(uuid -> upstream.queryProfile(uuid, false))
|
||||
.flatMap(profile -> Optional.ofNullable(profile.properties.get("textures")))
|
||||
.map(property -> asString(Base64.getDecoder().decode(property.value)))
|
||||
.flatMap(texturesPayload -> obtainTextureUrl(texturesPayload, "SKIN"));
|
||||
} catch (UncheckedIOException e) {
|
||||
Logging.HTTPD.log(Level.WARNING, "Failed to fetch skin for " + username, e);
|
||||
return of(newFixedLengthResponse(Status.INTERNAL_ERROR, null, null));
|
||||
throw newUncheckedIOException("Failed to fetch skin metadata for " + username, e);
|
||||
}
|
||||
|
||||
if (skinUrl.isPresent()) {
|
||||
|
@ -76,8 +70,7 @@ public class LegacySkinAPIFilter implements URLFilter {
|
|||
try {
|
||||
data = getURL(url);
|
||||
} catch (IOException e) {
|
||||
Logging.HTTPD.log(Level.WARNING, "Failed to retrieve skin from " + url, e);
|
||||
return of(newFixedLengthResponse(Status.INTERNAL_ERROR, null, null));
|
||||
throw newUncheckedIOException("Failed to retrieve skin from " + url, e);
|
||||
}
|
||||
Logging.HTTPD.info("Retrieved skin for " + username + " from " + url + ", " + data.length + " bytes");
|
||||
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 {
|
||||
JSONObject payload = asJsonObject(parseJson(texturesPayload));
|
||||
JSONObject textures = asJsonObject(payload.get("textures"));
|
||||
|
|
20
src/main/java/moe/yushi/authlibinjector/util/UUIDUtils.java
Normal file
20
src/main/java/moe/yushi/authlibinjector/util/UUIDUtils.java
Normal 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() {
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package moe.yushi.authlibinjector.yggdrasil;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public interface YggdrasilAPIProvider {
|
||||
String queryUUIDsByNames();
|
||||
String queryProfile(UUID uuid);
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue