From c21312b27971769ef345f17b0a4c910e3a900972 Mon Sep 17 00:00:00 2001
From: Gravita <12893402+gravit0@users.noreply.github.com>
Date: Sat, 25 Mar 2023 19:11:34 +0300
Subject: [PATCH] Launcher Auth

---
 build.gradle.kts                              |  2 +
 proxy/build.gradle.kts                        |  2 +
 .../client/InitialLoginSessionHandler.java    | 78 +++++++++----------
 .../proxy/connection/util/LauncherUtil.java   | 59 ++++++++++++++
 4 files changed, 99 insertions(+), 42 deletions(-)
 create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/connection/util/LauncherUtil.java

diff --git a/build.gradle.kts b/build.gradle.kts
index 8b6870d5..6e13c2b6 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -30,6 +30,8 @@ subprojects {
         mavenCentral()
         maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") // adventure
         maven("https://repo.papermc.io/repository/maven-public/")
+        // oss sonatype
+        maven("https://oss.sonatype.org/content/repositories/snapshots")
     }
     dependencies {
         testImplementation("org.junit.jupiter:junit-jupiter:$junitVersion")
diff --git a/proxy/build.gradle.kts b/proxy/build.gradle.kts
index 6d94c567..bfd8a405 100644
--- a/proxy/build.gradle.kts
+++ b/proxy/build.gradle.kts
@@ -122,6 +122,8 @@ dependencies {
     implementation("org.apache.logging.log4j:log4j-slf4j-impl:${log4jVersion}")
     implementation("org.apache.logging.log4j:log4j-iostreams:${log4jVersion}")
     implementation("org.apache.logging.log4j:log4j-jul:${log4jVersion}")
+    compileOnly("pro.gravit.launcher:launcher-core:5.3.5")
+    compileOnly("pro.gravit.launcher:launcher-ws-api:5.3.5")
 
     implementation("net.sf.jopt-simple:jopt-simple:$joptSimpleVersion") // command-line options
     implementation("net.minecrell:terminalconsoleappender:$terminalConsoleAppenderVersion")
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java
index 01bef5f9..0b07dc3e 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java
@@ -34,12 +34,15 @@ import com.velocitypowered.proxy.VelocityServer;
 import com.velocitypowered.proxy.connection.MinecraftConnection;
 import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
 import com.velocitypowered.proxy.crypto.IdentifiedKeyImpl;
+import com.velocitypowered.proxy.connection.util.LauncherUtil;
 import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder;
 import com.velocitypowered.proxy.protocol.packet.EncryptionRequest;
 import com.velocitypowered.proxy.protocol.packet.EncryptionResponse;
 import com.velocitypowered.proxy.protocol.packet.LoginPluginResponse;
 import com.velocitypowered.proxy.protocol.packet.ServerLogin;
 import io.netty.buffer.ByteBuf;
+
+import java.io.IOException;
 import java.net.InetSocketAddress;
 import java.security.GeneralSecurityException;
 import java.security.KeyPair;
@@ -55,6 +58,9 @@ import org.apache.logging.log4j.Logger;
 import org.asynchttpclient.ListenableFuture;
 import org.asynchttpclient.Response;
 import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+import pro.gravit.launcher.request.Request;
+import pro.gravit.launcher.request.RequestException;
+import pro.gravit.launcher.request.auth.CheckServerRequest;
 
 /**
  * Handles authenticating the player to Mojang's servers.
@@ -202,20 +208,17 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
       byte[] decryptedSharedSecret = decryptRsa(serverKeyPair, packet.getSharedSecret());
       String serverId = generateServerId(decryptedSharedSecret, serverKeyPair.getPublic());
 
-      String playerIp = ((InetSocketAddress) mcConnection.getRemoteAddress()).getHostString();
-      String url = String.format(MOJANG_HASJOINED_URL,
-          urlFormParameterEscaper().escape(login.getUsername()), serverId);
-
-      if (server.getConfiguration().shouldPreventClientProxyConnections()) {
-        url += "&ip=" + urlFormParameterEscaper().escape(playerIp);
-      }
 
-      ListenableFuture<Response> hasJoinedResponse = server.getAsyncHttpClient().prepareGet(url)
-          .execute();
-      hasJoinedResponse.addListener(() -> {
+      Request.getRequestService().request(new CheckServerRequest(login.getUsername(), serverId)).handleAsync((response, exception) -> {
         if (mcConnection.isClosed()) {
           // The player disconnected after we authenticated them.
-          return;
+          return null;
+        }
+
+        if(exception != null) {
+          if(exception instanceof ExecutionException) {
+            exception = exception.getCause();
+          }
         }
 
         // Go ahead and enable encryption. Once the client sends EncryptionResponse, encryption
@@ -227,50 +230,41 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
           // At this point, the connection is encrypted, but something's wrong on our side and
           // we can't do anything about it.
           mcConnection.close(true);
-          return;
+          return null;
         }
-
-        try {
-          Response profileResponse = hasJoinedResponse.get();
-          if (profileResponse.getStatusCode() == 200) {
-            final GameProfile profile = GENERAL_GSON.fromJson(profileResponse.getResponseBody(),
-                GameProfile.class);
-            // Not so fast, now we verify the public key for 1.19.1+
-            if (inbound.getIdentifiedKey() != null
+        if (exception == null) {
+          // All went well, initialize the session.
+          // Not so fast, now we verify the public key for 1.19.1+
+          if (inbound.getIdentifiedKey() != null
                 && inbound.getIdentifiedKey().getKeyRevision() == IdentifiedKey.Revision.LINKED_V2
                 && inbound.getIdentifiedKey() instanceof IdentifiedKeyImpl) {
-              IdentifiedKeyImpl key = (IdentifiedKeyImpl) inbound.getIdentifiedKey();
-              if (!key.internalAddHolder(profile.getId())) {
+            IdentifiedKeyImpl key = (IdentifiedKeyImpl) inbound.getIdentifiedKey();
+            if (!key.internalAddHolder(response.uuid)) {
                 inbound.disconnect(
                     Component.translatable("multiplayer.disconnect.invalid_public_key"));
-              }
             }
-            // All went well, initialize the session.
-            mcConnection.setSessionHandler(new AuthSessionHandler(
-                server, inbound, profile, true
-            ));
-          } else if (profileResponse.getStatusCode() == 204) {
-            // Apparently an offline-mode user logged onto this online-mode proxy.
-            inbound.disconnect(Component.translatable("velocity.error.online-mode-only",
-                NamedTextColor.RED));
-          } else {
-            // Something else went wrong
-            logger.error(
-                "Got an unexpected error code {} whilst contacting Mojang to log in {} ({})",
-                profileResponse.getStatusCode(), login.getUsername(), playerIp);
-            inbound.disconnect(Component.translatable("multiplayer.disconnect.authservers_down"));
           }
-        } catch (ExecutionException e) {
-          logger.error("Unable to authenticate with Mojang", e);
+          //
+          mcConnection.setSessionHandler(new AuthSessionHandler(
+                  server, inbound, LauncherUtil.makeGameProfile(response), true
+          ));
+        } else if (exception instanceof RequestException) {
+          // Apparently an offline-mode user logged onto this online-mode proxy.
+          inbound.disconnect(Component.translatable("velocity.error.online-mode-only",
+                  NamedTextColor.RED));
+        } else {
+          // Something else went wrong
+          logger.error("Unable to authenticate with Launcher", exception);
           inbound.disconnect(Component.translatable("multiplayer.disconnect.authservers_down"));
-        } catch (InterruptedException e) {
-          // not much we can do usefully
-          Thread.currentThread().interrupt();
         }
+        return null;
       }, mcConnection.eventLoop());
     } catch (GeneralSecurityException e) {
       logger.error("Unable to enable encryption", e);
       mcConnection.close(true);
+    } catch (IOException e) {
+      logger.error("Unable to authenticate with Launcher", e);
+      inbound.disconnect(Component.translatable("multiplayer.disconnect.authservers_down"));
     }
     return true;
   }
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/util/LauncherUtil.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/util/LauncherUtil.java
new file mode 100644
index 00000000..ef1ad00a
--- /dev/null
+++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/util/LauncherUtil.java
@@ -0,0 +1,59 @@
+package com.velocitypowered.proxy.connection.util;
+
+import com.velocitypowered.api.util.GameProfile;
+import pro.gravit.launcher.Launcher;
+import pro.gravit.launcher.events.request.CheckServerRequestEvent;
+import pro.gravit.launcher.profiles.PlayerProfile;
+import pro.gravit.launcher.profiles.Texture;
+import pro.gravit.utils.helper.SecurityHelper;
+
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+
+public class LauncherUtil {
+    public static GameProfile makeGameProfile(CheckServerRequestEvent event) {
+        PlayerProfile profile = event.playerProfile;
+        List<GameProfile.Property> properties = new ArrayList<>();
+        for (var e : profile.properties.entrySet()) {
+            properties.add(new GameProfile.Property(e.getKey(), e.getValue(), ""));
+        }
+        {
+            String key = "textures";
+            GameProfileTextureProperties textureProperty = new GameProfileTextureProperties();
+            textureProperty.profileId = event.playerProfile.uuid.toString().replace("-", "");
+            textureProperty.profileName = event.playerProfile.username;
+            textureProperty.timestamp = System.currentTimeMillis();
+            for (var texture : profile.assets.entrySet()) {
+                textureProperty.textures.put(texture.getKey(), new GameProfileTextureProperties.GameTexture(texture.getValue()));
+            }
+            String value = Base64.getEncoder().encodeToString(Launcher.gsonManager.gson.toJson(textureProperty).getBytes(StandardCharsets.UTF_8));
+            properties.add(new GameProfile.Property(key, value, ""));
+        }
+        return new GameProfile(profile.uuid, profile.username, properties);
+    }
+
+    public static class GameProfileTextureProperties {
+        public long timestamp;
+        public String profileId;
+        public String profileName;
+        public Map<String, GameTexture> textures = new HashMap<>();
+
+        public static class GameTexture {
+            public String url;
+            public String hash;
+            public Map<String, String> metadata;
+
+            public GameTexture(String url, String hash, Map<String, String> metadata) {
+                this.url = url;
+                this.hash = hash;
+                this.metadata = metadata;
+            }
+
+            public GameTexture(Texture texture) {
+                this.url = texture.url;
+                this.hash = texture.digest == null ? null : SecurityHelper.toHex(texture.digest);
+                this.metadata = texture.metadata;
+            }
+        }
+    }
+}
-- 
2.40.0

