Files
chomens-bot-java/src/main/java/me/chayapak1/chomens_bot/plugins/ChomeNSModIntegrationPlugin.java
2025-04-09 15:14:09 +07:00

279 lines
10 KiB
Java

package me.chayapak1.chomens_bot.plugins;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import me.chayapak1.chomens_bot.Bot;
import me.chayapak1.chomens_bot.chomeNSMod.Encryptor;
import me.chayapak1.chomens_bot.chomeNSMod.Packet;
import me.chayapak1.chomens_bot.chomeNSMod.PacketHandler;
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.chomeNSMod.PayloadMetadata;
import me.chayapak1.chomens_bot.data.chomeNSMod.PayloadState;
import me.chayapak1.chomens_bot.data.logging.LogType;
import me.chayapak1.chomens_bot.data.player.PlayerEntry;
import me.chayapak1.chomens_bot.util.UUIDUtilities;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.TranslationArgument;
import java.lang.reflect.InvocationTargetException;
import java.security.SecureRandom;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
// This is inspired from the ChomeNS Bot Proxy which is in the JavaScript version of ChomeNS Bot.
public class ChomeNSModIntegrationPlugin implements ChatPlugin.Listener, PlayersPlugin.Listener, TickPlugin.Listener {
private static final String ID = "chomens_mod";
private static final int ENCODED_PAYLOAD_LENGTH = 31_000; // just 32767 trimmed "a bit"
private static final long NONCE_EXPIRATION_MS = 30 * 1000; // 30 seconds
private static final SecureRandom RANDOM = new SecureRandom();
public static final List<Class<? extends Packet>> SERVERBOUND_PACKETS = new ArrayList<>();
static {
SERVERBOUND_PACKETS.add(ServerboundSuccessfulHandshakePacket.class);
SERVERBOUND_PACKETS.add(ServerboundRunCoreCommandPacket.class);
SERVERBOUND_PACKETS.add(ServerboundRunCommandPacket.class);
}
private final Bot bot;
private final PacketHandler handler;
private final List<Listener> listeners = new ArrayList<>();
public final List<PlayerEntry> connectedPlayers = Collections.synchronizedList(new ArrayList<>());
private final Map<PlayerEntry, Map<Integer, StringBuilder>> receivedParts = new ConcurrentHashMap<>();
private final List<PayloadMetadata> seenMetadata = Collections.synchronizedList(new ArrayList<>());
public ChomeNSModIntegrationPlugin (Bot bot) {
this.bot = bot;
this.handler = new PacketHandler(bot);
bot.chat.addListener(this);
bot.players.addListener(this);
bot.tick.addListener(this);
}
@Override
public void onSecondTick () {
tryHandshaking();
seenMetadata.removeIf(
metadata -> System.currentTimeMillis() - metadata.timestamp() > NONCE_EXPIRATION_MS
);
}
public void send (PlayerEntry target, Packet packet) {
if (!connectedPlayers.contains(target) && !(packet instanceof ClientboundHandshakePacket)) return;
final ByteBuf buf = Unpooled.buffer();
final PayloadMetadata metadata = generateMetadata();
metadata.serialize(buf);
buf.writeInt(packet.getId());
packet.serialize(buf);
final byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
try {
final int messageId = RANDOM.nextInt();
final String encrypted = Encryptor.encrypt(bytes, bot.config.chomeNSMod.password);
final Iterable<String> split = Splitter.fixedLength(ENCODED_PAYLOAD_LENGTH).split(encrypted);
int i = 1;
for (String part : split) {
final PayloadState state = i == Iterables.size(split)
? PayloadState.DONE
: PayloadState.JOINING;
final Component component = Component.translatable(
"",
Component.text(ID),
Component.text(messageId),
Component.text(state.ordinal()),
Component.text(part)
);
bot.chat.actionBar(component, target.profile.getId());
i++;
}
} catch (Exception ignored) { }
}
private PayloadMetadata generateMetadata () {
final byte[] nonce = new byte[8];
RANDOM.nextBytes(nonce);
final long timestamp = System.currentTimeMillis();
return new PayloadMetadata(nonce, timestamp);
}
private boolean isValidPayload (PayloadMetadata metadata) {
// check if the timestamp is less than the expiration time
if (System.currentTimeMillis() - metadata.timestamp() > NONCE_EXPIRATION_MS) return false;
// check if nonce is replayed in case the server owner
// is not being very nice and decided to pull out
// a replay attack
final boolean valid = !seenMetadata.contains(metadata);
if (valid) seenMetadata.add(metadata);
return valid;
}
private Packet deserialize (byte[] data) {
final ByteBuf buf = Unpooled.wrappedBuffer(data);
final PayloadMetadata metadata = PayloadMetadata.deserialize(buf);
if (!isValidPayload(metadata)) {
bot.logger.log(
LogType.INFO,
Component.translatable(
"Ignoring suspected replay attack payload with metadata: %s",
Component.text(metadata.toString()) // PayloadMetadata has toString()
)
);
return null;
}
final int id = buf.readInt();
final Class<? extends Packet> packetClass = SERVERBOUND_PACKETS.get(id);
if (packetClass == null) return null;
try {
return packetClass.getDeclaredConstructor(ByteBuf.class).newInstance(buf);
} catch (NoSuchMethodException | InvocationTargetException | InstantiationException |
IllegalAccessException e) {
return null;
}
}
@Override
public boolean systemMessageReceived (Component component, String string, String ansi) {
if (
!(component instanceof TranslatableComponent translatableComponent) ||
!translatableComponent.key().isEmpty()
) return true;
final List<TranslationArgument> arguments = translatableComponent.arguments();
if (
arguments.size() != 5 ||
!(arguments.get(0).asComponent() instanceof TextComponent idTextComponent) ||
!(arguments.get(1).asComponent() instanceof TextComponent uuidTextComponent) ||
!(arguments.get(2).asComponent() instanceof TextComponent messageIdTextComponent) ||
!(arguments.get(3).asComponent() instanceof TextComponent payloadStateTextComponent) ||
!(arguments.get(4).asComponent() instanceof TextComponent payloadTextComponent) ||
!idTextComponent.content().equals(ID)
) return true;
try {
final UUID uuid = UUIDUtilities.tryParse(uuidTextComponent.content());
if (uuid == null) return true;
final PlayerEntry player = bot.players.getEntry(uuid);
if (player == null) return false;
final int messageId = Integer.parseInt(messageIdTextComponent.content());
final int payloadStateIndex = Integer.parseInt(payloadStateTextComponent.content());
final PayloadState payloadState = PayloadState.values()[payloadStateIndex];
receivedParts.putIfAbsent(player, new ConcurrentHashMap<>());
final Map<Integer, StringBuilder> playerReceivedParts = receivedParts.get(player);
if (!playerReceivedParts.containsKey(messageId)) playerReceivedParts.put(messageId, new StringBuilder());
final StringBuilder builder = playerReceivedParts.get(messageId);
final String payload = payloadTextComponent.content();
builder.append(payload);
playerReceivedParts.put(messageId, builder);
if (payloadState == PayloadState.DONE) {
playerReceivedParts.remove(messageId);
final byte[] decryptedFullPayload = Encryptor.decrypt(
builder.toString(),
bot.config.chomeNSMod.password
);
final Packet packet = deserialize(decryptedFullPayload);
if (
packet == null ||
(
!(packet instanceof ServerboundSuccessfulHandshakePacket) &&
!connectedPlayers.contains(player)
)
) return false;
handlePacket(player, packet);
}
} catch (Exception ignored) { }
return false;
}
private void tryHandshaking () {
for (String username : bot.config.chomeNSMod.players) {
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 ServerboundSuccessfulHandshakePacket) {
connectedPlayers.removeIf(eachPlayer -> eachPlayer.equals(player));
connectedPlayers.add(player);
}
handler.handlePacket(player, packet);
for (Listener listener : listeners) listener.packetReceived(player, packet);
}
@Override
public void playerLeft (PlayerEntry target) {
connectedPlayers.removeIf(player -> player.equals(target));
receivedParts.remove(target);
}
@SuppressWarnings("unused")
public interface Listener {
default void packetReceived (PlayerEntry player, Packet packet) { }
}
}