diff --git a/gradle.properties b/gradle.properties index 3cebe8b..02ba5b5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ org.gradle.jvmargs=-Xmx2G minecraft_version=1.21.4 yarn_mappings=1.21.4+build.2 loader_version=0.16.9 -fabric_version=0.112.2+1.21.4 +fabric_version=0.113.0+1.21.4 # Other Dependencies jb_annotations_version = 23.0.0 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b1d008c..97955bb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] cca = "6.1.2" fpa = "0.2-SNAPSHOT" -elmendorf = "0.13.0" +elmendorf = "0.14.0" [libraries] cca-base = { module = "org.ladysnake.cardinal-components-api:cardinal-components-base", version.ref = "cca" } diff --git a/src/testmod/java/org/ladysnake/impersonatest/ImpersonateClientTest.java b/src/testmod/java/org/ladysnake/impersonatest/ImpersonateClientTest.java new file mode 100644 index 0000000..dcc0e10 --- /dev/null +++ b/src/testmod/java/org/ladysnake/impersonatest/ImpersonateClientTest.java @@ -0,0 +1,102 @@ +package org.ladysnake.impersonatest; + +import com.mojang.authlib.GameProfile; +import net.fabricmc.fabric.api.client.gametest.v1.ClientGameTestContext; +import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest; +import net.fabricmc.fabric.api.client.gametest.v1.TestDedicatedServerContext; +import net.fabricmc.fabric.api.client.gametest.v1.TestServerConnection; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.option.Perspective; +import net.minecraft.client.util.InputUtil; +import net.minecraft.network.DisconnectionInfo; +import net.minecraft.network.NetworkSide; +import net.minecraft.network.packet.c2s.common.SyncedClientOptions; +import net.minecraft.network.packet.c2s.play.ChatMessageC2SPacket; +import net.minecraft.server.PlayerManager; +import net.minecraft.server.network.ConnectedClientData; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.text.Text; +import net.minecraft.util.math.Vec3d; +import org.jetbrains.annotations.NotNull; +import org.ladysnake.elmendorf.impl.MockClientConnection; +import org.ladysnake.impersonate.impl.ImpersonateGamerules; + +import java.security.NoSuchAlgorithmException; +import java.util.UUID; + +public class ImpersonateClientTest implements FabricClientGameTest { + + public static final String MOCK_PLAYER_NAME = "test-mock-player"; + + @Override + public void runTest(ClientGameTestContext context) { + GameProfile profile = context.computeOnClient(MinecraftClient::getGameProfile); + try (TestDedicatedServerContext server = context.worldBuilder().createServer()) { + try (TestServerConnection connection = server.connect()) { + server.runOnServer(s -> s.getPlayerManager().addToOperators(profile)); + connection.getClientWorld().waitForChunksRender(); + context.runOnClient(client -> client.options.setPerspective(Perspective.THIRD_PERSON_FRONT)); + sendChatMessage(context); + context.takeScreenshot("1_before_impersonation", 5); + context.runOnClient(client -> client.getNetworkHandler().sendChatCommand("impersonate disguise as Pyrofab")); + context.waitTicks(20); + sendChatMessage(context); + context.takeScreenshot("2_impersonating_pyrofab", 5); + context.runOnClient(client -> client.getNetworkHandler().sendChatCommand("impersonate disguise as doctor4t")); + context.waitTicks(20); + sendChatMessage(context); + context.takeScreenshot("3_impersonating_doctor4t", 5); + Vec3d newPlayerPos = context.computeOnClient(mc -> mc.player.getPos().add(2, 0, 1)); + UUID otherPlayerId = UUID.randomUUID(); + ServerPlayerEntity otherPlayer = server.computeOnServer(s -> spawnServerPlayer(s.getOverworld(), newPlayerPos, otherPlayerId, MOCK_PLAYER_NAME)); + server.runOnServer(s -> + otherPlayer.networkHandler.onChatMessage(createChatMessagePacket(otherPlayerId, "Hi")) + ); + context.waitTicks(20); + context.takeScreenshot("4_other_player_joined", 5); + context.runOnClient(client -> client.getNetworkHandler().sendChatCommand("impersonate disguise as Xiribidus " + MOCK_PLAYER_NAME)); + context.waitTicks(20); + server.runOnServer(s -> + otherPlayer.networkHandler.onChatMessage(createChatMessagePacket(otherPlayerId, "Hi again")) + ); + context.takeScreenshot("5_other_player_impersonating_xiribidus", 5); + server.runOnServer(s -> s.getGameRules().get(ImpersonateGamerules.OP_REVEAL_IMPERSONATIONS).set(false, s)); + server.runOnServer(s -> + otherPlayer.networkHandler.onChatMessage(createChatMessagePacket(otherPlayerId, "Goodbye")) + ); + context.takeScreenshot("6_other_player_impersonating_xiribidus_no_reveal", 5); + server.runOnServer(s -> otherPlayer.networkHandler.onDisconnected(new DisconnectionInfo(Text.empty()))); + context.takeScreenshot("7_other_player_impersonating_xiribidus_no_reveal_disconnected", 5); + context.waitTicks(100); + } + } + } + + private static @NotNull ChatMessageC2SPacket createChatMessagePacket(UUID otherPlayerId, String text) { + try { + return ImpersonateTestSuite.createChatMessagePacket(otherPlayerId, text); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + public ServerPlayerEntity spawnServerPlayer(ServerWorld world, Vec3d pos, UUID uuid, String name) { + GameProfile profile = new GameProfile(uuid, name); + var connection = new MockClientConnection(NetworkSide.SERVERBOUND); + SyncedClientOptions clientOptions = SyncedClientOptions.createDefault(); + PlayerManager playerManager = world.getServer().getPlayerManager(); + ServerPlayerEntity mockPlayer = playerManager.createPlayer(profile, clientOptions); + playerManager.onPlayerConnect(connection, mockPlayer, ConnectedClientData.createDefault(profile, false)); + mockPlayer.setPosition(pos); + return mockPlayer; + } + + + private static void sendChatMessage(ClientGameTestContext context) { + context.getInput().pressKey(options -> options.chatKey); + context.waitTick(); + context.getInput().typeChars("Hello, World!"); + context.getInput().pressKey(InputUtil.GLFW_KEY_ENTER); + } +} diff --git a/src/testmod/java/org/ladysnake/impersonatest/ImpersonateClientTestSuite.java b/src/testmod/java/org/ladysnake/impersonatest/ImpersonateClientTestSuite.java deleted file mode 100644 index 26c77fc..0000000 --- a/src/testmod/java/org/ladysnake/impersonatest/ImpersonateClientTestSuite.java +++ /dev/null @@ -1,68 +0,0 @@ -package org.ladysnake.impersonatest; - -import net.fabricmc.fabric.api.client.gametest.v1.ClientGameTestContext; -import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest; -import net.fabricmc.loader.api.FabricLoader; -import net.minecraft.SharedConstants; -import net.minecraft.client.gui.screen.GameMenuScreen; -import net.minecraft.client.gui.screen.TitleScreen; -import net.minecraft.client.gui.screen.world.CreateWorldScreen; -import net.minecraft.client.gui.screen.world.LevelLoadingScreen; -import net.minecraft.client.gui.screen.world.SelectWorldScreen; -import net.minecraft.client.option.Perspective; -import net.minecraft.client.util.InputUtil; - -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.file.DirectoryStream; -import java.nio.file.Files; -import java.nio.file.Path; - -public class ImpersonateClientTestSuite implements FabricClientGameTest { - @Override - public void runTest(ClientGameTestContext context) { - context.waitForScreen(TitleScreen.class); - context.clickScreenButton("menu.singleplayer"); - - if (!isDirEmpty(FabricLoader.getInstance().getGameDir().resolve("saves"))) { - context.waitForScreen(SelectWorldScreen.class); - context.clickScreenButton("selectWorld.create"); - } - - context.waitForScreen(CreateWorldScreen.class); - context.runOnClient(client -> CreateWorldScreen.showTestWorld(client, client.currentScreen)); - context.waitForScreen(CreateWorldScreen.class); - context.clickScreenButton("selectWorld.create"); - - context.waitFor(client -> client.player != null && !(client.currentScreen instanceof LevelLoadingScreen), 30 * SharedConstants.TICKS_PER_MINUTE); - context.waitTicks(20); // a few more ticks for world loading - - context.runOnClient(client -> client.options.setPerspective(Perspective.THIRD_PERSON_FRONT)); - sendChatMessage(context, "before_impersonation"); - context.runOnClient(client -> client.getNetworkHandler().sendChatCommand("impersonate disguise as Pyrofab")); - context.waitTicks(20); - sendChatMessage(context, "impersonating_pyrofab"); - context.runOnClient(client -> client.getNetworkHandler().sendChatCommand("impersonate disguise as doctor4t")); - context.waitTicks(20); - sendChatMessage(context, "impersonating_doctor4t"); - - context.setScreen(() -> new GameMenuScreen(true)); - context.clickScreenButton("menu.returnToMenu"); - } - - private static void sendChatMessage(ClientGameTestContext context, String screenshotName) { - context.getInput().pressKey(options -> options.chatKey); - context.waitTick(); - context.getInput().typeChars("Hello, World!"); - context.getInput().pressKey(InputUtil.GLFW_KEY_ENTER); - context.takeScreenshot(screenshotName, 5); - } - - private static boolean isDirEmpty(Path path) { - try (DirectoryStream directory = Files.newDirectoryStream(path)) { - return !directory.iterator().hasNext(); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } -} diff --git a/src/testmod/java/org/ladysnake/impersonatest/ImpersonateTestSuite.java b/src/testmod/java/org/ladysnake/impersonatest/ImpersonateTestSuite.java index 0501d83..28a9e2d 100644 --- a/src/testmod/java/org/ladysnake/impersonatest/ImpersonateTestSuite.java +++ b/src/testmod/java/org/ladysnake/impersonatest/ImpersonateTestSuite.java @@ -38,6 +38,7 @@ import net.minecraft.text.Text; import net.minecraft.text.TextContent; import net.minecraft.util.Identifier; +import org.jetbrains.annotations.NotNull; import org.ladysnake.elmendorf.GameTestUtil; import org.ladysnake.elmendorf.impl.MockClientConnection; import org.ladysnake.impersonate.Impersonate; @@ -81,12 +82,7 @@ public void nameGetsRevealed(TestContext ctx) { public void nameInChatGetsRevealed(TestContext ctx) throws NoSuchAlgorithmException { // Do the bare minimum to simulate a legit client with a valid keypair UUID senderUuid = UUID.randomUUID(); - KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); - keyPairGenerator.initialize(2048); - KeyPair keyPair = keyPairGenerator.generateKeyPair(); - Signer signer = Signer.create(keyPair.getPrivate(), "SHA256withRSA"); - MessageChain.Packer messagePacker = new MessageChain(senderUuid, UUID.randomUUID()).getPacker(signer); - LastSeenMessageList lastSeenMessages = LastSeenMessageList.EMPTY; + ChatMessageC2SPacket chatMessagePacket = createChatMessagePacket(senderUuid, "Hi"); ServerPlayerEntity player = new ServerPlayerEntity( ctx.getWorld().getServer(), ctx.getWorld(), @@ -100,9 +96,6 @@ public void nameInChatGetsRevealed(TestContext ctx) throws NoSuchAlgorithmExcept ConnectedClientData.createDefault(player.getGameProfile(), false) ); Impersonator.get(player).impersonate(IMPERSONATION_KEY, new GameProfile(UUID.randomUUID(), "impersonated")); - String text = "Hi"; - Instant timestamp = Instant.now(); - long salt = NetworkEncryptionUtils.SecureRandomUtil.nextLong(); PlayerManager playerManager = ctx.getWorld().getServer().getPlayerManager(); ServerPlayerEntity otherPlayer = ctx.spawnServerPlayer(1, 0, 1); @@ -110,13 +103,7 @@ public void nameInChatGetsRevealed(TestContext ctx) throws NoSuchAlgorithmExcept playerManager.getPlayerList().add(player); playerManager.getPlayerList().add(otherPlayer); playerManager.addToOperators(player.getGameProfile()); - player.networkHandler.onChatMessage(new ChatMessageC2SPacket( - text, - timestamp, - salt, - messagePacker.pack(new MessageBody(text, timestamp, salt, lastSeenMessages)), - new LastSeenMessageList.Acknowledgment(0, new BitSet()) - )); + player.networkHandler.onChatMessage(chatMessagePacket); ctx.verifyConnection(player, conn -> conn.sent( ChatMessageS2CPacket.class, chatPacket -> chatPacket.serializedParameters().name().getString() @@ -134,4 +121,23 @@ public void nameInChatGetsRevealed(TestContext ctx) throws NoSuchAlgorithmExcept playerManager.getPlayerList().remove(otherPlayer); } } + + public static @NotNull ChatMessageC2SPacket createChatMessagePacket(UUID senderUuid, String text) throws NoSuchAlgorithmException { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + Signer signer = Signer.create(keyPair.getPrivate(), "SHA256withRSA"); + MessageChain.Packer messagePacker = new MessageChain(senderUuid, UUID.randomUUID()).getPacker(signer); + LastSeenMessageList lastSeenMessages = LastSeenMessageList.EMPTY; + Instant timestamp = Instant.now(); + long salt = NetworkEncryptionUtils.SecureRandomUtil.nextLong(); + ChatMessageC2SPacket chatMessagePacket = new ChatMessageC2SPacket( + text, + timestamp, + salt, + messagePacker.pack(new MessageBody(text, timestamp, salt, lastSeenMessages)), + new LastSeenMessageList.Acknowledgment(0, new BitSet()) + ); + return chatMessagePacket; + } } diff --git a/src/testmod/resources/fabric.mod.json b/src/testmod/resources/fabric.mod.json index 90909ae..32435b3 100644 --- a/src/testmod/resources/fabric.mod.json +++ b/src/testmod/resources/fabric.mod.json @@ -25,7 +25,7 @@ "org.ladysnake.impersonatest.ImpersonateTestSuite" ], "fabric-client-gametest": [ - "org.ladysnake.impersonatest.ImpersonateClientTestSuite" + "org.ladysnake.impersonatest.ImpersonateClientTest" ] }, "depends": {