aboutsummaryrefslogtreecommitdiff
path: root/mod/src/main/java/moe/ymc/acron/net/WSFrameHandler.java
blob: 912e73a6367c1c72eb9e175f1c255ba3e0829117 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
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<WebSocketFrame> {
    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<Action, Boolean> 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);
        }
    }
}