From eeda91c329de364cc40426c97fabea456e322ad7 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Mon, 31 Dec 2018 02:45:21 +0800 Subject: [PATCH] Support @mojang suffix --- .../authlibinjector/AuthlibInjector.java | 13 ++- .../httpd/LegacySkinAPIFilter.java | 6 +- .../httpd/QueryProfileFilter.java | 76 +++++++++++++++ .../httpd/QueryUUIDsFilter.java | 95 +++++++++++++++++++ .../authlibinjector/httpd/URLFilter.java | 3 +- .../yggdrasil/YggdrasilResponseBuilder.java | 45 +++++++++ 6 files changed, 232 insertions(+), 6 deletions(-) create mode 100644 src/main/java/moe/yushi/authlibinjector/httpd/QueryProfileFilter.java create mode 100644 src/main/java/moe/yushi/authlibinjector/httpd/QueryUUIDsFilter.java create mode 100644 src/main/java/moe/yushi/authlibinjector/yggdrasil/YggdrasilResponseBuilder.java diff --git a/src/main/java/moe/yushi/authlibinjector/AuthlibInjector.java b/src/main/java/moe/yushi/authlibinjector/AuthlibInjector.java index af2445d..bd36d34 100644 --- a/src/main/java/moe/yushi/authlibinjector/AuthlibInjector.java +++ b/src/main/java/moe/yushi/authlibinjector/AuthlibInjector.java @@ -23,6 +23,8 @@ import java.util.function.Consumer; import moe.yushi.authlibinjector.httpd.DefaultURLRedirector; import moe.yushi.authlibinjector.httpd.LegacySkinAPIFilter; +import moe.yushi.authlibinjector.httpd.QueryProfileFilter; +import moe.yushi.authlibinjector.httpd.QueryUUIDsFilter; import moe.yushi.authlibinjector.httpd.URLFilter; import moe.yushi.authlibinjector.httpd.URLProcessor; import moe.yushi.authlibinjector.transform.AuthlibLogInterceptor; @@ -33,6 +35,9 @@ import moe.yushi.authlibinjector.transform.SkinWhitelistTransformUnit; import moe.yushi.authlibinjector.transform.YggdrasilKeyTransformUnit; import moe.yushi.authlibinjector.transform.support.CitizensTransformer; import moe.yushi.authlibinjector.util.Logging; +import moe.yushi.authlibinjector.yggdrasil.CustomYggdrasilAPIProvider; +import moe.yushi.authlibinjector.yggdrasil.MojangYggdrasilAPIProvider; +import moe.yushi.authlibinjector.yggdrasil.YggdrasilClient; public final class AuthlibInjector { @@ -271,12 +276,18 @@ public final class AuthlibInjector { private static URLProcessor createURLProcessor(YggdrasilConfiguration config) { List filters = new ArrayList<>(); + YggdrasilClient customClient = new YggdrasilClient(new CustomYggdrasilAPIProvider(config)); + YggdrasilClient mojangClient = new YggdrasilClient(new MojangYggdrasilAPIProvider()); + if (Boolean.TRUE.equals(config.getMeta().get("feature.legacy_skin_api"))) { Logging.CONFIG.info("Disabled local redirect for legacy skin API, as the remote Yggdrasil server supports it"); } else { - filters.add(new LegacySkinAPIFilter(config)); + filters.add(new LegacySkinAPIFilter(customClient)); } + filters.add(new QueryUUIDsFilter(mojangClient, customClient)); + filters.add(new QueryProfileFilter(mojangClient, customClient)); + return new URLProcessor(filters, new DefaultURLRedirector(config)); } diff --git a/src/main/java/moe/yushi/authlibinjector/httpd/LegacySkinAPIFilter.java b/src/main/java/moe/yushi/authlibinjector/httpd/LegacySkinAPIFilter.java index bf0f1db..cff5015 100644 --- a/src/main/java/moe/yushi/authlibinjector/httpd/LegacySkinAPIFilter.java +++ b/src/main/java/moe/yushi/authlibinjector/httpd/LegacySkinAPIFilter.java @@ -17,14 +17,12 @@ import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; -import moe.yushi.authlibinjector.YggdrasilConfiguration; import moe.yushi.authlibinjector.internal.fi.iki.elonen.IHTTPSession; import moe.yushi.authlibinjector.internal.fi.iki.elonen.Response; import moe.yushi.authlibinjector.internal.fi.iki.elonen.Status; 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 { @@ -33,8 +31,8 @@ public class LegacySkinAPIFilter implements URLFilter { private YggdrasilClient upstream; - public LegacySkinAPIFilter(YggdrasilConfiguration configuration) { - this.upstream = new YggdrasilClient(new CustomYggdrasilAPIProvider(configuration)); + public LegacySkinAPIFilter(YggdrasilClient upstream) { + this.upstream = upstream; } @Override diff --git a/src/main/java/moe/yushi/authlibinjector/httpd/QueryProfileFilter.java b/src/main/java/moe/yushi/authlibinjector/httpd/QueryProfileFilter.java new file mode 100644 index 0000000..34a65e8 --- /dev/null +++ b/src/main/java/moe/yushi/authlibinjector/httpd/QueryProfileFilter.java @@ -0,0 +1,76 @@ +package moe.yushi.authlibinjector.httpd; + +import static java.util.Optional.empty; +import static moe.yushi.authlibinjector.util.UUIDUtils.fromUnsignedUUID; + +import java.io.IOException; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import moe.yushi.authlibinjector.internal.fi.iki.elonen.IHTTPSession; +import moe.yushi.authlibinjector.internal.fi.iki.elonen.Response; +import moe.yushi.authlibinjector.internal.fi.iki.elonen.Status; +import moe.yushi.authlibinjector.yggdrasil.GameProfile; +import moe.yushi.authlibinjector.yggdrasil.YggdrasilClient; +import moe.yushi.authlibinjector.yggdrasil.YggdrasilResponseBuilder; + +public class QueryProfileFilter implements URLFilter { + + private static final Pattern PATH_REGEX = Pattern.compile("^/session/minecraft/profile/(?[0-9a-f]{32})$"); + + private YggdrasilClient mojangClient; + private YggdrasilClient customClient; + + public QueryProfileFilter(YggdrasilClient mojangClient, YggdrasilClient customClient) { + this.mojangClient = mojangClient; + this.customClient = customClient; + } + + @Override + public boolean canHandle(String domain, String path) { + return domain.equals("sessionserver.mojang.com") && path.startsWith("/session/minecraft/profile/"); + } + + @Override + public Optional handle(String domain, String path, IHTTPSession session) throws IOException { + if (!domain.equals("sessionserver.mojang.com")) + return empty(); + Matcher matcher = PATH_REGEX.matcher(path); + if (!matcher.find()) + return empty(); + + UUID uuid; + try { + uuid = fromUnsignedUUID(matcher.group("uuid")); + } catch (IllegalArgumentException e) { + return Optional.of(Response.newFixedLength(Status.NO_CONTENT, null, null)); + } + + boolean withSignature = false; + List unsignedValues = session.getParameters().get("unsigned"); + if (unsignedValues != null && unsignedValues.get(0).equals("false")) { + withSignature = true; + } + + Optional response; + if (QueryUUIDsFilter.isMaskedUUID(uuid)) { + response = mojangClient.queryProfile(QueryUUIDsFilter.unmaskUUID(uuid), withSignature); + response.ifPresent(profile -> { + profile.id = uuid; + profile.name += QueryUUIDsFilter.NAME_SUFFIX; + }); + } else { + response = customClient.queryProfile(uuid, withSignature); + } + + if (response.isPresent()) { + return Optional.of(Response.newFixedLength(Status.OK, null, YggdrasilResponseBuilder.queryProfile(response.get(), withSignature))); + } else { + return Optional.of(Response.newFixedLength(Status.NO_CONTENT, null, null)); + } + } + +} diff --git a/src/main/java/moe/yushi/authlibinjector/httpd/QueryUUIDsFilter.java b/src/main/java/moe/yushi/authlibinjector/httpd/QueryUUIDsFilter.java new file mode 100644 index 0000000..cf68b64 --- /dev/null +++ b/src/main/java/moe/yushi/authlibinjector/httpd/QueryUUIDsFilter.java @@ -0,0 +1,95 @@ +package moe.yushi.authlibinjector.httpd; + +import static moe.yushi.authlibinjector.util.IOUtils.CONTENT_TYPE_JSON; +import static moe.yushi.authlibinjector.util.IOUtils.asBytes; +import static moe.yushi.authlibinjector.util.IOUtils.asString; +import static moe.yushi.authlibinjector.util.JsonUtils.asJsonArray; +import static moe.yushi.authlibinjector.util.JsonUtils.asJsonString; +import static moe.yushi.authlibinjector.util.JsonUtils.parseJson; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +import moe.yushi.authlibinjector.internal.fi.iki.elonen.IHTTPSession; +import moe.yushi.authlibinjector.internal.fi.iki.elonen.Response; +import moe.yushi.authlibinjector.internal.fi.iki.elonen.Status; +import moe.yushi.authlibinjector.util.Logging; +import moe.yushi.authlibinjector.yggdrasil.YggdrasilClient; +import moe.yushi.authlibinjector.yggdrasil.YggdrasilResponseBuilder; + +public class QueryUUIDsFilter implements URLFilter { + + private YggdrasilClient mojangClient; + private YggdrasilClient customClient; + + public QueryUUIDsFilter(YggdrasilClient mojangClient, YggdrasilClient customClient) { + this.mojangClient = mojangClient; + this.customClient = customClient; + } + + @Override + public boolean canHandle(String domain, String path) { + return domain.equals("api.mojang.com") && path.startsWith("/profiles/"); + } + + @Override + public Optional handle(String domain, String path, IHTTPSession session) throws IOException { + if (domain.equals("api.mojang.com") && path.equals("/profiles/minecraft") && session.getMethod().equals("POST")) { + Set request = new LinkedHashSet<>(); + asJsonArray(parseJson(asString(asBytes(session.getInputStream())))) + .forEach(element -> request.add(asJsonString(element))); + return Optional.of(Response.newFixedLength(Status.OK, CONTENT_TYPE_JSON, + YggdrasilResponseBuilder.queryUUIDs(performQuery(request)))); + } else { + return Optional.empty(); + } + } + + private Map performQuery(Set names) { + Set customNames = new LinkedHashSet<>(); + Set mojangNames = new LinkedHashSet<>(); + names.forEach(name -> { + if (name.endsWith(NAME_SUFFIX)) { + mojangNames.add(name.substring(0, name.length() - NAME_SUFFIX.length())); + } else { + customNames.add(name); + } + }); + + Map result = new LinkedHashMap<>(); + if (!customNames.isEmpty()) { + result.putAll(customClient.queryUUIDs(customNames)); + } + if (!mojangNames.isEmpty()) { + mojangClient.queryUUIDs(mojangNames) + .forEach((name, uuid) -> { + result.put(name + NAME_SUFFIX, maskUUID(uuid)); + }); + } + return result; + } + + private static final int MSB_MASK = 0x00008000; + static final String NAME_SUFFIX = "@mojang"; + + static UUID maskUUID(UUID uuid) { + if (isMaskedUUID(uuid)) { + Logging.HTTPD.warning("UUID already masked: " + uuid); + } + return new UUID(uuid.getMostSignificantBits() | MSB_MASK, uuid.getLeastSignificantBits()); + } + + static boolean isMaskedUUID(UUID uuid) { + return (uuid.getMostSignificantBits() & MSB_MASK) != 0; + } + + static UUID unmaskUUID(UUID uuid) { + return new UUID(uuid.getMostSignificantBits() & (~MSB_MASK), uuid.getLeastSignificantBits()); + } + +} diff --git a/src/main/java/moe/yushi/authlibinjector/httpd/URLFilter.java b/src/main/java/moe/yushi/authlibinjector/httpd/URLFilter.java index 083e5b9..46e8cdc 100644 --- a/src/main/java/moe/yushi/authlibinjector/httpd/URLFilter.java +++ b/src/main/java/moe/yushi/authlibinjector/httpd/URLFilter.java @@ -1,5 +1,6 @@ package moe.yushi.authlibinjector.httpd; +import java.io.IOException; import java.util.Optional; import moe.yushi.authlibinjector.internal.fi.iki.elonen.IHTTPSession; @@ -9,5 +10,5 @@ public interface URLFilter { boolean canHandle(String domain, String path); - Optional handle(String domain, String path, IHTTPSession session); + Optional handle(String domain, String path, IHTTPSession session) throws IOException; } diff --git a/src/main/java/moe/yushi/authlibinjector/yggdrasil/YggdrasilResponseBuilder.java b/src/main/java/moe/yushi/authlibinjector/yggdrasil/YggdrasilResponseBuilder.java new file mode 100644 index 0000000..cfe8b97 --- /dev/null +++ b/src/main/java/moe/yushi/authlibinjector/yggdrasil/YggdrasilResponseBuilder.java @@ -0,0 +1,45 @@ +package moe.yushi.authlibinjector.yggdrasil; + +import static moe.yushi.authlibinjector.util.UUIDUtils.toUnsignedUUID; + +import java.util.Map; +import java.util.UUID; + +import moe.yushi.authlibinjector.internal.org.json.simple.JSONArray; +import moe.yushi.authlibinjector.internal.org.json.simple.JSONObject; + +public final class YggdrasilResponseBuilder { + private YggdrasilResponseBuilder() { + } + + public static String queryUUIDs(Map result) { + JSONArray response = new JSONArray(); + result.forEach((name, uuid) -> { + JSONObject entry = new JSONObject(); + entry.put("id", toUnsignedUUID(uuid)); + entry.put("name", name); + response.add(entry); + }); + return response.toJSONString(); + } + + public static String queryProfile(GameProfile profile, boolean withSignature) { + JSONObject response = new JSONObject(); + response.put("id", toUnsignedUUID(profile.id)); + response.put("name", profile.name); + + JSONArray properties = new JSONArray(); + profile.properties.forEach((name, value) -> { + JSONObject entry = new JSONObject(); + entry.put("name", name); + entry.put("value", value.value); + if (withSignature && value.signature != null) { + entry.put("signature", value.signature); + } + properties.add(entry); + }); + response.put("properties", properties); + + return response.toJSONString(); + } +}