feat: make the chomens mod integration feel more like chomens proxy and also fix AuthPlugin

most stuff are still pretty broken, there's no chunking things yet, so long datas will simply just break, but it works!
This commit is contained in:
Chayapak 2025-03-25 19:14:27 +07:00
parent 265a35080b
commit a2b564abf5
Signed by: ChomeNS
SSH key fingerprint: SHA256:0YoxhdyXsgbc0nfeB2N6FYE60mxMU7DS4uCUMaw2mvA
11 changed files with 228 additions and 228 deletions

View file

@ -1 +1 @@
2160
2195

View file

@ -133,7 +133,7 @@ public class Main {
// initialize plugins
console = new ConsolePlugin(config);
LoggerPlugin.init();
AuthPlugin.init(config);
ChomeNSModIntegrationPlugin.init();
if (config.database.enabled) database = new DatabasePlugin(config);
if (config.discord.enabled) discord = new DiscordPlugin(config);
if (config.irc.enabled) new IRCPlugin(config);

View file

@ -0,0 +1,65 @@
package me.chayapak1.chomens_bot.chomeNSMod;
import me.chayapak1.chomens_bot.Bot;
import me.chayapak1.chomens_bot.chomeNSMod.clientboundPackets.ClientboundCoreOutputPacket;
import me.chayapak1.chomens_bot.chomeNSMod.serverboundPackets.ServerboundRunCommandPacket;
import me.chayapak1.chomens_bot.chomeNSMod.serverboundPackets.ServerboundRunCoreCommandPacket;
import me.chayapak1.chomens_bot.command.ChomeNSModCommandContext;
import me.chayapak1.chomens_bot.data.player.PlayerEntry;
import net.kyori.adventure.text.Component;
import java.util.concurrent.CompletableFuture;
public class PacketHandler {
private final Bot bot;
public PacketHandler (Bot bot) {
this.bot = bot;
}
public void handlePacket (PlayerEntry player, Packet packet) {
if (packet instanceof ServerboundRunCoreCommandPacket t_packet) handlePacket(player, t_packet);
else if (packet instanceof ServerboundRunCommandPacket t_packet) handlePacket(player, t_packet);
}
private void handlePacket (PlayerEntry player, ServerboundRunCoreCommandPacket packet) {
final CompletableFuture<Component> future = bot.core.runTracked(packet.command);
if (future == null) {
bot.chomeNSMod.send(
player,
new ClientboundCoreOutputPacket(
packet.runID,
Component.empty()
)
);
return;
}
future.thenApply(output -> {
bot.chomeNSMod.send(
player,
new ClientboundCoreOutputPacket(
packet.runID,
output
)
);
return null;
});
}
private void handlePacket (PlayerEntry player, ServerboundRunCommandPacket packet) {
final String input = packet.input; // the input is raw, no prefix included
final ChomeNSModCommandContext context = new ChomeNSModCommandContext(
bot,
player
);
final Component component = bot.commandHandler.executeCommand(input, context, null);
if (component != null) context.sendOutput(component);
}
}

View file

@ -0,0 +1,28 @@
package me.chayapak1.chomens_bot.chomeNSMod.clientboundPackets;
import io.netty.buffer.ByteBuf;
import me.chayapak1.chomens_bot.chomeNSMod.Packet;
import me.chayapak1.chomens_bot.chomeNSMod.Types;
import net.kyori.adventure.text.Component;
public class ClientboundCommandOutputPacket implements Packet {
public final Component output;
public ClientboundCommandOutputPacket (Component output) {
this.output = output;
}
public ClientboundCommandOutputPacket (ByteBuf buf) {
this.output = Types.readComponent(buf);
}
@Override
public int getId () {
return 2;
}
@Override
public void serialize (ByteBuf buf) {
Types.writeComponent(buf, this.output);
}
}

View file

@ -3,20 +3,19 @@ package me.chayapak1.chomens_bot.chomeNSMod.clientboundPackets;
import io.netty.buffer.ByteBuf;
import me.chayapak1.chomens_bot.chomeNSMod.Packet;
public class ClientboundSuccessfulHandshakePacket implements Packet {
public ClientboundSuccessfulHandshakePacket () {
public class ClientboundHandshakePacket implements Packet {
public ClientboundHandshakePacket() {
}
public ClientboundSuccessfulHandshakePacket (ByteBuf buf) {
public ClientboundHandshakePacket(ByteBuf buf) {
}
@Override
public int getId() {
public int getId () {
return 0;
}
@Override
public void serialize (ByteBuf buf) {
}
}

View file

@ -0,0 +1,27 @@
package me.chayapak1.chomens_bot.chomeNSMod.serverboundPackets;
import io.netty.buffer.ByteBuf;
import me.chayapak1.chomens_bot.chomeNSMod.Packet;
import me.chayapak1.chomens_bot.chomeNSMod.Types;
public class ServerboundRunCommandPacket implements Packet {
public final String input;
public ServerboundRunCommandPacket (String input) {
this.input = input;
}
public ServerboundRunCommandPacket (ByteBuf buf) {
this.input = Types.readString(buf);
}
@Override
public int getId () {
return 2;
}
@Override
public void serialize (ByteBuf buf) {
Types.writeString(buf, this.input);
}
}

View file

@ -3,19 +3,20 @@ package me.chayapak1.chomens_bot.chomeNSMod.serverboundPackets;
import io.netty.buffer.ByteBuf;
import me.chayapak1.chomens_bot.chomeNSMod.Packet;
public class ServerboundHandshakePacket implements Packet {
public ServerboundHandshakePacket () {
public class ServerboundSuccessfulHandshakePacket implements Packet {
public ServerboundSuccessfulHandshakePacket() {
}
public ServerboundHandshakePacket (ByteBuf buf) {
public ServerboundSuccessfulHandshakePacket(ByteBuf buf) {
}
@Override
public int getId () {
public int getId() {
return 0;
}
@Override
public void serialize (ByteBuf buf) {
}
}

View file

@ -0,0 +1,30 @@
package me.chayapak1.chomens_bot.command;
import me.chayapak1.chomens_bot.Bot;
import me.chayapak1.chomens_bot.chomeNSMod.clientboundPackets.ClientboundCommandOutputPacket;
import me.chayapak1.chomens_bot.data.player.PlayerEntry;
import net.kyori.adventure.text.Component;
public class ChomeNSModCommandContext extends CommandContext {
public ChomeNSModCommandContext (Bot bot, PlayerEntry sender) {
super(
bot,
".cbot ", // intentionally hardcoded
sender,
true
);
}
@Override
public void sendOutput (Component component) {
bot.chomeNSMod.send(
sender,
new ClientboundCommandOutputPacket(component)
);
}
@Override
public Component displayName () {
return sender.displayName;
}
}

View file

@ -1,87 +1,15 @@
package me.chayapak1.chomens_bot.plugins;
import me.chayapak1.chomens_bot.Bot;
import me.chayapak1.chomens_bot.Configuration;
import me.chayapak1.chomens_bot.data.logging.LogType;
import me.chayapak1.chomens_bot.data.player.PlayerEntry;
import me.chayapak1.chomens_bot.util.LoggerUtilities;
import me.chayapak1.chomens_bot.util.UUIDUtilities;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.NamedTextColor;
import org.geysermc.mcprotocollib.network.event.session.DisconnectedEvent;
import javax.crypto.Cipher;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.concurrent.TimeUnit;
public class AuthPlugin implements PlayersPlugin.Listener, ChatPlugin.Listener {
private static final String ID = "chomens_bot_verify";
private static PrivateKey PRIVATE_KEY;
private static final Path PRIVATE_KEY_PATH = Path.of("private.key");
private static final Path PUBLIC_KEY_PATH = Path.of("public.key");
private static final String BEGIN_PRIVATE_KEY = "-----BEGIN CHOMENS BOT PRIVATE KEY-----";
private static final String END_PRIVATE_KEY = "-----END CHOMENS BOT PRIVATE KEY-----";
private static final String BEGIN_PUBLIC_KEY = "-----BEGIN CHOMENS BOT PUBLIC KEY-----";
private static final String END_PUBLIC_KEY = "-----END CHOMENS BOT PUBLIC KEY-----";
public static void init (Configuration config) {
if (!config.ownerAuthentication.enabled) return;
try {
// let's only check for the private key here
if (!Files.exists(PRIVATE_KEY_PATH)) {
final KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048);
final KeyPair pair = keyGen.generateKeyPair();
// write the keys
// (note: no newline split is intentional)
final String encodedPrivateKey =
BEGIN_PRIVATE_KEY + "\n" +
Base64.getEncoder().encodeToString(pair.getPrivate().getEncoded()) +
"\n" + END_PRIVATE_KEY;
final String encodedPublicKey =
BEGIN_PUBLIC_KEY + "\n" +
Base64.getEncoder().encodeToString(pair.getPublic().getEncoded()) +
"\n" + END_PUBLIC_KEY;
final BufferedWriter privateKeyWriter = Files.newBufferedWriter(PRIVATE_KEY_PATH);
privateKeyWriter.write(encodedPrivateKey);
privateKeyWriter.close();
final BufferedWriter publicKeyWriter = Files.newBufferedWriter(PUBLIC_KEY_PATH);
publicKeyWriter.write(encodedPublicKey);
publicKeyWriter.close();
}
// is this a good way to remove the things?
final String privateKeyString = new String(Files.readAllBytes(PRIVATE_KEY_PATH))
.replace(BEGIN_PRIVATE_KEY + "\n", "")
.replace("\n" + END_PRIVATE_KEY, "")
.replace("\n", "")
.trim();
final byte[] privateKeyBytes = Base64.getDecoder().decode(privateKeyString);
final KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PRIVATE_KEY = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyBytes));
} catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException | IllegalArgumentException e) {
LoggerUtilities.error(e);
}
}
private final Bot bot;
public boolean isAuthenticating = false;
@ -95,8 +23,9 @@ public class AuthPlugin implements PlayersPlugin.Listener, ChatPlugin.Listener {
bot.executor.scheduleAtFixedRate(() -> {
if (!isAuthenticating || !bot.config.ownerAuthentication.enabled) return;
checkAuthenticated();
timeoutCheck();
sendAuthRequestMessage();
}, 500, 500, TimeUnit.MILLISECONDS);
bot.addListener(new Bot.Listener() {
@ -110,13 +39,28 @@ public class AuthPlugin implements PlayersPlugin.Listener, ChatPlugin.Listener {
bot.players.addListener(this);
}
private String decrypt (byte[] data) throws Exception {
final Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, PRIVATE_KEY);
private void checkAuthenticated () {
final PlayerEntry target = bot.players.getEntry(bot.config.ownerName);
final byte[] decryptedBytes = cipher.doFinal(data);
if (target == null) return;
return new String(decryptedBytes);
if (!bot.chomeNSMod.connectedPlayers.contains(target)) return;
isAuthenticating = false;
bot.logger.log(
LogType.AUTH,
Component
.text("Player has been verified")
.color(NamedTextColor.GREEN)
);
bot.chat.tellraw(
Component
.text("You have been verified")
.color(NamedTextColor.GREEN),
target.profile.getId()
);
}
private void timeoutCheck () {
@ -129,18 +73,6 @@ public class AuthPlugin implements PlayersPlugin.Listener, ChatPlugin.Listener {
bot.filterManager.add(target, "Authentication timed out");
}
private void sendAuthRequestMessage () {
final PlayerEntry target = bot.players.getEntry(bot.config.ownerName);
if (target == null) return;
final Component component = Component
.text(ID)
.append(Component.text(UUIDUtilities.selector(bot.profile.getId())));
bot.chat.tellraw(component, target.profile.getId());
}
@Override
public void playerJoined (PlayerEntry target) {
if (!target.profile.getName().equals(bot.config.ownerName) || !bot.options.useCore) return;
@ -149,55 +81,6 @@ public class AuthPlugin implements PlayersPlugin.Listener, ChatPlugin.Listener {
isAuthenticating = true;
}
@Override
public boolean systemMessageReceived (Component component, String string, String ansi) {
if (!bot.config.ownerAuthentication.enabled) return true;
if (!(component instanceof TextComponent textComponent)) return true;
final String id = textComponent.content();
if (!id.equals(ID)) return true;
if (!isAuthenticating) return false;
if (component.children().size() != 1) return true;
if (!(component.children().getFirst() instanceof TextComponent dataComponent)) return true;
final String data = dataComponent.content();
try {
final String decrypted = decrypt(Base64.getDecoder().decode(data));
// what should i use here? should it be the ID?
// or does it even matter?
if (!decrypted.equals(ID)) return false;
isAuthenticating = false;
bot.logger.log(
LogType.AUTH,
Component
.text("Player has been verified")
.color(NamedTextColor.GREEN)
);
final PlayerEntry target = bot.players.getEntry(bot.config.ownerName);
if (target == null) return false; // sad :(
bot.chat.tellraw(
Component
.text("You have been verified")
.color(NamedTextColor.GREEN),
target.profile.getId()
);
} catch (Exception ignored) {}
return false;
}
@Override
public void playerLeft (PlayerEntry target) {
if (!target.profile.getName().equals(bot.config.ownerName)) return;

View file

@ -3,14 +3,15 @@ package me.chayapak1.chomens_bot.plugins;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import me.chayapak1.chomens_bot.Bot;
import me.chayapak1.chomens_bot.Configuration;
import me.chayapak1.chomens_bot.chomeNSMod.Packet;
import me.chayapak1.chomens_bot.chomeNSMod.PacketHandler;
import me.chayapak1.chomens_bot.chomeNSMod.Types;
import me.chayapak1.chomens_bot.chomeNSMod.clientboundPackets.ClientboundCoreOutputPacket;
import me.chayapak1.chomens_bot.chomeNSMod.clientboundPackets.ClientboundSuccessfulHandshakePacket;
import me.chayapak1.chomens_bot.chomeNSMod.serverboundPackets.ServerboundHandshakePacket;
import me.chayapak1.chomens_bot.chomeNSMod.clientboundPackets.ClientboundHandshakePacket;
import me.chayapak1.chomens_bot.chomeNSMod.serverboundPackets.ServerboundRunCommandPacket;
import me.chayapak1.chomens_bot.chomeNSMod.serverboundPackets.ServerboundRunCoreCommandPacket;
import me.chayapak1.chomens_bot.chomeNSMod.serverboundPackets.ServerboundSuccessfulHandshakePacket;
import me.chayapak1.chomens_bot.data.player.PlayerEntry;
import me.chayapak1.chomens_bot.util.Ascii85;
import me.chayapak1.chomens_bot.util.LoggerUtilities;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
@ -27,19 +28,19 @@ import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
// This is inspired from the ChomeNS Bot Proxy which is in the JavaScript version of ChomeNS Bot.
// Right now it is not really used anywhere, so you'll see duplicate codes from AuthPlugin
public class ChomeNSModIntegrationPlugin implements ChatPlugin.Listener, PlayersPlugin.Listener {
private static final String ID = "chomens_mod";
public static final List<Class<? extends Packet>> SERVERBOUND_PACKETS = new ArrayList<>();
static {
SERVERBOUND_PACKETS.add(ServerboundHandshakePacket.class);
SERVERBOUND_PACKETS.add(ServerboundSuccessfulHandshakePacket.class);
SERVERBOUND_PACKETS.add(ServerboundRunCoreCommandPacket.class);
SERVERBOUND_PACKETS.add(ServerboundRunCommandPacket.class);
}
private static PrivateKey PRIVATE_KEY;
@ -56,9 +57,7 @@ public class ChomeNSModIntegrationPlugin implements ChatPlugin.Listener, Players
private static final String BEGIN_PUBLIC_KEY = "-----BEGIN CHOMENS BOT PUBLIC KEY-----";
private static final String END_PUBLIC_KEY = "-----END CHOMENS BOT PUBLIC KEY-----";
public static void init (Configuration config) {
if (!config.ownerAuthentication.enabled) return;
public static void init () {
try {
// let's only check for the private key here
if (!Files.exists(PRIVATE_KEY_PATH)) {
@ -132,15 +131,20 @@ public class ChomeNSModIntegrationPlugin implements ChatPlugin.Listener, Players
private final Bot bot;
private final PacketHandler handler;
private final List<Listener> listeners = new ArrayList<>();
public final List<PlayerEntry> connectedPlayers = new ArrayList<>();
public ChomeNSModIntegrationPlugin (Bot bot) {
this.bot = bot;
this.handler = new PacketHandler(bot);
bot.chat.addListener(this);
bot.players.addListener(this);
bot.executor.scheduleAtFixedRate(this::tryHandshaking, 1, 1, TimeUnit.SECONDS);
}
public byte[] decrypt (byte[] data) throws Exception {
@ -160,11 +164,11 @@ public class ChomeNSModIntegrationPlugin implements ChatPlugin.Listener, Players
final byte[] encryptedBytes = cipher.doFinal(data);
return Base64.getEncoder().encodeToString(encryptedBytes);
return Ascii85.encode(encryptedBytes);
}
public void send (PlayerEntry target, Packet packet) {
if (!connectedPlayers.contains(target)) return;
if (!connectedPlayers.contains(target) && !(packet instanceof ClientboundHandshakePacket)) return; // LoL sus check
final ByteBuf buf = Unpooled.buffer();
@ -177,11 +181,13 @@ public class ChomeNSModIntegrationPlugin implements ChatPlugin.Listener, Players
try {
final String encrypted = encrypt(target.profile.getName(), bytes);
final Component component = Component
.text(ID)
.append(Component.text(encrypted));
final Component component = Component.translatable(
"",
Component.text(ID),
Component.text(encrypted)
);
bot.chat.tellraw(component, target.profile.getId());
bot.chat.actionBar(component, target.profile.getId());
} catch (Exception ignored) {}
}
@ -222,7 +228,7 @@ public class ChomeNSModIntegrationPlugin implements ChatPlugin.Listener, Players
final String data = dataComponent.content();
try {
final byte[] decrypted = decrypt(Base64.getDecoder().decode(data));
final byte[] decrypted = decrypt(Ascii85.decode(data));
final Pair<PlayerEntry, Packet> deserialized = deserialize(decrypted);
@ -232,72 +238,31 @@ public class ChomeNSModIntegrationPlugin implements ChatPlugin.Listener, Players
final Packet packet = deserialized.getValue();
handlePacket(player, packet);
// isAuthenticating = false;
//
// bot.logger.log(
// LogType.AUTH,
// Component
// .text("Player has been verified")
// .color(NamedTextColor.GREEN)
// );
//
// final PlayerEntry target = bot.players.getEntry(bot.config.ownerName);
//
// if (target == null) return false; // sad :(
//
// bot.chat.tellraw(
// Component
// .text("You have been verified")
// .color(NamedTextColor.GREEN),
// target.profile.getId()
// );
} catch (Exception ignored) {}
return false;
}
private void tryHandshaking () {
// is looping through the usernames from the client public keys list a good idea?
for (String username : CLIENT_PUBLIC_KEYS.keySet()) {
final PlayerEntry target = bot.players.getEntry(username);
if (target == null || connectedPlayers.contains(target)) continue;
send(target, new ClientboundHandshakePacket());
}
}
private void handlePacket (PlayerEntry player, Packet packet) {
if (packet instanceof ServerboundHandshakePacket t_packet) handlePacket(player, t_packet);
else if (packet instanceof ServerboundRunCoreCommandPacket t_packet) handlePacket(player, t_packet);
for (Listener listener : listeners) listener.packetReceived(player, packet);
}
private void handlePacket (PlayerEntry player, ServerboundHandshakePacket ignoredPacket) {
connectedPlayers.remove(player);
connectedPlayers.add(player);
send(player, new ClientboundSuccessfulHandshakePacket());
}
private void handlePacket (PlayerEntry player, ServerboundRunCoreCommandPacket packet) {
final CompletableFuture<Component> future = bot.core.runTracked(packet.command);
if (future == null) {
send(
player,
new ClientboundCoreOutputPacket(
packet.runID,
Component.empty()
)
);
return;
if (packet instanceof ServerboundSuccessfulHandshakePacket) {
connectedPlayers.remove(player);
connectedPlayers.add(player);
}
future.thenApply(output -> {
send(
player,
new ClientboundCoreOutputPacket(
packet.runID,
output
)
);
handler.handlePacket(player, packet);
return null;
});
for (Listener listener : listeners) listener.packetReceived(player, packet);
}
@Override

View file

@ -86,7 +86,8 @@ public class CommandHandlerPlugin {
) {
final boolean inGame = context instanceof PlayerCommandContext;
final boolean discord = context instanceof DiscordCommandContext;
final boolean console = context instanceof ConsoleCommandContext;
final boolean bypass = context instanceof ConsoleCommandContext || context instanceof ChomeNSModCommandContext;
if (commandPerSecond > 100) return null;
@ -105,7 +106,7 @@ public class CommandHandlerPlugin {
if (command == null && !inGame) return Component.text("Unknown command: " + commandName).color(NamedTextColor.RED);
else if (command == null) return null;
if (!console && disabled) return Component.text("ChomeNS Bot is currently disabled").color(NamedTextColor.RED);
if (!bypass && disabled) return Component.text("ChomeNS Bot is currently disabled").color(NamedTextColor.RED);
final TrustLevel trustLevel = command.trustLevel;
@ -118,7 +119,7 @@ public class CommandHandlerPlugin {
final String[] args = Arrays.copyOfRange(splitInput, (trustLevel != TrustLevel.PUBLIC && inGame) ? 2 : 1, splitInput.length);
if (command.trustLevel != TrustLevel.PUBLIC && !console) {
if (command.trustLevel != TrustLevel.PUBLIC && !bypass) {
if (discord) {
final Member member = event.getMember();
@ -167,7 +168,8 @@ public class CommandHandlerPlugin {
}
}
if (!console && command.consoleOnly) return Component.text("This command can only be run via console").color(NamedTextColor.RED);
// should i give access to all bypass contexts instead of only console?
if (!bypass && command.consoleOnly) return Component.text("This command can only be run via console").color(NamedTextColor.RED);
// should these be here?
context.fullArgs = fullArgs;