diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/VerifyResponseTask.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/VerifyResponseTask.java index d8596910..f9696218 100644 --- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/VerifyResponseTask.java +++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/VerifyResponseTask.java @@ -36,15 +36,18 @@ import com.comphenix.protocol.wrappers.WrappedChatComponent; import com.comphenix.protocol.wrappers.WrappedGameProfile; import com.github.games647.craftapi.model.auth.Verification; import com.github.games647.craftapi.model.skin.SkinProperty; +import com.github.games647.craftapi.resolver.AbstractResolver; import com.github.games647.craftapi.resolver.MojangResolver; import com.github.games647.fastlogin.bukkit.BukkitLoginSession; import com.github.games647.fastlogin.bukkit.FastLoginBukkit; import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.net.InetAddress; -import java.net.InetSocketAddress; +import java.net.*; +import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.security.Key; import java.security.KeyPair; @@ -138,7 +141,10 @@ public class VerifyResponseTask implements Runnable { try { MojangResolver resolver = plugin.getCore().getResolver(); InetAddress address = socketAddress.getAddress(); - Optional response = resolver.hasJoined(requestedUsername, serverId, address); + + // TODO edit marker + Optional response = VerifyResponseTask.hasJoined(resolver, requestedUsername, serverId, address); + if (response.isPresent()) { Verification verification = response.get(); plugin.getLog().info("Profile {} has a verified premium account", requestedUsername); @@ -162,14 +168,91 @@ public class VerifyResponseTask implements Runnable { } else { //user tried to fake an authentication disconnect("invalid-session", - "GameProfile {0} ({1}) tried to log in with an invalid session ServerId: {2}", - session.getRequestUsername(), socketAddress, serverId); + String.format("GameProfile %s (%s) tried to log in with an invalid session. ServerId: %s", + session.getRequestUsername(), socketAddress, serverId)); } } catch (IOException ioEx) { disconnect("error-kick", "Failed to connect to session server", ioEx); } } + /** + * A reimplementation of {@link MojangResolver#hasJoined(String, String, InetAddress)} using various crude reflection-based hacks + * to access the protected code. The significant difference is that unlike in the CraftAPI implementation, which sends the "ip" parameter + * when the hostIp parameter is an IPv4 address, but skips it for IPv6, this implementation ommits the "ip" parameter also for IPv4, effectively + * enabling transparent proxies to work. + * + * TODO Reimplement as MojangResolver subclass overriding hasJoined method -> "ProxyAgnosticMojangResolver" perhaps? + * + * @param resolver The mojang resolver object the method will operate on + * @param username The literal username of the player + * @param serverHash The computed server hash sent to mojang session servers + * @param hostIp The host IP address - not used, kept to maintain similar signature + * @return An optional object containing the verification information, if any. If there is no verification information, the session servers consider the join invalid. + * @throws IOException When an error occurs during the HTTP communication + * @author games647, Enginecrafter77 + */ + public static Optional hasJoined(MojangResolver resolver, String username, String serverHash, InetAddress hostIp) throws IOException { + /*String url; + if (hostIp instanceof Inet6Address) { + url = String.format("https://sessionserver.mojang.com/session/minecraft/hasJoined?username=%s&serverId=%s", username, serverHash); + } else { + String encodedIP = URLEncoder.encode(hostIp.getHostAddress(), StandardCharsets.UTF_8.name()); + url = String.format("https://sessionserver.mojang.com/session/minecraft/hasJoined?username=%s&serverId=%s&ip=%s", username, serverHash, encodedIP); + }*/ + String url = String.format("https://sessionserver.mojang.com/session/minecraft/hasJoined?username=%s&serverId=%s", username, serverHash); + + try + { + Class inputStreamActionClass = Class.forName("com.github.games647.craftapi.resolver.AbstractResolver$InputStreamAction"); + Method getConnectionMethod = AbstractResolver.class.getDeclaredMethod("getConnection", String.class); + Method parseRequestMethod = AbstractResolver.class.getDeclaredMethod("parseRequest", HttpURLConnection.class, inputStreamActionClass); + Method readJsonMethod = AbstractResolver.class.getDeclaredMethod("readJson", InputStream.class, Class.class); + + getConnectionMethod.setAccessible(true); + parseRequestMethod.setAccessible(true); + readJsonMethod.setAccessible(true); + + HttpURLConnection conn = (HttpURLConnection)getConnectionMethod.invoke(resolver, url); + int responseCode = conn.getResponseCode(); + + Verification result = null; + if(responseCode != 204) + { + AbstractResolverAdapter.InputStreamActionAdapter action = new AbstractResolverAdapter.InputStreamActionAdapter() { + @Override + public Verification useStream(InputStream inp) throws IOException + { + try + { + return (Verification)readJsonMethod.invoke(resolver, new Object[] {inp, Verification.class}); + } + catch(ReflectiveOperationException exc) + { + throw new IOException("Reflective method access failed", exc); + } + } + }; + result = (Verification)parseRequestMethod.invoke(resolver, new Object[] {conn, action}); + } + return Optional.ofNullable(result); + } + catch(ReflectiveOperationException exc) + { + throw new RuntimeException("Error occured in reflective hacks to MojangResolver", exc); + } + } + + /** + * An adapter class used to make the InputStreamAction interface accessible to us. + * I know, it's a crude and disgusting solution, but bear with me, it's just temporary. + */ + public static class AbstractResolverAdapter extends AbstractResolver + { + @FunctionalInterface + protected static interface InputStreamActionAdapter extends AbstractResolver.InputStreamAction {} + } + private void setPremiumUUID(UUID premiumUUID) { if (plugin.getConfig().getBoolean("premiumUuid") && premiumUUID != null) { try {