mirror of
https://github.com/yushijinhun/authlib-injector.git
synced 2024-11-15 06:11:09 +01:00
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;
|
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"));
|
||||||
|
|
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