package moe.ymc.acron.net; import com.google.gson.JsonParseException; import io.netty.channel.Channel; 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.Event; import moe.ymc.acron.s2c.EventQueue; import moe.ymc.acron.s2c.response.EventError; import moe.ymc.acron.s2c.response.EventOk; 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; final Request request; try { request = Serializer.deserialize(frame); id = request.getId(); } catch (JsonParseException | IllegalArgumentException | IllegalStateException e) { ctx.channel().writeAndFlush( Serializer.serialize(new EventError(-2, EventError.Code.BAD_REQUEST.value, e.getMessage())) ); return; } try { ctx.channel().writeAndFlush(Serializer.serialize(handle(request, identification, configuration, ctx.channel()))); } 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, EventError.Code.SERVER_ERROR.value, e.getMessage())) ); } } @NotNull private Event handle(@NotNull Request request, @NotNull ClientIdentification identification, @NotNull ClientConfiguration configuration, @NotNull Channel channel) throws Throwable { 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) { return new EventError(reqCmd.id(), EventError.Code.FORBIDDEN.value, "This client is not allowed to " + "execute this command."); } // TODO: Ok event may be sent after executing the command. CmdQueue.enqueue(reqCmd.id(), res.r(), channel, reqCmd.config() == null ? configuration : convertConfiguration(reqCmd.config()), reqCmd.cmd()); return new EventOk(request.getId()); } else if (request instanceof final ReqSetConfig reqSetConfig) { channel.attr(Attributes.CONFIGURATION).set(convertConfiguration(reqSetConfig)); return new EventOk(request.getId()); } // This should not occur. throw new IllegalStateException("This should not occur."); } 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); } } }