From a5a68662fe6e0ec9a7af12450bf1b1d4d8e4ddde Mon Sep 17 00:00:00 2001
From: Gravita <12893402+gravit0@users.noreply.github.com>
Date: Sat, 16 Mar 2024 15:21:19 +0300
Subject: [PATCH] Launcher Auth

---
 proxy/build.gradle.kts                        |   3 +-
 .../client/InitialLoginSessionHandler.java    | 146 ++++++++----------
 .../proxy/connection/util/LauncherUtil.java   |  90 +++++++++++
 settings.gradle.kts                           |   1 +
 4 files changed, 157 insertions(+), 83 deletions(-)
 create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/connection/util/LauncherUtil.java

diff --git a/proxy/build.gradle.kts b/proxy/build.gradle.kts
index 0edd10cc..9f161c4a 100644
--- a/proxy/build.gradle.kts
+++ b/proxy/build.gradle.kts
@@ -110,7 +110,8 @@ dependencies {
     implementation(libs.netty.transport.native.kqueue)
     implementation(variantOf(libs.netty.transport.native.kqueue) { classifier("osx-x86_64") })
     implementation(variantOf(libs.netty.transport.native.kqueue) { classifier("osx-aarch_64") })
-
+    compileOnly("pro.gravit.launcher:launcher-core:5.6.0-SNAPSHOT")
+    compileOnly("pro.gravit.launcher:launcher-ws-api:5.6.0-SNAPSHOT")
     implementation(libs.jopt)
     implementation(libs.terminalconsoleappender)
     runtimeOnly(libs.jline)
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 5cc5371c..ec3e73e3 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
@@ -17,8 +17,6 @@
 
 package com.velocitypowered.proxy.connection.client;
 
-import static com.google.common.net.UrlEscapers.urlFormParameterEscaper;
-import static com.velocitypowered.proxy.VelocityServer.GENERAL_GSON;
 import static com.velocitypowered.proxy.connection.VelocityConstants.EMPTY_BYTE_ARRAY;
 import static com.velocitypowered.proxy.crypto.EncryptionUtils.decryptRsa;
 import static com.velocitypowered.proxy.crypto.EncryptionUtils.generateServerId;
@@ -33,6 +31,7 @@ import com.velocitypowered.api.util.GameProfile;
 import com.velocitypowered.proxy.VelocityServer;
 import com.velocitypowered.proxy.connection.MinecraftConnection;
 import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
+import com.velocitypowered.proxy.connection.util.LauncherUtil;
 import com.velocitypowered.proxy.crypto.IdentifiedKeyImpl;
 import com.velocitypowered.proxy.protocol.StateRegistry;
 import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder;
@@ -41,22 +40,24 @@ import com.velocitypowered.proxy.protocol.packet.EncryptionResponsePacket;
 import com.velocitypowered.proxy.protocol.packet.LoginPluginResponsePacket;
 import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket;
 import io.netty.buffer.ByteBuf;
-import java.net.InetSocketAddress;
-import java.net.URI;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
+
+import java.io.IOException;
 import java.security.GeneralSecurityException;
 import java.security.KeyPair;
 import java.security.MessageDigest;
 import java.util.Arrays;
 import java.util.Optional;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ThreadLocalRandom;
 import net.kyori.adventure.text.Component;
 import net.kyori.adventure.text.format.NamedTextColor;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+import pro.gravit.launcher.base.api.ConfigService;
+import pro.gravit.launcher.base.request.Request;
+import pro.gravit.launcher.base.request.RequestException;
+import pro.gravit.launcher.base.request.auth.CheckServerRequest;
 
 /**
  * Handles authenticating the player to Mojang's servers.
@@ -200,87 +201,68 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
 
       byte[] decryptedSharedSecret = decryptRsa(serverKeyPair, packet.getSharedSecret());
       String serverId = generateServerId(decryptedSharedSecret, serverKeyPair.getPublic());
+      Request.getRequestService().request(new CheckServerRequest(login.getUsername(), serverId,
+              ConfigService.checkServerConfig.needHardware,
+              ConfigService.checkServerConfig.needProperties)).handleAsync((response, exception) -> {
+        if (mcConnection.isClosed()) {
+          // The player disconnected after we authenticated them.
+          return null;
+        }
 
-      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);
-      }
-
-      final HttpRequest httpRequest = HttpRequest.newBuilder()
-              .setHeader("User-Agent",
-                      server.getVersion().getName() + "/" + server.getVersion().getVersion())
-              .uri(URI.create(url))
-              .build();
-      final HttpClient httpClient = server.createHttpClient();
-      httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString())
-          .whenCompleteAsync((response, throwable) -> {
-            if (mcConnection.isClosed()) {
-              // The player disconnected after we authenticated them.
-              return;
-            }
-
-            if (throwable != null) {
-              logger.error("Unable to authenticate player", throwable);
-              inbound.disconnect(Component.translatable("multiplayer.disconnect.authservers_down"));
-              return;
-            }
-
-            // Go ahead and enable encryption. Once the client sends EncryptionResponse, encryption
-            // is enabled.
-            try {
-              mcConnection.enableEncryption(decryptedSharedSecret);
-            } catch (GeneralSecurityException e) {
-              logger.error("Unable to enable encryption for connection", e);
-              // 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;
-            }
+        if(exception != null) {
+          if(exception instanceof ExecutionException) {
+            exception = exception.getCause();
+          }
+        }
 
-            if (response.statusCode() == 200) {
-              final GameProfile profile = GENERAL_GSON.fromJson(response.body(),
-                  GameProfile.class);
-              // 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 final IdentifiedKeyImpl key) {
-                if (!key.internalAddHolder(profile.getId())) {
-                  inbound.disconnect(
-                      Component.translatable("multiplayer.disconnect.invalid_public_key"));
-                }
-              }
-              // All went well, initialize the session.
-              mcConnection.setActiveSessionHandler(StateRegistry.LOGIN,
-                  new AuthSessionHandler(server, inbound, profile, true));
-            } else if (response.statusCode() == 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 {} ({})",
-                  response.statusCode(), login.getUsername(), playerIp);
-              inbound.disconnect(Component.translatable("multiplayer.disconnect.authservers_down"));
-            }
-          }, mcConnection.eventLoop())
-          .thenRun(() -> {
-            if (httpClient instanceof final AutoCloseable closeable) {
-              try {
-                closeable.close();
-              } catch (Exception e) {
-                // In Java 21, the HttpClient does not throw any Exception
-                // when trying to clean its resources, so this should not happen
-                logger.error("An unknown error occurred while trying to close an HttpClient", e);
-              }
+        // Go ahead and enable encryption. Once the client sends EncryptionResponse, encryption
+        // is enabled.
+        try {
+          mcConnection.enableEncryption(decryptedSharedSecret);
+        } catch (GeneralSecurityException e) {
+          logger.error("Unable to enable encryption for connection", e);
+          // 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 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(response.uuid)) {
+                inbound.disconnect(
+                    Component.translatable("multiplayer.disconnect.invalid_public_key"));
             }
-          });
+          }
+          //
+          mcConnection.setActiveSessionHandler(StateRegistry.LOGIN, new AuthSessionHandler(
+                  server, inbound, LauncherUtil.makeGameProfile(response), true
+          ));
+        } else if (exception instanceof RequestException) {
+          // Apparently an offline-mode user logged onto this online-mode proxy.
+          logger.error("Unable to authenticate {} with Launcher: {}", login.getUsername(), exception.getMessage());
+          inbound.disconnect(Component.translatable("velocity.error.online-mode-only",
+                  NamedTextColor.RED));
+        } else {
+          // Something else went wrong
+          logger.error("Unable to authenticate {} with Launcher: {}", login.getUsername(), exception.getMessage());
+          inbound.disconnect(Component.translatable("multiplayer.disconnect.authservers_down"));
+        }
+        return null;
+      }, mcConnection.eventLoop()).exceptionally((ex) -> {
+        logger.error("Exception in pre-login stage", ex);
+        return null;
+      });
     } 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..26f8c7e3
--- /dev/null
+++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/util/LauncherUtil.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2023 Velocity Contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.velocitypowered.proxy.connection.util;
+
+import com.velocitypowered.api.util.GameProfile;
+import pro.gravit.launcher.base.Launcher;
+import pro.gravit.launcher.base.events.request.CheckServerRequestEvent;
+import pro.gravit.launcher.base.profiles.PlayerProfile;
+import pro.gravit.launcher.base.profiles.Texture;
+import pro.gravit.utils.helper.SecurityHelper;
+
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+
+public class LauncherUtil {
+    private static final String SESSION_ID_PROPERTY = "launcher_session_id";
+    private static final String HARDWARE_ID_PROPERTY = "launcher_hardware_id";
+    private static final String CUSTOM_PROPERTY_PREFIX = "launcher_";
+    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(), ""));
+        }
+        if(event.sessionId != null) {
+            properties.add(new GameProfile.Property(SESSION_ID_PROPERTY, event.sessionId, ""));
+        }
+        if(event.hardwareId != null) {
+            properties.add(new GameProfile.Property(HARDWARE_ID_PROPERTY, event.hardwareId, ""));
+        }
+        if(event.sessionProperties != null) {
+            for (var e : event.sessionProperties.entrySet()) {
+                properties.add(new GameProfile.Property(CUSTOM_PROPERTY_PREFIX+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;
+            }
+        }
+    }
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 594ed9ff..203b075f 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -5,6 +5,7 @@ dependencyResolutionManagement {
     repositories {
         mavenCentral()
         maven("https://repo.papermc.io/repository/maven-public/")
+        mavenLocal()
     }
 }
 
-- 
2.44.0

