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

---
 build.gradle.kts                              |   2 +-
 proxy/build.gradle.kts                        |   3 +-
 .../client/InitialLoginSessionHandler.java    | 143 ++++++++----------
 .../proxy/connection/util/LauncherUtil.java   |  89 +++++++++++
 settings.gradle.kts                           |   2 +
 5 files changed, 155 insertions(+), 84 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 9e862dad..93b84640 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -12,7 +12,7 @@ subprojects {
 
     java {
         toolchain {
-            languageVersion.set(JavaLanguageVersion.of(17))
+            languageVersion.set(JavaLanguageVersion.of(21))
         }
     }
 
diff --git a/proxy/build.gradle.kts b/proxy/build.gradle.kts
index f345f232..89545bf2 100644
--- a/proxy/build.gradle.kts
+++ b/proxy/build.gradle.kts
@@ -128,7 +128,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.7.0-SNAPSHOT")
+    compileOnly("pro.gravit.launcher:launcher-ws-api:5.7.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 00187d34..67a6762b 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;
@@ -42,22 +41,22 @@ import com.velocitypowered.proxy.protocol.packet.LoginPluginResponsePacket;
 import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket;
 import com.velocitypowered.proxy.util.VelocityProperties;
 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.request.RequestException;
+import pro.gravit.launcher.core.api.LauncherAPIHolder;
 
 /**
  * Handles authenticating the player to Mojang's servers.
@@ -200,87 +199,67 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
 
       byte[] decryptedSharedSecret = decryptRsa(serverKeyPair, packet.getSharedSecret());
       String serverId = generateServerId(decryptedSharedSecret, serverKeyPair.getPublic());
+      LauncherAPIHolder.user().checkServer(login.getUsername(), serverId, true)
+              .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.user().getUUID())) {
+                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 (Throwable 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..198b4828
--- /dev/null
+++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/util/LauncherUtil.java
@@ -0,0 +1,89 @@
+/*
+ * 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.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.velocitypowered.api.util.GameProfile;
+import pro.gravit.launcher.core.api.features.UserFeatureAPI;
+import pro.gravit.launcher.core.api.model.Texture;
+
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+
+public class LauncherUtil {
+    private static Gson gson = new GsonBuilder().create();
+    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(UserFeatureAPI.CheckServerResponse response) {
+        List<GameProfile.Property> properties = new ArrayList<>();
+        for (var e : response.user().getProperties().entrySet()) {
+            properties.add(new GameProfile.Property(e.getKey(), e.getValue(), ""));
+        }
+        if(response.sessionId() != null) {
+            properties.add(new GameProfile.Property(SESSION_ID_PROPERTY, response.sessionId(), ""));
+        }
+        if(response.hardwareId() != null) {
+            properties.add(new GameProfile.Property(HARDWARE_ID_PROPERTY, response.hardwareId(), ""));
+        }
+        if(response.sessionProperties() != null) {
+            for (var e : response.sessionProperties().entrySet()) {
+                properties.add(new GameProfile.Property(CUSTOM_PROPERTY_PREFIX+e.getKey(), e.getValue(), ""));
+            }
+        }
+        {
+            String key = "textures";
+            GameProfileTextureProperties textureProperty = new GameProfileTextureProperties();
+            textureProperty.profileId = response.user().getUUID().toString().replace("-", "");
+            textureProperty.profileName = response.user().getUsername();
+            textureProperty.timestamp = System.currentTimeMillis();
+            for (var texture : response.user().getAssets().entrySet()) {
+                textureProperty.textures.put(texture.getKey(), new GameProfileTextureProperties.GameTexture(texture.getValue()));
+            }
+            String value = Base64.getEncoder().encodeToString(gson.toJson(textureProperty).getBytes(StandardCharsets.UTF_8));
+            properties.add(new GameProfile.Property(key, value, ""));
+        }
+        return new GameProfile(response.user().getUUID(), response.user().getUsername(), 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.getUrl();
+                this.hash = texture.getHash();
+                this.metadata = texture.getMetadata();
+            }
+        }
+    }
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 40384465..d3a425b6 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -4,7 +4,9 @@ dependencyResolutionManagement {
     repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
     repositories {
         mavenCentral()
+        maven("https://maven.gravitlauncher.com/")
         maven("https://repo.papermc.io/repository/maven-public/")
+        mavenLocal()
     }
 }
 
-- 
2.50.0

