From 85045e1e4a15e0a5657d189e83dd202a2c37f2b0 Mon Sep 17 00:00:00 2001 From: Yuuta Liang Date: Wed, 13 Jul 2022 11:16:27 -0700 Subject: First Commit Signed-off-by: Trumeet --- src/main/java/moe/ymc/acron/Acron.java | 32 +++++ .../java/moe/ymc/acron/MinecraftServerHolder.java | 28 ++++ src/main/java/moe/ymc/acron/auth/Action.java | 10 ++ src/main/java/moe/ymc/acron/auth/Client.java | 9 ++ .../java/moe/ymc/acron/auth/PolicyChecker.java | 39 ++++++ src/main/java/moe/ymc/acron/auth/Rule.java | 10 ++ src/main/java/moe/ymc/acron/c2s/ReqCmd.java | 51 +++++++ src/main/java/moe/ymc/acron/c2s/ReqSetConfig.java | 100 ++++++++++++++ src/main/java/moe/ymc/acron/c2s/Request.java | 6 + src/main/java/moe/ymc/acron/cmd/CmdOut.java | 53 ++++++++ src/main/java/moe/ymc/acron/cmd/CmdQueue.java | 17 +++ .../java/moe/ymc/acron/cmd/CmdResConsumer.java | 33 +++++ src/main/java/moe/ymc/acron/cmd/CmdSrc.java | 34 +++++ src/main/java/moe/ymc/acron/config/Config.java | 30 +++++ .../java/moe/ymc/acron/config/ConfigReloadCmd.java | 41 ++++++ .../java/moe/ymc/acron/config/json/Client.java | 45 +++++++ .../java/moe/ymc/acron/config/json/Config.java | 103 ++++++++++++++ .../json/ConfigDeserializationException.java | 18 +++ .../ymc/acron/config/json/ConfigDeserializer.java | 27 ++++ .../ymc/acron/config/json/ConfigJsonObject.java | 7 + src/main/java/moe/ymc/acron/config/json/Rule.java | 35 +++++ src/main/java/moe/ymc/acron/jvav/Pair.java | 4 + .../moe/ymc/acron/mixin/CommandManagerMixin.java | 38 ++++++ .../moe/ymc/acron/mixin/LivingEntityMixin.java | 41 ++++++ .../acron/mixin/MinecraftDedicatedServerMixin.java | 21 +++ .../moe/ymc/acron/mixin/MinecraftServerMixin.java | 32 +++++ .../mixin/ServerLoginNetworkHandlerMixin.java | 47 +++++++ .../moe/ymc/acron/mixin/ServerNetworkIoMixin.java | 65 +++++++++ .../acron/mixin/ServerPlayNetworkHandlerMixin.java | 43 ++++++ .../ymc/acron/mixin/ServerPlayerEntityMixin.java | 32 +++++ .../java/moe/ymc/acron/net/AcronInitializer.java | 25 ++++ src/main/java/moe/ymc/acron/net/Attributes.java | 13 ++ src/main/java/moe/ymc/acron/net/AuthHandler.java | 91 +++++++++++++ .../moe/ymc/acron/net/ClientConfiguration.java | 20 +++ .../moe/ymc/acron/net/ClientIdentification.java | 11 ++ .../java/moe/ymc/acron/net/HandshakeComplete.java | 7 + .../java/moe/ymc/acron/net/WSFrameHandler.java | 149 +++++++++++++++++++++ src/main/java/moe/ymc/acron/s2c/Entity.java | 19 +++ src/main/java/moe/ymc/acron/s2c/Event.java | 4 + .../java/moe/ymc/acron/s2c/EventCmdDenied.java | 7 + src/main/java/moe/ymc/acron/s2c/EventCmdOut.java | 12 ++ src/main/java/moe/ymc/acron/s2c/EventCmdRes.java | 9 ++ .../java/moe/ymc/acron/s2c/EventDisconnected.java | 10 ++ .../java/moe/ymc/acron/s2c/EventEntityDeath.java | 10 ++ src/main/java/moe/ymc/acron/s2c/EventError.java | 9 ++ src/main/java/moe/ymc/acron/s2c/EventLagging.java | 8 ++ src/main/java/moe/ymc/acron/s2c/EventOk.java | 7 + .../java/moe/ymc/acron/s2c/EventPlayerJoined.java | 8 ++ .../java/moe/ymc/acron/s2c/EventPlayerMessage.java | 10 ++ src/main/java/moe/ymc/acron/s2c/EventQueue.java | 28 ++++ .../moe/ymc/acron/serialization/Serializer.java | 54 ++++++++ 51 files changed, 1562 insertions(+) create mode 100644 src/main/java/moe/ymc/acron/Acron.java create mode 100644 src/main/java/moe/ymc/acron/MinecraftServerHolder.java create mode 100644 src/main/java/moe/ymc/acron/auth/Action.java create mode 100644 src/main/java/moe/ymc/acron/auth/Client.java create mode 100644 src/main/java/moe/ymc/acron/auth/PolicyChecker.java create mode 100644 src/main/java/moe/ymc/acron/auth/Rule.java create mode 100644 src/main/java/moe/ymc/acron/c2s/ReqCmd.java create mode 100644 src/main/java/moe/ymc/acron/c2s/ReqSetConfig.java create mode 100644 src/main/java/moe/ymc/acron/c2s/Request.java create mode 100644 src/main/java/moe/ymc/acron/cmd/CmdOut.java create mode 100644 src/main/java/moe/ymc/acron/cmd/CmdQueue.java create mode 100644 src/main/java/moe/ymc/acron/cmd/CmdResConsumer.java create mode 100644 src/main/java/moe/ymc/acron/cmd/CmdSrc.java create mode 100644 src/main/java/moe/ymc/acron/config/Config.java create mode 100644 src/main/java/moe/ymc/acron/config/ConfigReloadCmd.java create mode 100644 src/main/java/moe/ymc/acron/config/json/Client.java create mode 100644 src/main/java/moe/ymc/acron/config/json/Config.java create mode 100644 src/main/java/moe/ymc/acron/config/json/ConfigDeserializationException.java create mode 100644 src/main/java/moe/ymc/acron/config/json/ConfigDeserializer.java create mode 100644 src/main/java/moe/ymc/acron/config/json/ConfigJsonObject.java create mode 100644 src/main/java/moe/ymc/acron/config/json/Rule.java create mode 100644 src/main/java/moe/ymc/acron/jvav/Pair.java create mode 100644 src/main/java/moe/ymc/acron/mixin/CommandManagerMixin.java create mode 100644 src/main/java/moe/ymc/acron/mixin/LivingEntityMixin.java create mode 100644 src/main/java/moe/ymc/acron/mixin/MinecraftDedicatedServerMixin.java create mode 100644 src/main/java/moe/ymc/acron/mixin/MinecraftServerMixin.java create mode 100644 src/main/java/moe/ymc/acron/mixin/ServerLoginNetworkHandlerMixin.java create mode 100644 src/main/java/moe/ymc/acron/mixin/ServerNetworkIoMixin.java create mode 100644 src/main/java/moe/ymc/acron/mixin/ServerPlayNetworkHandlerMixin.java create mode 100644 src/main/java/moe/ymc/acron/mixin/ServerPlayerEntityMixin.java create mode 100644 src/main/java/moe/ymc/acron/net/AcronInitializer.java create mode 100644 src/main/java/moe/ymc/acron/net/Attributes.java create mode 100644 src/main/java/moe/ymc/acron/net/AuthHandler.java create mode 100644 src/main/java/moe/ymc/acron/net/ClientConfiguration.java create mode 100644 src/main/java/moe/ymc/acron/net/ClientIdentification.java create mode 100644 src/main/java/moe/ymc/acron/net/HandshakeComplete.java create mode 100644 src/main/java/moe/ymc/acron/net/WSFrameHandler.java create mode 100644 src/main/java/moe/ymc/acron/s2c/Entity.java create mode 100644 src/main/java/moe/ymc/acron/s2c/Event.java create mode 100644 src/main/java/moe/ymc/acron/s2c/EventCmdDenied.java create mode 100644 src/main/java/moe/ymc/acron/s2c/EventCmdOut.java create mode 100644 src/main/java/moe/ymc/acron/s2c/EventCmdRes.java create mode 100644 src/main/java/moe/ymc/acron/s2c/EventDisconnected.java create mode 100644 src/main/java/moe/ymc/acron/s2c/EventEntityDeath.java create mode 100644 src/main/java/moe/ymc/acron/s2c/EventError.java create mode 100644 src/main/java/moe/ymc/acron/s2c/EventLagging.java create mode 100644 src/main/java/moe/ymc/acron/s2c/EventOk.java create mode 100644 src/main/java/moe/ymc/acron/s2c/EventPlayerJoined.java create mode 100644 src/main/java/moe/ymc/acron/s2c/EventPlayerMessage.java create mode 100644 src/main/java/moe/ymc/acron/s2c/EventQueue.java create mode 100644 src/main/java/moe/ymc/acron/serialization/Serializer.java (limited to 'src/main/java/moe/ymc/acron') diff --git a/src/main/java/moe/ymc/acron/Acron.java b/src/main/java/moe/ymc/acron/Acron.java new file mode 100644 index 0000000..d6f6214 --- /dev/null +++ b/src/main/java/moe/ymc/acron/Acron.java @@ -0,0 +1,32 @@ +package moe.ymc.acron; + +import moe.ymc.acron.config.Config; +import moe.ymc.acron.config.json.ConfigDeserializer; +import net.fabricmc.api.ModInitializer; +import net.fabricmc.loader.api.FabricLoader; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; +import java.nio.file.Path; + +public class Acron implements ModInitializer { + private static final Logger LOGGER = LogManager.getLogger(); + + @Override + public void onInitialize() { + LOGGER.debug("onInitialize"); + try { + final Path config = FabricLoader + .getInstance().getConfigDir() + .resolve("acron.json"); + if (!config.toFile().exists()) { + throw new IllegalStateException("Cannot find config/acron.json."); + } + final Config cfg = ConfigDeserializer.deserialize(config.toFile(), true); + Config.setGlobalConfig(cfg); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/moe/ymc/acron/MinecraftServerHolder.java b/src/main/java/moe/ymc/acron/MinecraftServerHolder.java new file mode 100644 index 0000000..f522884 --- /dev/null +++ b/src/main/java/moe/ymc/acron/MinecraftServerHolder.java @@ -0,0 +1,28 @@ +package moe.ymc.acron; + +import net.minecraft.server.dedicated.MinecraftDedicatedServer; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; + +public class MinecraftServerHolder { + private static final Logger LOGGER = LogManager.getLogger(); + public static MinecraftDedicatedServer server; + + public static void setServer(@NotNull MinecraftDedicatedServer server) { + if (MinecraftServerHolder.server != null) { + throw new IllegalStateException(); + } + LOGGER.debug("Got MinecraftDedicatedServer on thread {}.", + Thread.currentThread().getName()); + MinecraftServerHolder.server = server; + } + + public static @NotNull MinecraftDedicatedServer getServer() { + if (server == null) { + throw new IllegalStateException(String.format("[%s] getServer() called before a server is ready.", + Thread.currentThread().getName())); + } + return server; + } +} diff --git a/src/main/java/moe/ymc/acron/auth/Action.java b/src/main/java/moe/ymc/acron/auth/Action.java new file mode 100644 index 0000000..17d29a3 --- /dev/null +++ b/src/main/java/moe/ymc/acron/auth/Action.java @@ -0,0 +1,10 @@ +package moe.ymc.acron.auth; + +import com.google.gson.annotations.SerializedName; + +public enum Action { + @SerializedName("allow") + ALLOW, + @SerializedName("deny") + DENY +} diff --git a/src/main/java/moe/ymc/acron/auth/Client.java b/src/main/java/moe/ymc/acron/auth/Client.java new file mode 100644 index 0000000..2124ad4 --- /dev/null +++ b/src/main/java/moe/ymc/acron/auth/Client.java @@ -0,0 +1,9 @@ +package moe.ymc.acron.auth; + +import org.jetbrains.annotations.NotNull; + +public record Client(@NotNull String id, + @NotNull String token, + @NotNull Action policyMode, + @NotNull Rule[] rules) { +} diff --git a/src/main/java/moe/ymc/acron/auth/PolicyChecker.java b/src/main/java/moe/ymc/acron/auth/PolicyChecker.java new file mode 100644 index 0000000..5dea02a --- /dev/null +++ b/src/main/java/moe/ymc/acron/auth/PolicyChecker.java @@ -0,0 +1,39 @@ +package moe.ymc.acron.auth; + +import moe.ymc.acron.jvav.Pair; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; + +public class PolicyChecker { + private static final Logger LOGGER = LogManager.getLogger(); + + public static Pair check(@NotNull Client client, + @NotNull String command) { + for (int i = 0; i < client.rules().length; i++) { + final Rule rule = client.rules()[i]; + if (rule.cmdPattern().matcher(command).matches()) { + if (rule.action() == Action.DENY) { + LOGGER.warn("The command from client {}, `{}`, was " + + "explicitly denied by rule #{} (starting from 1).", + client.id(), + command, + i + 1); + } else { + LOGGER.warn("The command from client {}, `{}`, was " + + "explicitly allowed by rule #{} (starting from 1).", + client.id(), + command, + i + 1); + } + return new Pair<>(rule.action(), rule.display()); + } + } + LOGGER.warn("The command from client {}, `{}`, was " + + "implicitly {} by the default policy mode.", + client.id(), + command, + client.policyMode() == Action.ALLOW ? "allowed" : "denied"); + return new Pair<>(client.policyMode() == Action.ALLOW ? Action.ALLOW : Action.DENY, false); + } +} diff --git a/src/main/java/moe/ymc/acron/auth/Rule.java b/src/main/java/moe/ymc/acron/auth/Rule.java new file mode 100644 index 0000000..55ad0d7 --- /dev/null +++ b/src/main/java/moe/ymc/acron/auth/Rule.java @@ -0,0 +1,10 @@ +package moe.ymc.acron.auth; + +import org.jetbrains.annotations.NotNull; + +import java.util.regex.Pattern; + +public record Rule(@NotNull Pattern cmdPattern, + @NotNull Action action, + boolean display) { +} diff --git a/src/main/java/moe/ymc/acron/c2s/ReqCmd.java b/src/main/java/moe/ymc/acron/c2s/ReqCmd.java new file mode 100644 index 0000000..bcd8d48 --- /dev/null +++ b/src/main/java/moe/ymc/acron/c2s/ReqCmd.java @@ -0,0 +1,51 @@ +package moe.ymc.acron.c2s; + +import com.google.gson.*; +import com.google.gson.annotations.SerializedName; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Type; + +public record ReqCmd(@SerializedName("id") int id, + @SerializedName("cmd") @NotNull String cmd, + @SerializedName("config") @Nullable ReqSetConfig config) + implements Request { + @Override + public void validate() { + if (cmd == null) { + throw new IllegalArgumentException("Property 'cmd' cannot be null."); + } + } + + @Override + public int getId() { + return id; + } + + public static class ReqCmdDeserializer implements JsonDeserializer { + @Override + public ReqCmd deserialize(JsonElement json, + Type typeOfT, + JsonDeserializationContext context) throws JsonParseException { + final JsonObject object = json.getAsJsonObject(); + final int id = object.has("id") ? + object.get("id").getAsInt() : + 0; + final String cmd = object.has("cmd") ? + object.get("cmd").getAsString() : + null; + // We cannot use context#deserialize here + // because RuntimeTypeAdapterFactory keeps kicking in + // and asking for the 'type' property + // which is obviously redundant for an inner field. + // Thus, I pass it directly to the deserializer + // to bypass the RuntimeTypeAdapterFactory. + final ReqSetConfig reqSetConfig = object.has("config") ? + new ReqSetConfig.ReqSetConfigDeserializer() + .deserialize(object.get("config"), ReqSetConfig.class, context) : + null; + return new ReqCmd(id, cmd, reqSetConfig); + } + } +} diff --git a/src/main/java/moe/ymc/acron/c2s/ReqSetConfig.java b/src/main/java/moe/ymc/acron/c2s/ReqSetConfig.java new file mode 100644 index 0000000..dc2c878 --- /dev/null +++ b/src/main/java/moe/ymc/acron/c2s/ReqSetConfig.java @@ -0,0 +1,100 @@ +package moe.ymc.acron.c2s; + +import com.google.gson.*; +import com.google.gson.annotations.SerializedName; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Type; + +public record ReqSetConfig(@SerializedName("id") int id, + @SerializedName("world") @Nullable WorldKey world, + @SerializedName("pos") @Nullable Vec3d pos, + @SerializedName("rot") @Nullable Vec2f rot, + @SerializedName("name") @Nullable String name) + implements Request { + @Override + public void validate() { + } + + @Override + public int getId() { + return 0; + } + + public static class ReqSetConfigDeserializer implements JsonDeserializer { + @Override + public ReqSetConfig deserialize(JsonElement json, + Type typeOfT, + JsonDeserializationContext context) throws JsonParseException { + final JsonObject object = json.getAsJsonObject(); + final int id = object.has("id") ? + object.get("id").getAsInt() : + -1; + final WorldKey world = object.has("world") ? + WorldKey.valueOf(object.get("world").getAsString().toUpperCase()) : + null; + final Vec3d pos = object.has("pos") ? + context.deserialize(object.get("pos"), Vec3d.class) : + null; + final Vec2f rot = object.has("rot") ? + context.deserialize(object.get("rot"), Vec2f.class) : + null; + final String name = object.has("name") ? + object.get("name").getAsString() : + null; + return new ReqSetConfig(-1, + world, + pos, + rot, + name); + } + } + + public enum WorldKey { + OVERWORLD, + NETHER, + END + } + + public record Vec3d(@SerializedName("x") double x, + @SerializedName("y") double y, + @SerializedName("z") double z) { + public static class Vec3dDeserializer implements JsonDeserializer { + @Override + public Vec3d deserialize(JsonElement json, + Type typeOfT, + JsonDeserializationContext context) throws JsonParseException { + final JsonObject object = json.getAsJsonObject(); + final double x = object.has("x") ? + object.get("x").getAsDouble() : + 0.0; + final double y = object.has("y") ? + object.get("y").getAsDouble() : + 0.0; + final double z = object.has("z") ? + object.get("z").getAsDouble() : + 0.0; + return new Vec3d(x, y, z); + } + } + } + + public record Vec2f(@SerializedName("x") float x, + @SerializedName("y") float y) { + public static class Vec2fDeserializer implements JsonDeserializer { + @Override + public Vec2f deserialize(JsonElement json, + Type typeOfT, + JsonDeserializationContext context) throws JsonParseException { + final JsonObject object = json.getAsJsonObject(); + final float x = object.has("x") ? + object.get("x").getAsFloat() : + 0.0f; + final float y = object.has("y") ? + object.get("y").getAsFloat() : + 0.0f; + return new Vec2f(x, y); + } + } + } +} diff --git a/src/main/java/moe/ymc/acron/c2s/Request.java b/src/main/java/moe/ymc/acron/c2s/Request.java new file mode 100644 index 0000000..af81705 --- /dev/null +++ b/src/main/java/moe/ymc/acron/c2s/Request.java @@ -0,0 +1,6 @@ +package moe.ymc.acron.c2s; + +public interface Request { + int getId(); + void validate() throws IllegalArgumentException; +} diff --git a/src/main/java/moe/ymc/acron/cmd/CmdOut.java b/src/main/java/moe/ymc/acron/cmd/CmdOut.java new file mode 100644 index 0000000..717bd0c --- /dev/null +++ b/src/main/java/moe/ymc/acron/cmd/CmdOut.java @@ -0,0 +1,53 @@ +package moe.ymc.acron.cmd; + +import io.netty.channel.Channel; +import moe.ymc.acron.s2c.EventCmdOut; +import moe.ymc.acron.serialization.Serializer; +import net.minecraft.server.command.CommandOutput; +import net.minecraft.text.Text; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +public class CmdOut implements CommandOutput { + private static final Logger LOGGER = LogManager.getLogger(); + + private final @NotNull Channel channel; + private final int id; + private final boolean display; + + public CmdOut(@NotNull Channel channel, + int id, + boolean display) { + this.channel = channel; + this.id = id; + this.display = display; + } + + @Override + public void sendSystemMessage(Text message, UUID sender) { + LOGGER.debug("sendSystemMessage[{}]: {}", + id, + message.getString()); + channel.writeAndFlush( + Serializer.serialize(new EventCmdOut(id, sender, message.getString())) + ); + } + + @Override + public boolean shouldReceiveFeedback() { + return true; + } + + @Override + public boolean shouldTrackOutput() { + return true; + } + + @Override + public boolean shouldBroadcastConsoleToOps() { + return display; + } +} diff --git a/src/main/java/moe/ymc/acron/cmd/CmdQueue.java b/src/main/java/moe/ymc/acron/cmd/CmdQueue.java new file mode 100644 index 0000000..3c49143 --- /dev/null +++ b/src/main/java/moe/ymc/acron/cmd/CmdQueue.java @@ -0,0 +1,17 @@ +package moe.ymc.acron.cmd; + +import io.netty.channel.Channel; +import moe.ymc.acron.MinecraftServerHolder; +import moe.ymc.acron.net.ClientConfiguration; +import org.jetbrains.annotations.NotNull; + +public class CmdQueue { + public static void enqueue(int id, + boolean display, + @NotNull final Channel channel, + @NotNull final ClientConfiguration configuration, + @NotNull final String command) { + MinecraftServerHolder.getServer().enqueueCommand(command, + new CmdSrc(channel, id, display, configuration, MinecraftServerHolder.getServer())); + } +} diff --git a/src/main/java/moe/ymc/acron/cmd/CmdResConsumer.java b/src/main/java/moe/ymc/acron/cmd/CmdResConsumer.java new file mode 100644 index 0000000..13a65f8 --- /dev/null +++ b/src/main/java/moe/ymc/acron/cmd/CmdResConsumer.java @@ -0,0 +1,33 @@ +package moe.ymc.acron.cmd; + +import com.mojang.brigadier.ResultConsumer; +import com.mojang.brigadier.context.CommandContext; +import io.netty.channel.Channel; +import moe.ymc.acron.s2c.EventCmdRes; +import moe.ymc.acron.serialization.Serializer; +import net.minecraft.server.command.ServerCommandSource; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; + +public class CmdResConsumer implements ResultConsumer { + private static final Logger LOGGER = LogManager.getLogger(); + + private final @NotNull Channel channel; + private final int id; + + public CmdResConsumer(@NotNull Channel channel, + int id) { + this.channel = channel; + this.id = id; + } + + @Override + public void onCommandComplete(CommandContext context, boolean success, int result) { + LOGGER.debug("onCommandComplete[{}]: {} {}", + id, + success, + result); + channel.writeAndFlush(Serializer.serialize(new EventCmdRes(id, success, result))); + } +} diff --git a/src/main/java/moe/ymc/acron/cmd/CmdSrc.java b/src/main/java/moe/ymc/acron/cmd/CmdSrc.java new file mode 100644 index 0000000..983b4ed --- /dev/null +++ b/src/main/java/moe/ymc/acron/cmd/CmdSrc.java @@ -0,0 +1,34 @@ +package moe.ymc.acron.cmd; + +import io.netty.channel.Channel; +import moe.ymc.acron.net.ClientConfiguration; +import net.minecraft.command.argument.EntityAnchorArgumentType; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.text.LiteralText; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; + +class CmdSrc extends ServerCommandSource { + private static final Logger LOGGER = LogManager.getLogger(); + + public CmdSrc(@NotNull Channel channel, + int id, + boolean display, + @NotNull ClientConfiguration configuration, + @NotNull MinecraftServer server) { + super(new CmdOut(channel, id, display), + configuration.pos(), + configuration.rot(), + configuration.world(), + 4, + configuration.name(), + new LiteralText(configuration.name()), + server, + null, + false, + new CmdResConsumer(channel, id), + EntityAnchorArgumentType.EntityAnchor.FEET); + } +} diff --git a/src/main/java/moe/ymc/acron/config/Config.java b/src/main/java/moe/ymc/acron/config/Config.java new file mode 100644 index 0000000..3749c25 --- /dev/null +++ b/src/main/java/moe/ymc/acron/config/Config.java @@ -0,0 +1,30 @@ +package moe.ymc.acron.config; + +import moe.ymc.acron.auth.Client; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; + +import java.net.InetAddress; +import java.util.Map; + +public record Config(@NotNull InetAddress address, + int port, + boolean useNativeTransport, + @NotNull Map clients) { + private static final Logger LOGGER = LogManager.getLogger(); + private static Config globalConfig; + + @NotNull + public static Config getGlobalConfig() { + return globalConfig; + } + + public static void setGlobalConfig(@NotNull Config globalConfig) { + Config.globalConfig = globalConfig; + LOGGER.info("Config loaded with {} clients. Listening on {}:{}.", + globalConfig.clients.size(), + globalConfig.address.toString(), + globalConfig.port); + } +} \ No newline at end of file diff --git a/src/main/java/moe/ymc/acron/config/ConfigReloadCmd.java b/src/main/java/moe/ymc/acron/config/ConfigReloadCmd.java new file mode 100644 index 0000000..2774c4d --- /dev/null +++ b/src/main/java/moe/ymc/acron/config/ConfigReloadCmd.java @@ -0,0 +1,41 @@ +package moe.ymc.acron.config; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import moe.ymc.acron.config.json.ConfigDeserializationException; +import moe.ymc.acron.config.json.ConfigDeserializer; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.text.LiteralText; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; +import java.nio.file.Path; + +public class ConfigReloadCmd implements Command { + private static final Logger LOGGER = LogManager.getLogger(); + + @Override + public int run(CommandContext context) throws CommandSyntaxException { + LOGGER.info("Reloading rules."); + try { + final Path config = FabricLoader + .getInstance().getConfigDir() + .resolve("acron.json"); + if (!config.toFile().exists()) { + throw new IllegalStateException("Cannot find config/acron.json."); + } + final Config cfg = ConfigDeserializer.deserialize(config.toFile(), false); + Config.setGlobalConfig(cfg); + context.getSource().sendFeedback(new LiteralText("Rules reloaded."), true); + return 0; + } catch (IOException | ConfigDeserializationException e) { + LOGGER.error("Cannot reload config.", e); + context.getSource().sendError(new LiteralText("Cannot reload rules: " + + e.getMessage())); + return 1; + } + } +} diff --git a/src/main/java/moe/ymc/acron/config/json/Client.java b/src/main/java/moe/ymc/acron/config/json/Client.java new file mode 100644 index 0000000..4d31308 --- /dev/null +++ b/src/main/java/moe/ymc/acron/config/json/Client.java @@ -0,0 +1,45 @@ +package moe.ymc.acron.config.json; + +import com.google.gson.annotations.SerializedName; +import moe.ymc.acron.auth.Action; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +class Client implements ConfigJsonObject { + @SerializedName("id") + private final String id; + + @SerializedName("token") + private final String token; + + @SerializedName("policy_mode") + private final Action policyMode; + + @SerializedName("rules") + private final List rules; + + private Client(String id, + String token, + Action policyMode, + List rules) { + this.id = id; + this.token = token; + this.policyMode = policyMode; + this.rules = rules; + } + + @Override + public @NotNull moe.ymc.acron.auth.Client create(boolean startup) throws ConfigDeserializationException { + if (id == null || id.trim().equals("") || + token == null || token.trim().equals("")) { + throw new ConfigDeserializationException(".clients[].id or .clients[].token is not supplied."); + } + return new moe.ymc.acron.auth.Client(id, + token, + policyMode == null ? Action.DENY : policyMode, + rules == null ? new moe.ymc.acron.auth.Rule[]{} : + rules.stream().map(rule -> rule.create(startup)).toList() + .toArray(new moe.ymc.acron.auth.Rule[rules.size()])); + } +} diff --git a/src/main/java/moe/ymc/acron/config/json/Config.java b/src/main/java/moe/ymc/acron/config/json/Config.java new file mode 100644 index 0000000..e8c5a83 --- /dev/null +++ b/src/main/java/moe/ymc/acron/config/json/Config.java @@ -0,0 +1,103 @@ +package moe.ymc.acron.config.json; + +import com.google.gson.annotations.SerializedName; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +class Config implements ConfigJsonObject { + private static final Logger LOGGER = LogManager.getLogger(); + + @SerializedName("listen") + private final String listen; + + @SerializedName("port") + private final Integer port; + + @SerializedName("native_transport") + private final boolean nativeTransport; + + @SerializedName("clients") + private final List clients; + + private Config(String listen, + Integer port, + boolean nativeTransport, + List clients) { + this.listen = listen; + this.port = port; + this.nativeTransport = nativeTransport; + this.clients = clients; + } + + @Override + public @NotNull moe.ymc.acron.config.Config create(boolean startup) throws ConfigDeserializationException { + final InetAddress address; + final int p; + final boolean nt; + if (!startup) { + address = moe.ymc.acron.config.Config.getGlobalConfig().address(); + p = moe.ymc.acron.config.Config.getGlobalConfig().port(); + nt = moe.ymc.acron.config.Config.getGlobalConfig().useNativeTransport(); + } else { + if (listen == null || listen.trim().equals("")) { + address = InetAddress.getLoopbackAddress(); + } else { + try { + address = InetAddress.getByName(listen); + } catch (UnknownHostException e) { + throw new ConfigDeserializationException("Cannot parse address: " + e.getMessage(), + true); + } + } + if (port == null) { + p = 25575; + } else { + if (port < 0 || port > 65535) { + throw new ConfigDeserializationException("The port is out of range.", true); + } + p = port; + } + nt = nativeTransport; + } + + + Map map; + try { + if (clients != null) { + map = clients.stream() + .collect(Collectors. + toMap(client -> client.create(startup).id(), + client -> client.create(startup))); + } else { + map = new HashMap<>(0); + } + } catch (IllegalStateException e) { + // Collision. + LOGGER.error("Duplicate clients with the same ID in the Acron configuration. All clients are ignored. " + + "Fix the configuration and reload.", e); + if (!startup) { + throw new ConfigDeserializationException("Duplicate clients with the same ID: " + e.getMessage()); + } + map = new HashMap<>(0); + } catch (ConfigDeserializationException e) { + LOGGER.error("Cannot parse the Acron configuration. All clients are ignored. " + + "Fix the configuration and reload.", e); + if (!startup) { + throw e; + } + map = new HashMap<>(0); + } + return new moe.ymc.acron.config.Config(address, + p, + nt, + map); + } +} diff --git a/src/main/java/moe/ymc/acron/config/json/ConfigDeserializationException.java b/src/main/java/moe/ymc/acron/config/json/ConfigDeserializationException.java new file mode 100644 index 0000000..baf5b35 --- /dev/null +++ b/src/main/java/moe/ymc/acron/config/json/ConfigDeserializationException.java @@ -0,0 +1,18 @@ +package moe.ymc.acron.config.json; + +public class ConfigDeserializationException extends RuntimeException { + private final boolean fetal; + + public ConfigDeserializationException(String message) { + this(message, false); + } + + public ConfigDeserializationException(String message, boolean fetal) { + super(message); + this.fetal = fetal; + } + + public boolean isFetal() { + return fetal; + } +} diff --git a/src/main/java/moe/ymc/acron/config/json/ConfigDeserializer.java b/src/main/java/moe/ymc/acron/config/json/ConfigDeserializer.java new file mode 100644 index 0000000..e91b355 --- /dev/null +++ b/src/main/java/moe/ymc/acron/config/json/ConfigDeserializer.java @@ -0,0 +1,27 @@ +package moe.ymc.acron.config.json; + +import com.google.gson.Gson; +import moe.ymc.acron.config.Config; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.Reader; + +public class ConfigDeserializer { + public static @NotNull Config deserialize(@NotNull File file, boolean startup) + throws ConfigDeserializationException, IOException { + final Reader reader = new FileReader(file); + final moe.ymc.acron.config.json.Config config; + try { + config = new Gson() + .fromJson(reader, moe.ymc.acron.config.json.Config.class); + } catch (Throwable e) { + throw new ConfigDeserializationException("Cannot parse JSON: " + e.getMessage(), + true); + } + reader.close(); + return config.create(startup); + } +} diff --git a/src/main/java/moe/ymc/acron/config/json/ConfigJsonObject.java b/src/main/java/moe/ymc/acron/config/json/ConfigJsonObject.java new file mode 100644 index 0000000..0efd9a9 --- /dev/null +++ b/src/main/java/moe/ymc/acron/config/json/ConfigJsonObject.java @@ -0,0 +1,7 @@ +package moe.ymc.acron.config.json; + +import org.jetbrains.annotations.NotNull; + +public interface ConfigJsonObject { + @NotNull T create(boolean startup) throws ConfigDeserializationException; +} diff --git a/src/main/java/moe/ymc/acron/config/json/Rule.java b/src/main/java/moe/ymc/acron/config/json/Rule.java new file mode 100644 index 0000000..114e17d --- /dev/null +++ b/src/main/java/moe/ymc/acron/config/json/Rule.java @@ -0,0 +1,35 @@ +package moe.ymc.acron.config.json; + +import com.google.gson.annotations.SerializedName; +import moe.ymc.acron.auth.Action; +import org.jetbrains.annotations.NotNull; + +import java.util.regex.Pattern; + +class Rule implements ConfigJsonObject { + @SerializedName("regex") + private final String regex; + + @SerializedName("action") + private final Action action; + + @SerializedName("display") + private final boolean display; + + private Rule(String regex, + Action action, + boolean display) { + this.regex = regex; + this.action = action; + this.display = display; + } + + public @NotNull moe.ymc.acron.auth.Rule create(boolean startup) throws ConfigDeserializationException { + if (regex == null || regex.trim().equals("") || + action == null) throw new ConfigDeserializationException(".clients.[]rules.regex or .clients.[]rules.action is" + + "not specified."); + return new moe.ymc.acron.auth.Rule(Pattern.compile(regex), + action, + display); + } +} diff --git a/src/main/java/moe/ymc/acron/jvav/Pair.java b/src/main/java/moe/ymc/acron/jvav/Pair.java new file mode 100644 index 0000000..29b83dc --- /dev/null +++ b/src/main/java/moe/ymc/acron/jvav/Pair.java @@ -0,0 +1,4 @@ +package moe.ymc.acron.jvav; + +public record Pair(L l, R r) { +} \ No newline at end of file diff --git a/src/main/java/moe/ymc/acron/mixin/CommandManagerMixin.java b/src/main/java/moe/ymc/acron/mixin/CommandManagerMixin.java new file mode 100644 index 0000000..20de2e7 --- /dev/null +++ b/src/main/java/moe/ymc/acron/mixin/CommandManagerMixin.java @@ -0,0 +1,38 @@ +package moe.ymc.acron.mixin; + +import com.mojang.brigadier.CommandDispatcher; +import moe.ymc.acron.config.ConfigReloadCmd; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import static net.minecraft.server.command.CommandManager.literal; + + +@Mixin(CommandManager.class) +public abstract class CommandManagerMixin { + private static final Logger LOGGER = LogManager.getLogger(); + + @Shadow + @Final + private CommandDispatcher dispatcher; + + @Inject(method = "", at = @At("RETURN")) + private void onRegister(CommandManager.RegistrationEnvironment arg, CallbackInfo ci) { + LOGGER.debug("onRegister"); + dispatcher.register( + literal("acron").then( + literal("rule").then( + literal("update").requires(player -> player.hasPermissionLevel(4)) + .executes(new ConfigReloadCmd())) + ) + ); + } +} diff --git a/src/main/java/moe/ymc/acron/mixin/LivingEntityMixin.java b/src/main/java/moe/ymc/acron/mixin/LivingEntityMixin.java new file mode 100644 index 0000000..a4cb744 --- /dev/null +++ b/src/main/java/moe/ymc/acron/mixin/LivingEntityMixin.java @@ -0,0 +1,41 @@ +package moe.ymc.acron.mixin; + +import moe.ymc.acron.s2c.Entity; +import moe.ymc.acron.s2c.EventEntityDeath; +import moe.ymc.acron.s2c.EventQueue; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.entity.damage.DamageTracker; +import net.minecraft.world.World; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(LivingEntity.class) +public abstract class LivingEntityMixin extends net.minecraft.entity.Entity { + private static final Logger LOGGER = LogManager.getLogger(); + + @Shadow public abstract DamageTracker getDamageTracker(); + + public LivingEntityMixin(EntityType type, World world) { + super(type, world); + } + + // The original onDeath() will call getDamageTracker().update(), + // which clears all recent damages, making the getDeathMessage() + // output always generic. + // Thus, we need to use @At("HEAD") to get the injection called + // before it does anything else. + @Inject(at = @At("HEAD"), method = "onDeath") + public void onDeath(DamageSource source, CallbackInfo ci) { + LOGGER.debug("onDeath[{}]", + getUuid()); + EventQueue.enqueue(new EventEntityDeath(new Entity(this), + getDamageTracker().getDeathMessage().getString())); + } +} \ No newline at end of file diff --git a/src/main/java/moe/ymc/acron/mixin/MinecraftDedicatedServerMixin.java b/src/main/java/moe/ymc/acron/mixin/MinecraftDedicatedServerMixin.java new file mode 100644 index 0000000..50af0b4 --- /dev/null +++ b/src/main/java/moe/ymc/acron/mixin/MinecraftDedicatedServerMixin.java @@ -0,0 +1,21 @@ +package moe.ymc.acron.mixin; + +import moe.ymc.acron.MinecraftServerHolder; +import net.minecraft.server.dedicated.MinecraftDedicatedServer; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(MinecraftDedicatedServer.class) +public class MinecraftDedicatedServerMixin { + private static final Logger LOGGER = LogManager.getLogger(); + + @Inject(at = @At("RETURN"), method = "") + private void init(CallbackInfo info) { + LOGGER.debug("init"); + MinecraftServerHolder.setServer((MinecraftDedicatedServer) (Object) this); + } +} diff --git a/src/main/java/moe/ymc/acron/mixin/MinecraftServerMixin.java b/src/main/java/moe/ymc/acron/mixin/MinecraftServerMixin.java new file mode 100644 index 0000000..fc2a88c --- /dev/null +++ b/src/main/java/moe/ymc/acron/mixin/MinecraftServerMixin.java @@ -0,0 +1,32 @@ +package moe.ymc.acron.mixin; + +import moe.ymc.acron.s2c.EventLagging; +import moe.ymc.acron.s2c.EventQueue; +import net.minecraft.server.MinecraftServer; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(MinecraftServer.class) +public class MinecraftServerMixin { + private static final Logger LOGGER = LogManager.getLogger(); + + @Redirect(method = "runServer()V", + at = @At(value = "INVOKE", + target = "Lorg/apache/logging/log4j/Logger;warn(Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;)V")) + private void startServer(Logger instance, String s, Object o1, Object o2) { + instance.warn(s, o1, o2); + if (s.equals("Can't keep up! " + + "Is the server overloaded? " + + "Running {}ms or {} ticks behind") && + o1 instanceof Long && + o2 instanceof Long) { + LOGGER.debug("Lag: {}ms, {} ticks", + o1, + o2); + EventQueue.enqueue(new EventLagging((long) o1, (long) o2)); + } + } +} diff --git a/src/main/java/moe/ymc/acron/mixin/ServerLoginNetworkHandlerMixin.java b/src/main/java/moe/ymc/acron/mixin/ServerLoginNetworkHandlerMixin.java new file mode 100644 index 0000000..7e4115e --- /dev/null +++ b/src/main/java/moe/ymc/acron/mixin/ServerLoginNetworkHandlerMixin.java @@ -0,0 +1,47 @@ +package moe.ymc.acron.mixin; + +import com.mojang.authlib.GameProfile; +import moe.ymc.acron.s2c.Entity; +import moe.ymc.acron.s2c.EventDisconnected; +import moe.ymc.acron.s2c.EventPlayerJoined; +import moe.ymc.acron.s2c.EventQueue; +import net.minecraft.network.ClientConnection; +import net.minecraft.server.network.ServerLoginNetworkHandler; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ServerLoginNetworkHandler.class) +public class ServerLoginNetworkHandlerMixin { + private static final Logger LOGGER = LogManager.getLogger(); + + @Shadow + @Nullable + GameProfile profile; + + @Shadow + @Final + public ClientConnection connection; + + @Inject(at = @At("RETURN"), method = "onDisconnected") + private void onDisconnected(Text reason, CallbackInfo ci) { + EventQueue.enqueue(new EventDisconnected(profile == null ? null : + new Entity(profile), + reason.getString())); + } + + @Inject(at = @At("RETURN"), method = "addToServer") + private void addToServer(ServerPlayerEntity entity, CallbackInfo ci) { + LOGGER.debug("addToServer: {}", + entity.getUuid()); + EventQueue.enqueue(new EventPlayerJoined(new Entity(entity))); + } +} diff --git a/src/main/java/moe/ymc/acron/mixin/ServerNetworkIoMixin.java b/src/main/java/moe/ymc/acron/mixin/ServerNetworkIoMixin.java new file mode 100644 index 0000000..7c9b60b --- /dev/null +++ b/src/main/java/moe/ymc/acron/mixin/ServerNetworkIoMixin.java @@ -0,0 +1,65 @@ +package moe.ymc.acron.mixin; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.MultithreadEventLoopGroup; +import io.netty.channel.ServerChannel; +import io.netty.channel.epoll.Epoll; +import io.netty.channel.epoll.EpollServerSocketChannel; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import moe.ymc.acron.config.Config; +import moe.ymc.acron.net.AcronInitializer; +import net.minecraft.server.ServerNetworkIo; +import net.minecraft.util.Lazy; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.List; + +import static net.minecraft.server.ServerNetworkIo.EPOLL_CHANNEL; + +@Mixin(ServerNetworkIo.class) +public class ServerNetworkIoMixin { + private static final Logger LOGGER = LogManager.getLogger(); + + @Shadow + @Final + private List channels; + + @Shadow + @Final + public static Lazy DEFAULT_CHANNEL; + + @Inject(at = @At("RETURN"), method = "") + private void init(CallbackInfo info) { + LOGGER.debug("Adding Acron channel."); + Lazy group; + Class channel; + if (Epoll.isAvailable() && Config.getGlobalConfig().useNativeTransport()) { + channel = EpollServerSocketChannel.class; + group = EPOLL_CHANNEL; + LOGGER.info("Using native transport."); + } else { + channel = NioServerSocketChannel.class; + group = DEFAULT_CHANNEL; + LOGGER.info("Not using native transport due to " + + "it is either disabled in acron.json or not available."); + } + channels.add(new ServerBootstrap() + .channel(channel) + .childHandler(new AcronInitializer()) + .group(group.get()) + .localAddress(Config.getGlobalConfig().address(), + Config.getGlobalConfig().port()) + .bind() + .syncUninterruptibly()); + + } +} diff --git a/src/main/java/moe/ymc/acron/mixin/ServerPlayNetworkHandlerMixin.java b/src/main/java/moe/ymc/acron/mixin/ServerPlayNetworkHandlerMixin.java new file mode 100644 index 0000000..0bcfb0a --- /dev/null +++ b/src/main/java/moe/ymc/acron/mixin/ServerPlayNetworkHandlerMixin.java @@ -0,0 +1,43 @@ +package moe.ymc.acron.mixin; + +import moe.ymc.acron.s2c.Entity; +import moe.ymc.acron.s2c.EventDisconnected; +import moe.ymc.acron.s2c.EventPlayerMessage; +import moe.ymc.acron.s2c.EventQueue; +import net.minecraft.network.ClientConnection; +import net.minecraft.server.filter.TextStream; +import net.minecraft.server.network.ServerPlayNetworkHandler; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ServerPlayNetworkHandler.class) +public class ServerPlayNetworkHandlerMixin { + private static final Logger LOGGER = LogManager.getLogger(); + + @Shadow + public ServerPlayerEntity player; + + @Shadow + @Final + public ClientConnection connection; + + @Inject(at = @At("RETURN"), method = "handleMessage") + private void handleMessage(TextStream.Message message, CallbackInfo ci) { + EventQueue.enqueue(new EventPlayerMessage(new Entity(player), + message.getRaw())); + } + + @Inject(at = @At("RETURN"), method = "onDisconnected") + private void onDisconnected(Text reason, CallbackInfo ci) { + EventQueue.enqueue(new EventDisconnected(new Entity(player), + reason.getString())); + } +} diff --git a/src/main/java/moe/ymc/acron/mixin/ServerPlayerEntityMixin.java b/src/main/java/moe/ymc/acron/mixin/ServerPlayerEntityMixin.java new file mode 100644 index 0000000..17c8517 --- /dev/null +++ b/src/main/java/moe/ymc/acron/mixin/ServerPlayerEntityMixin.java @@ -0,0 +1,32 @@ +package moe.ymc.acron.mixin; + +import moe.ymc.acron.s2c.EventEntityDeath; +import moe.ymc.acron.s2c.EventQueue; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.world.World; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ServerPlayerEntity.class) +public abstract class ServerPlayerEntityMixin extends LivingEntity { + private static final Logger LOGGER = LogManager.getLogger(); + + public ServerPlayerEntityMixin(EntityType entityType, World world) { + super(entityType, world); + } + + @Inject(at = @At("HEAD"), method = "onDeath") + public void onDeath(DamageSource source, CallbackInfo ci) { + LOGGER.debug("onDeath: {}", + getUuid()); + EventQueue.enqueue(new EventEntityDeath(new moe.ymc.acron.s2c.Entity(this), + getDamageTracker().getDeathMessage().getString())); + } +} \ No newline at end of file diff --git a/src/main/java/moe/ymc/acron/net/AcronInitializer.java b/src/main/java/moe/ymc/acron/net/AcronInitializer.java new file mode 100644 index 0000000..c9953e3 --- /dev/null +++ b/src/main/java/moe/ymc/acron/net/AcronInitializer.java @@ -0,0 +1,25 @@ +package moe.ymc.acron.net; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpServerCodec; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * A channel initializer for all Acron handlers. + */ +public class AcronInitializer extends ChannelInitializer { + private static final Logger LOGGER = LogManager.getLogger(); + + @Override + protected void initChannel(SocketChannel ch) throws Exception { + LOGGER.debug("initChannel"); + ch.pipeline() + .addLast(new HttpServerCodec()) + .addLast(new HttpObjectAggregator(65536)) + .addLast(new AuthHandler()) + ; + } +} diff --git a/src/main/java/moe/ymc/acron/net/Attributes.java b/src/main/java/moe/ymc/acron/net/Attributes.java new file mode 100644 index 0000000..ddb0f5c --- /dev/null +++ b/src/main/java/moe/ymc/acron/net/Attributes.java @@ -0,0 +1,13 @@ +package moe.ymc.acron.net; + +import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker; +import io.netty.util.AttributeKey; + +final class Attributes { + public static final AttributeKey ID = + AttributeKey.newInstance("CLENT_ID"); + public static final AttributeKey CONFIGURATION = + AttributeKey.newInstance("CLIENT_CONFIG"); + public static final AttributeKey HANDSHAKER = + AttributeKey.newInstance("HANDSHAKER"); +} diff --git a/src/main/java/moe/ymc/acron/net/AuthHandler.java b/src/main/java/moe/ymc/acron/net/AuthHandler.java new file mode 100644 index 0000000..47abf9f --- /dev/null +++ b/src/main/java/moe/ymc/acron/net/AuthHandler.java @@ -0,0 +1,91 @@ +package moe.ymc.acron.net; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.*; +import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker; +import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory; +import moe.ymc.acron.auth.Client; +import moe.ymc.acron.config.Config; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Handle handshake request and authentication. + * We cannot use WebSocketServerProtocolHandler because it does not allow + * us doing anything before handshaking. + */ +public class AuthHandler extends SimpleChannelInboundHandler { + private static final Logger LOGGER = LogManager.getLogger(); + + @Override + protected void channelRead0(ChannelHandlerContext ctx, HttpRequest msg) throws Exception { + LOGGER.debug("channelRead0: {}", msg.uri()); + if (msg.method() != HttpMethod.GET) { + ctx.channel().writeAndFlush(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, + HttpResponseStatus.BAD_REQUEST)); + return; + } + HttpHeaders headers = msg.headers(); + + if (!"Upgrade".equalsIgnoreCase(headers.get(HttpHeaderNames.CONNECTION)) || + !"WebSocket".equalsIgnoreCase(headers.get(HttpHeaderNames.UPGRADE))) { + ctx.channel().writeAndFlush(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, + HttpResponseStatus.BAD_REQUEST)); + return; + } + + final QueryStringDecoder decoder = new QueryStringDecoder(msg.uri()); + if (!decoder.path().equals("/ws")) { + ctx.fireChannelRead(msg); + return; + } + if (decoder.parameters().isEmpty() || + decoder.parameters().get("id") == null || + decoder.parameters().get("id").size() != 1 || + decoder.parameters().get("token") == null || + decoder.parameters().get("token").size() != 1) { + ctx.writeAndFlush(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, + HttpResponseStatus.BAD_REQUEST)); + return; + } + + final String id = decoder.parameters().get("id").get(0); + final String token = decoder.parameters().get("token").get(0); + final String versionRaw = (decoder.parameters().get("version") == null || + decoder.parameters().get("version").isEmpty()) ? "0" : + decoder.parameters().get("version").get(0); + try { + if (Integer.parseInt(versionRaw) != 0) { + ctx.writeAndFlush(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, + HttpResponseStatus.BAD_REQUEST)); + return; + } + } catch (NumberFormatException ignored) { + ctx.writeAndFlush(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, + HttpResponseStatus.BAD_REQUEST)); + return; + } + + final Client client = Config.getGlobalConfig().clients().get(id); + if (client == null || + !client.token().equals(DigestUtils.sha256Hex(token))) { + ctx.writeAndFlush(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, + HttpResponseStatus.UNAUTHORIZED)); + return; + } + ctx.channel().attr(Attributes.ID).set(new ClientIdentification(0, client)); + WebSocketServerHandshakerFactory wsFactory = + new WebSocketServerHandshakerFactory("/ws", null, true); + final WebSocketServerHandshaker handshaker = wsFactory.newHandshaker(msg); + if (handshaker == null) { + WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel()); + return; + } + ctx.channel().attr(Attributes.HANDSHAKER).set(handshaker); + handshaker.handshake(ctx.channel(), msg); + ctx.pipeline().replace(this, "websocketHandler", new WSFrameHandler()); + ctx.fireUserEventTriggered(new HandshakeComplete()); + } +} diff --git a/src/main/java/moe/ymc/acron/net/ClientConfiguration.java b/src/main/java/moe/ymc/acron/net/ClientConfiguration.java new file mode 100644 index 0000000..450ccd4 --- /dev/null +++ b/src/main/java/moe/ymc/acron/net/ClientConfiguration.java @@ -0,0 +1,20 @@ +package moe.ymc.acron.net; + +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.math.Vec2f; +import net.minecraft.util.math.Vec3d; +import org.jetbrains.annotations.NotNull; + +public record ClientConfiguration(@NotNull Vec3d pos, + @NotNull Vec2f rot, + @NotNull ServerWorld world, + @NotNull String name) { + public ClientConfiguration(@NotNull ServerWorld world, + @NotNull String name) { + // Rcon defaults. @see RconCommandOutput + this(Vec3d.of(world.getSpawnPos()), + Vec2f.ZERO, + world, + name); + } +} diff --git a/src/main/java/moe/ymc/acron/net/ClientIdentification.java b/src/main/java/moe/ymc/acron/net/ClientIdentification.java new file mode 100644 index 0000000..1cb4375 --- /dev/null +++ b/src/main/java/moe/ymc/acron/net/ClientIdentification.java @@ -0,0 +1,11 @@ +package moe.ymc.acron.net; + +import moe.ymc.acron.auth.Client; +import org.jetbrains.annotations.NotNull; + +/** + * Read only client configurations. + */ +public record ClientIdentification(int version, + @NotNull Client client) { +} diff --git a/src/main/java/moe/ymc/acron/net/HandshakeComplete.java b/src/main/java/moe/ymc/acron/net/HandshakeComplete.java new file mode 100644 index 0000000..348b5e2 --- /dev/null +++ b/src/main/java/moe/ymc/acron/net/HandshakeComplete.java @@ -0,0 +1,7 @@ +package moe.ymc.acron.net; + +/** + * User event used to tell WSFrameHandler that the handshake is complete. + */ +public class HandshakeComplete { +} diff --git a/src/main/java/moe/ymc/acron/net/WSFrameHandler.java b/src/main/java/moe/ymc/acron/net/WSFrameHandler.java new file mode 100644 index 0000000..8eba1f8 --- /dev/null +++ b/src/main/java/moe/ymc/acron/net/WSFrameHandler.java @@ -0,0 +1,149 @@ +package moe.ymc.acron.net; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.websocketx.*; +import moe.ymc.acron.MinecraftServerHolder; +import moe.ymc.acron.auth.Action; +import moe.ymc.acron.auth.PolicyChecker; +import moe.ymc.acron.c2s.ReqCmd; +import moe.ymc.acron.c2s.ReqSetConfig; +import moe.ymc.acron.c2s.Request; +import moe.ymc.acron.cmd.CmdQueue; +import moe.ymc.acron.jvav.Pair; +import moe.ymc.acron.s2c.EventCmdDenied; +import moe.ymc.acron.s2c.EventError; +import moe.ymc.acron.s2c.EventOk; +import moe.ymc.acron.s2c.EventQueue; +import moe.ymc.acron.serialization.Serializer; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.math.Vec2f; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; + +/** + * The handler for WebSocket requests. + */ +public class WSFrameHandler extends SimpleChannelInboundHandler { + private static final Logger LOGGER = LogManager.getLogger(); + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + super.handlerAdded(ctx); + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame msg) throws Exception { + LOGGER.debug("channelRead0: {} {}", + this, + ctx.channel()); + final WebSocketServerHandshaker handshaker = + ctx.channel().attr(Attributes.HANDSHAKER).get(); + if (msg instanceof CloseWebSocketFrame) { + handshaker.close(ctx.channel(), (CloseWebSocketFrame) msg.retain()); + return; + } + if (msg instanceof PingWebSocketFrame) { + ctx.write(new PongWebSocketFrame(msg.content().retain())); + return; + } + if (msg instanceof BinaryWebSocketFrame) { + throw new UnsupportedOperationException("Only text frames are accepted."); + } + final TextWebSocketFrame frame = (TextWebSocketFrame) msg; + + final ClientIdentification identification = ctx.channel().attr(Attributes.ID).get(); + final ClientConfiguration configuration = ctx.channel().attr(Attributes.CONFIGURATION).get(); + int id = -2; + try { + final Request request = Serializer.deserialize(frame); + id = request.getId(); + if (request instanceof final ReqCmd reqCmd) { + LOGGER.info("Client {} executed a command: `{}`.", + identification.client().id(), + reqCmd.cmd()); + final Pair res = PolicyChecker.check(identification.client(), + reqCmd.cmd()); + if (res.l() == Action.DENY) { + ctx.channel().writeAndFlush(Serializer.serialize(new EventCmdDenied(reqCmd.id()))); + return; + } + // Write it before enqueueing to prevent potential + // thread safety issues. + ctx.channel().writeAndFlush(Serializer.serialize(new EventOk(id))); + CmdQueue.enqueue(id, + res.r(), + ctx.channel(), + reqCmd.config() == null ? + configuration : + convertConfiguration(reqCmd.config()), + reqCmd.cmd()); + } else if (request instanceof final ReqSetConfig reqSetConfig) { + ctx.channel().attr(Attributes.CONFIGURATION).set(convertConfiguration(reqSetConfig)); + ctx.channel().writeAndFlush(Serializer.serialize(new EventOk(id))); + } + } catch (Throwable e) { + LOGGER.info("An error occurred while processing the request. " + + "This may just be a malformed request. " + + "It is reported to the client.", + e); + ctx.channel().writeAndFlush(Serializer.serialize(new EventError(id, e.getMessage()))); + } + } + + private ClientConfiguration convertConfiguration(@NotNull ReqSetConfig request) { + final ServerWorld world; + if (request.world() != null) { + switch (request.world()) { + case OVERWORLD -> world = MinecraftServerHolder.getServer().getWorld(World.OVERWORLD); + case NETHER -> world = MinecraftServerHolder.getServer().getWorld(World.NETHER); + case END -> world = MinecraftServerHolder.getServer().getWorld(World.END); + default -> throw new IllegalArgumentException(); + } + } else { + world = MinecraftServerHolder.getServer().getOverworld(); + } + if (world == null) { + throw new IllegalStateException(String.format("The requested world %s is not available at this time.", + request.world())); + } + return new ClientConfiguration( + request.pos() == null ? + Vec3d.of(world.getSpawnPos()) : + new Vec3d(request.pos().x(), request.pos().y(), request.pos().z()), + request.rot() == null ? + Vec2f.ZERO : + new Vec2f(request.rot().x(), request.rot().y()), + world, + request.name() == null ? this.toString() : request.name() + ); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + LOGGER.debug("handshakeComplete: {} {}", + this, + ctx.channel()); + if (evt instanceof HandshakeComplete) { + final ClientIdentification identification = ctx.channel().attr(Attributes.ID).get(); + LOGGER.info("Client {} connected. It has {} rules with {} policy mode.", + identification.client().id(), + identification.client().rules().length, + identification.client().policyMode()); + final ServerWorld defaultWorld = MinecraftServerHolder.getServer().getOverworld(); + if (defaultWorld == null) { + throw new IllegalStateException("The default world is not available at this time."); + } + final ClientConfiguration configuration = + new ClientConfiguration(defaultWorld, + identification.client().id()); + ctx.channel().attr(Attributes.CONFIGURATION).set(configuration); + EventQueue.registerMessageRecipient(ctx.channel()); + } else { + ctx.fireUserEventTriggered(evt); + } + } +} diff --git a/src/main/java/moe/ymc/acron/s2c/Entity.java b/src/main/java/moe/ymc/acron/s2c/Entity.java new file mode 100644 index 0000000..97bd567 --- /dev/null +++ b/src/main/java/moe/ymc/acron/s2c/Entity.java @@ -0,0 +1,19 @@ +package moe.ymc.acron.s2c; + +import com.google.gson.annotations.SerializedName; +import com.mojang.authlib.GameProfile; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +public record Entity(@SerializedName("name") @NotNull String name, + @SerializedName("uuid") @NotNull UUID uuid) { + public Entity(@NotNull net.minecraft.entity.Entity entity) { + this(entity.getName().getString(), + entity.getUuid()); + } + + public Entity(@NotNull GameProfile profile) { + this(profile.getName(), profile.getId()); + } +} diff --git a/src/main/java/moe/ymc/acron/s2c/Event.java b/src/main/java/moe/ymc/acron/s2c/Event.java new file mode 100644 index 0000000..1abc35c --- /dev/null +++ b/src/main/java/moe/ymc/acron/s2c/Event.java @@ -0,0 +1,4 @@ +package moe.ymc.acron.s2c; + +public interface Event { +} diff --git a/src/main/java/moe/ymc/acron/s2c/EventCmdDenied.java b/src/main/java/moe/ymc/acron/s2c/EventCmdDenied.java new file mode 100644 index 0000000..912b430 --- /dev/null +++ b/src/main/java/moe/ymc/acron/s2c/EventCmdDenied.java @@ -0,0 +1,7 @@ +package moe.ymc.acron.s2c; + +import com.google.gson.annotations.SerializedName; + +public record EventCmdDenied(@SerializedName("id") int id) + implements Event { +} diff --git a/src/main/java/moe/ymc/acron/s2c/EventCmdOut.java b/src/main/java/moe/ymc/acron/s2c/EventCmdOut.java new file mode 100644 index 0000000..3b8d06e --- /dev/null +++ b/src/main/java/moe/ymc/acron/s2c/EventCmdOut.java @@ -0,0 +1,12 @@ +package moe.ymc.acron.s2c; + +import com.google.gson.annotations.SerializedName; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +public record EventCmdOut(@SerializedName("id") int id, + @SerializedName("sender") @NotNull UUID sender, + @SerializedName("text") @NotNull String text) + implements Event { +} diff --git a/src/main/java/moe/ymc/acron/s2c/EventCmdRes.java b/src/main/java/moe/ymc/acron/s2c/EventCmdRes.java new file mode 100644 index 0000000..dae858a --- /dev/null +++ b/src/main/java/moe/ymc/acron/s2c/EventCmdRes.java @@ -0,0 +1,9 @@ +package moe.ymc.acron.s2c; + +import com.google.gson.annotations.SerializedName; + +public record EventCmdRes(@SerializedName("id") int id, + @SerializedName("success") boolean success, + @SerializedName("result") int result) + implements Event { +} diff --git a/src/main/java/moe/ymc/acron/s2c/EventDisconnected.java b/src/main/java/moe/ymc/acron/s2c/EventDisconnected.java new file mode 100644 index 0000000..bf0e279 --- /dev/null +++ b/src/main/java/moe/ymc/acron/s2c/EventDisconnected.java @@ -0,0 +1,10 @@ +package moe.ymc.acron.s2c; + +import com.google.gson.annotations.SerializedName; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public record EventDisconnected(@SerializedName("player") @Nullable Entity player, + @SerializedName("reason") @NotNull String reason) + implements Event { +} diff --git a/src/main/java/moe/ymc/acron/s2c/EventEntityDeath.java b/src/main/java/moe/ymc/acron/s2c/EventEntityDeath.java new file mode 100644 index 0000000..201cdf3 --- /dev/null +++ b/src/main/java/moe/ymc/acron/s2c/EventEntityDeath.java @@ -0,0 +1,10 @@ +package moe.ymc.acron.s2c; + +import com.google.gson.annotations.SerializedName; +import org.jetbrains.annotations.NotNull; + +// TODO: More detailed death report. +public record EventEntityDeath(@SerializedName("entity") @NotNull Entity entity, + @SerializedName("message") @NotNull String message) + implements Event { +} diff --git a/src/main/java/moe/ymc/acron/s2c/EventError.java b/src/main/java/moe/ymc/acron/s2c/EventError.java new file mode 100644 index 0000000..7f48fa8 --- /dev/null +++ b/src/main/java/moe/ymc/acron/s2c/EventError.java @@ -0,0 +1,9 @@ +package moe.ymc.acron.s2c; + +import com.google.gson.annotations.SerializedName; +import org.jetbrains.annotations.Nullable; + +public record EventError(@SerializedName("id") int id, + @SerializedName("message") @Nullable String message) + implements Event { +} diff --git a/src/main/java/moe/ymc/acron/s2c/EventLagging.java b/src/main/java/moe/ymc/acron/s2c/EventLagging.java new file mode 100644 index 0000000..d0a3f61 --- /dev/null +++ b/src/main/java/moe/ymc/acron/s2c/EventLagging.java @@ -0,0 +1,8 @@ +package moe.ymc.acron.s2c; + +import com.google.gson.annotations.SerializedName; + +public record EventLagging(@SerializedName("ms") long ms, + @SerializedName("ticks") long ticks) + implements Event { +} diff --git a/src/main/java/moe/ymc/acron/s2c/EventOk.java b/src/main/java/moe/ymc/acron/s2c/EventOk.java new file mode 100644 index 0000000..5be5c65 --- /dev/null +++ b/src/main/java/moe/ymc/acron/s2c/EventOk.java @@ -0,0 +1,7 @@ +package moe.ymc.acron.s2c; + +import com.google.gson.annotations.SerializedName; + +public record EventOk(@SerializedName("id") int id) + implements Event { +} diff --git a/src/main/java/moe/ymc/acron/s2c/EventPlayerJoined.java b/src/main/java/moe/ymc/acron/s2c/EventPlayerJoined.java new file mode 100644 index 0000000..eaadee1 --- /dev/null +++ b/src/main/java/moe/ymc/acron/s2c/EventPlayerJoined.java @@ -0,0 +1,8 @@ +package moe.ymc.acron.s2c; + +import com.google.gson.annotations.SerializedName; +import org.jetbrains.annotations.NotNull; + +public record EventPlayerJoined(@SerializedName("player") @NotNull Entity player) + implements Event { +} diff --git a/src/main/java/moe/ymc/acron/s2c/EventPlayerMessage.java b/src/main/java/moe/ymc/acron/s2c/EventPlayerMessage.java new file mode 100644 index 0000000..f5273ac --- /dev/null +++ b/src/main/java/moe/ymc/acron/s2c/EventPlayerMessage.java @@ -0,0 +1,10 @@ +package moe.ymc.acron.s2c; + +import com.google.gson.annotations.SerializedName; +import org.jetbrains.annotations.NotNull; + +public record EventPlayerMessage(@SerializedName("player") @NotNull Entity player, + @SerializedName("text") @NotNull String text) + implements Event { + +} diff --git a/src/main/java/moe/ymc/acron/s2c/EventQueue.java b/src/main/java/moe/ymc/acron/s2c/EventQueue.java new file mode 100644 index 0000000..8c470a1 --- /dev/null +++ b/src/main/java/moe/ymc/acron/s2c/EventQueue.java @@ -0,0 +1,28 @@ +package moe.ymc.acron.s2c; + +import io.netty.channel.Channel; +import io.netty.channel.group.ChannelGroup; +import io.netty.channel.group.DefaultChannelGroup; +import io.netty.util.concurrent.GlobalEventExecutor; +import moe.ymc.acron.serialization.Serializer; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; + +public class EventQueue { + private static final Logger LOGGER = LogManager.getLogger(); + + private static final ChannelGroup sMessageRecipients = + new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); + + public static void registerMessageRecipient(@NotNull Channel channel) { + sMessageRecipients.add(channel); + } + + public static void enqueue(@NotNull Event message) { + LOGGER.debug("Enqueue: {} ({} channels)", + message, + sMessageRecipients.size()); + sMessageRecipients.writeAndFlush(Serializer.serialize(message)); + } +} diff --git a/src/main/java/moe/ymc/acron/serialization/Serializer.java b/src/main/java/moe/ymc/acron/serialization/Serializer.java new file mode 100644 index 0000000..28c7e18 --- /dev/null +++ b/src/main/java/moe/ymc/acron/serialization/Serializer.java @@ -0,0 +1,54 @@ +package moe.ymc.acron.serialization; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.typeadapters.RuntimeTypeAdapterFactory; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import moe.ymc.acron.c2s.ReqCmd; +import moe.ymc.acron.c2s.ReqSetConfig; +import moe.ymc.acron.c2s.Request; +import moe.ymc.acron.s2c.*; +import org.jetbrains.annotations.NotNull; + +public class Serializer { + @NotNull + public static Request deserialize(@NotNull TextWebSocketFrame frame) { + final String text = frame.text(); + final RuntimeTypeAdapterFactory adapter = + RuntimeTypeAdapterFactory.of(Request.class, "type") + .registerSubtype(ReqSetConfig.class, "set_config") + .registerSubtype(ReqCmd.class, "cmd") + ; + final Gson gson = new GsonBuilder() + .registerTypeAdapter(ReqSetConfig.class, new ReqSetConfig.ReqSetConfigDeserializer()) + .registerTypeAdapter(ReqSetConfig.Vec3d.class, new ReqSetConfig.Vec3d.Vec3dDeserializer()) + .registerTypeAdapter(ReqSetConfig.Vec2f.class, new ReqSetConfig.Vec2f.Vec2fDeserializer()) + .registerTypeAdapter(ReqCmd.class, new ReqCmd.ReqCmdDeserializer()) + .registerTypeAdapterFactory(adapter) + .create(); + final Request request = gson.fromJson(text, Request.class); + request.validate(); + return request; + } + + @NotNull + public static TextWebSocketFrame serialize(@NotNull Event message) { + final RuntimeTypeAdapterFactory adapter = + RuntimeTypeAdapterFactory.of(Event.class, "type") + .registerSubtype(EventDisconnected.class, "disconnect") + .registerSubtype(EventPlayerMessage.class, "message") + .registerSubtype(EventPlayerJoined.class, "join") + .registerSubtype(EventEntityDeath.class, "death") + .registerSubtype(EventCmdOut.class, "cmd_output") + .registerSubtype(EventCmdRes.class, "cmd_result") + .registerSubtype(EventLagging.class, "lagging") + .registerSubtype(EventCmdDenied.class, "cmd_denied") + .registerSubtype(EventError.class, "error") + .registerSubtype(EventOk.class, "ok") + ; + final Gson gson = new GsonBuilder() + .registerTypeAdapterFactory(adapter) + .create(); + return new TextWebSocketFrame(gson.toJson(message, message.getClass())); + } +} -- cgit v1.2.3