From d8fe269327a1a51f2588a3573a4764613da16388 Mon Sep 17 00:00:00 2001 From: Trumeet Date: Tue, 26 Jul 2022 19:06:11 -0700 Subject: Move the mod to mod/ --- README.md | 404 +-------------------- acron.json | 19 - build.gradle | 78 ---- gradle.properties | 11 - mod/README.md | 403 ++++++++++++++++++++ mod/acron.json | 19 + mod/build.gradle | 78 ++++ mod/gradle.properties | 11 + mod/settings.gradle | 9 + .../typeadapters/RuntimeTypeAdapterFactory.java | 272 ++++++++++++++ mod/src/main/java/moe/ymc/acron/Acron.java | 32 ++ .../java/moe/ymc/acron/MinecraftServerHolder.java | 28 ++ mod/src/main/java/moe/ymc/acron/auth/Action.java | 10 + mod/src/main/java/moe/ymc/acron/auth/Client.java | 9 + .../java/moe/ymc/acron/auth/PolicyChecker.java | 42 +++ mod/src/main/java/moe/ymc/acron/auth/Rule.java | 10 + mod/src/main/java/moe/ymc/acron/c2s/ReqCmd.java | 51 +++ .../main/java/moe/ymc/acron/c2s/ReqSetConfig.java | 63 ++++ mod/src/main/java/moe/ymc/acron/c2s/Request.java | 6 + mod/src/main/java/moe/ymc/acron/cmd/CmdOut.java | 53 +++ mod/src/main/java/moe/ymc/acron/cmd/CmdQueue.java | 17 + .../java/moe/ymc/acron/cmd/CmdResConsumer.java | 33 ++ mod/src/main/java/moe/ymc/acron/cmd/CmdSrc.java | 34 ++ mod/src/main/java/moe/ymc/acron/common/Vec2f.java | 25 ++ mod/src/main/java/moe/ymc/acron/common/Vec3d.java | 34 ++ .../main/java/moe/ymc/acron/common/WorldKey.java | 35 ++ mod/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 + .../main/java/moe/ymc/acron/config/json/Rule.java | 35 ++ mod/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 | 33 ++ .../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 ++ .../main/java/moe/ymc/acron/net/Attributes.java | 13 + .../main/java/moe/ymc/acron/net/AuthHandler.java | 98 +++++ .../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 | 174 +++++++++ mod/src/main/java/moe/ymc/acron/s2c/Entity.java | 26 ++ mod/src/main/java/moe/ymc/acron/s2c/Event.java | 4 + .../main/java/moe/ymc/acron/s2c/EventQueue.java | 28 ++ .../moe/ymc/acron/s2c/event/EventDisconnected.java | 12 + .../moe/ymc/acron/s2c/event/EventEntityDeath.java | 12 + .../java/moe/ymc/acron/s2c/event/EventLagging.java | 9 + .../moe/ymc/acron/s2c/event/EventPlayerJoined.java | 10 + .../ymc/acron/s2c/event/EventPlayerMessage.java | 12 + .../moe/ymc/acron/s2c/response/EventCmdOut.java | 13 + .../moe/ymc/acron/s2c/response/EventCmdRes.java | 10 + .../moe/ymc/acron/s2c/response/EventError.java | 22 ++ .../java/moe/ymc/acron/s2c/response/EventOk.java | 8 + .../moe/ymc/acron/serialization/Serializer.java | 60 +++ mod/src/main/resources/acron.mixins.json | 21 ++ mod/src/main/resources/fabric.mod.json | 24 ++ settings.gradle | 9 - .../typeadapters/RuntimeTypeAdapterFactory.java | 272 -------------- 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 | 42 --- 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 | 63 ---- 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/common/Vec2f.java | 25 -- src/main/java/moe/ymc/acron/common/Vec3d.java | 34 -- src/main/java/moe/ymc/acron/common/WorldKey.java | 35 -- 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 | 33 -- .../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 | 98 ----- .../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 | 174 --------- src/main/java/moe/ymc/acron/s2c/Entity.java | 26 -- src/main/java/moe/ymc/acron/s2c/Event.java | 4 - src/main/java/moe/ymc/acron/s2c/EventQueue.java | 28 -- .../moe/ymc/acron/s2c/event/EventDisconnected.java | 12 - .../moe/ymc/acron/s2c/event/EventEntityDeath.java | 12 - .../java/moe/ymc/acron/s2c/event/EventLagging.java | 9 - .../moe/ymc/acron/s2c/event/EventPlayerJoined.java | 10 - .../ymc/acron/s2c/event/EventPlayerMessage.java | 12 - .../moe/ymc/acron/s2c/response/EventCmdOut.java | 13 - .../moe/ymc/acron/s2c/response/EventCmdRes.java | 10 - .../moe/ymc/acron/s2c/response/EventError.java | 22 -- .../java/moe/ymc/acron/s2c/response/EventOk.java | 8 - .../moe/ymc/acron/serialization/Serializer.java | 60 --- src/main/resources/acron.mixins.json | 21 -- src/main/resources/fabric.mod.json | 24 -- 122 files changed, 2530 insertions(+), 2517 deletions(-) delete mode 100644 acron.json delete mode 100644 build.gradle delete mode 100644 gradle.properties create mode 100644 mod/README.md create mode 100644 mod/acron.json create mode 100644 mod/build.gradle create mode 100644 mod/gradle.properties create mode 100644 mod/settings.gradle create mode 100644 mod/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java create mode 100644 mod/src/main/java/moe/ymc/acron/Acron.java create mode 100644 mod/src/main/java/moe/ymc/acron/MinecraftServerHolder.java create mode 100644 mod/src/main/java/moe/ymc/acron/auth/Action.java create mode 100644 mod/src/main/java/moe/ymc/acron/auth/Client.java create mode 100644 mod/src/main/java/moe/ymc/acron/auth/PolicyChecker.java create mode 100644 mod/src/main/java/moe/ymc/acron/auth/Rule.java create mode 100644 mod/src/main/java/moe/ymc/acron/c2s/ReqCmd.java create mode 100644 mod/src/main/java/moe/ymc/acron/c2s/ReqSetConfig.java create mode 100644 mod/src/main/java/moe/ymc/acron/c2s/Request.java create mode 100644 mod/src/main/java/moe/ymc/acron/cmd/CmdOut.java create mode 100644 mod/src/main/java/moe/ymc/acron/cmd/CmdQueue.java create mode 100644 mod/src/main/java/moe/ymc/acron/cmd/CmdResConsumer.java create mode 100644 mod/src/main/java/moe/ymc/acron/cmd/CmdSrc.java create mode 100644 mod/src/main/java/moe/ymc/acron/common/Vec2f.java create mode 100644 mod/src/main/java/moe/ymc/acron/common/Vec3d.java create mode 100644 mod/src/main/java/moe/ymc/acron/common/WorldKey.java create mode 100644 mod/src/main/java/moe/ymc/acron/config/Config.java create mode 100644 mod/src/main/java/moe/ymc/acron/config/ConfigReloadCmd.java create mode 100644 mod/src/main/java/moe/ymc/acron/config/json/Client.java create mode 100644 mod/src/main/java/moe/ymc/acron/config/json/Config.java create mode 100644 mod/src/main/java/moe/ymc/acron/config/json/ConfigDeserializationException.java create mode 100644 mod/src/main/java/moe/ymc/acron/config/json/ConfigDeserializer.java create mode 100644 mod/src/main/java/moe/ymc/acron/config/json/ConfigJsonObject.java create mode 100644 mod/src/main/java/moe/ymc/acron/config/json/Rule.java create mode 100644 mod/src/main/java/moe/ymc/acron/jvav/Pair.java create mode 100644 mod/src/main/java/moe/ymc/acron/mixin/CommandManagerMixin.java create mode 100644 mod/src/main/java/moe/ymc/acron/mixin/LivingEntityMixin.java create mode 100644 mod/src/main/java/moe/ymc/acron/mixin/MinecraftDedicatedServerMixin.java create mode 100644 mod/src/main/java/moe/ymc/acron/mixin/MinecraftServerMixin.java create mode 100644 mod/src/main/java/moe/ymc/acron/mixin/ServerLoginNetworkHandlerMixin.java create mode 100644 mod/src/main/java/moe/ymc/acron/mixin/ServerNetworkIoMixin.java create mode 100644 mod/src/main/java/moe/ymc/acron/mixin/ServerPlayNetworkHandlerMixin.java create mode 100644 mod/src/main/java/moe/ymc/acron/mixin/ServerPlayerEntityMixin.java create mode 100644 mod/src/main/java/moe/ymc/acron/net/AcronInitializer.java create mode 100644 mod/src/main/java/moe/ymc/acron/net/Attributes.java create mode 100644 mod/src/main/java/moe/ymc/acron/net/AuthHandler.java create mode 100644 mod/src/main/java/moe/ymc/acron/net/ClientConfiguration.java create mode 100644 mod/src/main/java/moe/ymc/acron/net/ClientIdentification.java create mode 100644 mod/src/main/java/moe/ymc/acron/net/HandshakeComplete.java create mode 100644 mod/src/main/java/moe/ymc/acron/net/WSFrameHandler.java create mode 100644 mod/src/main/java/moe/ymc/acron/s2c/Entity.java create mode 100644 mod/src/main/java/moe/ymc/acron/s2c/Event.java create mode 100644 mod/src/main/java/moe/ymc/acron/s2c/EventQueue.java create mode 100644 mod/src/main/java/moe/ymc/acron/s2c/event/EventDisconnected.java create mode 100644 mod/src/main/java/moe/ymc/acron/s2c/event/EventEntityDeath.java create mode 100644 mod/src/main/java/moe/ymc/acron/s2c/event/EventLagging.java create mode 100644 mod/src/main/java/moe/ymc/acron/s2c/event/EventPlayerJoined.java create mode 100644 mod/src/main/java/moe/ymc/acron/s2c/event/EventPlayerMessage.java create mode 100644 mod/src/main/java/moe/ymc/acron/s2c/response/EventCmdOut.java create mode 100644 mod/src/main/java/moe/ymc/acron/s2c/response/EventCmdRes.java create mode 100644 mod/src/main/java/moe/ymc/acron/s2c/response/EventError.java create mode 100644 mod/src/main/java/moe/ymc/acron/s2c/response/EventOk.java create mode 100644 mod/src/main/java/moe/ymc/acron/serialization/Serializer.java create mode 100644 mod/src/main/resources/acron.mixins.json create mode 100644 mod/src/main/resources/fabric.mod.json delete mode 100644 settings.gradle delete mode 100644 src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java delete mode 100644 src/main/java/moe/ymc/acron/Acron.java delete mode 100644 src/main/java/moe/ymc/acron/MinecraftServerHolder.java delete mode 100644 src/main/java/moe/ymc/acron/auth/Action.java delete mode 100644 src/main/java/moe/ymc/acron/auth/Client.java delete mode 100644 src/main/java/moe/ymc/acron/auth/PolicyChecker.java delete mode 100644 src/main/java/moe/ymc/acron/auth/Rule.java delete mode 100644 src/main/java/moe/ymc/acron/c2s/ReqCmd.java delete mode 100644 src/main/java/moe/ymc/acron/c2s/ReqSetConfig.java delete mode 100644 src/main/java/moe/ymc/acron/c2s/Request.java delete mode 100644 src/main/java/moe/ymc/acron/cmd/CmdOut.java delete mode 100644 src/main/java/moe/ymc/acron/cmd/CmdQueue.java delete mode 100644 src/main/java/moe/ymc/acron/cmd/CmdResConsumer.java delete mode 100644 src/main/java/moe/ymc/acron/cmd/CmdSrc.java delete mode 100644 src/main/java/moe/ymc/acron/common/Vec2f.java delete mode 100644 src/main/java/moe/ymc/acron/common/Vec3d.java delete mode 100644 src/main/java/moe/ymc/acron/common/WorldKey.java delete mode 100644 src/main/java/moe/ymc/acron/config/Config.java delete mode 100644 src/main/java/moe/ymc/acron/config/ConfigReloadCmd.java delete mode 100644 src/main/java/moe/ymc/acron/config/json/Client.java delete mode 100644 src/main/java/moe/ymc/acron/config/json/Config.java delete mode 100644 src/main/java/moe/ymc/acron/config/json/ConfigDeserializationException.java delete mode 100644 src/main/java/moe/ymc/acron/config/json/ConfigDeserializer.java delete mode 100644 src/main/java/moe/ymc/acron/config/json/ConfigJsonObject.java delete mode 100644 src/main/java/moe/ymc/acron/config/json/Rule.java delete mode 100644 src/main/java/moe/ymc/acron/jvav/Pair.java delete mode 100644 src/main/java/moe/ymc/acron/mixin/CommandManagerMixin.java delete mode 100644 src/main/java/moe/ymc/acron/mixin/LivingEntityMixin.java delete mode 100644 src/main/java/moe/ymc/acron/mixin/MinecraftDedicatedServerMixin.java delete mode 100644 src/main/java/moe/ymc/acron/mixin/MinecraftServerMixin.java delete mode 100644 src/main/java/moe/ymc/acron/mixin/ServerLoginNetworkHandlerMixin.java delete mode 100644 src/main/java/moe/ymc/acron/mixin/ServerNetworkIoMixin.java delete mode 100644 src/main/java/moe/ymc/acron/mixin/ServerPlayNetworkHandlerMixin.java delete mode 100644 src/main/java/moe/ymc/acron/mixin/ServerPlayerEntityMixin.java delete mode 100644 src/main/java/moe/ymc/acron/net/AcronInitializer.java delete mode 100644 src/main/java/moe/ymc/acron/net/Attributes.java delete mode 100644 src/main/java/moe/ymc/acron/net/AuthHandler.java delete mode 100644 src/main/java/moe/ymc/acron/net/ClientConfiguration.java delete mode 100644 src/main/java/moe/ymc/acron/net/ClientIdentification.java delete mode 100644 src/main/java/moe/ymc/acron/net/HandshakeComplete.java delete mode 100644 src/main/java/moe/ymc/acron/net/WSFrameHandler.java delete mode 100644 src/main/java/moe/ymc/acron/s2c/Entity.java delete mode 100644 src/main/java/moe/ymc/acron/s2c/Event.java delete mode 100644 src/main/java/moe/ymc/acron/s2c/EventQueue.java delete mode 100644 src/main/java/moe/ymc/acron/s2c/event/EventDisconnected.java delete mode 100644 src/main/java/moe/ymc/acron/s2c/event/EventEntityDeath.java delete mode 100644 src/main/java/moe/ymc/acron/s2c/event/EventLagging.java delete mode 100644 src/main/java/moe/ymc/acron/s2c/event/EventPlayerJoined.java delete mode 100644 src/main/java/moe/ymc/acron/s2c/event/EventPlayerMessage.java delete mode 100644 src/main/java/moe/ymc/acron/s2c/response/EventCmdOut.java delete mode 100644 src/main/java/moe/ymc/acron/s2c/response/EventCmdRes.java delete mode 100644 src/main/java/moe/ymc/acron/s2c/response/EventError.java delete mode 100644 src/main/java/moe/ymc/acron/s2c/response/EventOk.java delete mode 100644 src/main/java/moe/ymc/acron/serialization/Serializer.java delete mode 100644 src/main/resources/acron.mixins.json delete mode 100644 src/main/resources/fabric.mod.json diff --git a/README.md b/README.md index aa7f24f..a82b37b 100644 --- a/README.md +++ b/README.md @@ -46,9 +46,9 @@ For each response JSON parameter, the format is: `(JSON path)` (type, limit, always present or conditions): Description. -## Installation +## Installing the mod -To build this mod, you need to run `gradle build`, and the output JAR will be at `build/libs/acron-x.x.jar`. +To build this mod, you need to run `gradle build` inside `mod/`, and the output JAR will be at `build/libs/acron-x.x.jar`. Then, copy it to the `mods/` folder in your Minecraft server working directory. @@ -164,402 +164,12 @@ Note, listen port and address cannot be changed during runtime. > it will print a warning and skip the whole new configuration file until the > error is fixed. -## Client API +## Using the client -Acron uses polymorphic JSONs when communicating with clients. Therefore, each JSON -has to contain a valid `type` parameter indicating its type: +As Acron has an open protocol, it is easy to implement the client-side service on your own. However, Acron officially +has [libacron](client/libacron), a client library written in C. -```json -{ - "type": "cmd", - "id": 1, - "cmd": "list" -} -``` - -### Request ordering - -To work in a full-duplex environment, each command can specify a `id` parameter. Acron will -return any results or errors with the same ID. - -Sample request: - -```json -{ - "type": "cmd", - "id": 1, - "cmd": "list" -} -``` - -The parameter `id` can be any integer, but it is the client developer's responsibility to -make it a unique value, so he or she can identify it. - -Parameter `id` defaults to -1. - -In response, any non-server-push responses (i. e. messages) will include the same `id` parameter: - -```json -{ - "type": "cmd_result", - "id": 1, - "result": 0, - "success": true -} -``` - -If the server fails to parse the request and returns an error, it will report the default ID `-2`. - -### Error Handling - -Error handling: Besides from the handshake request, which will send errors using HTTP status -codes, all faulty WebSocket requests will receive error in the following format: - -```json -{ - "type": "error", - "id": 1, - "code": 500, - "message": "Error message. Not machine-readable." -} -``` - -Parameters: - -* `.code` (int, HTTP status codes, always present): The machine-readable error code (e. g. 400 for Bad Request). -* `.message` (string, any, always present): The human-readable error message. - -Global error codes: - -* 400: The request is invalid. -* 500: The server encountered an unknown error. - -**`.type` and `.id` are included in every request / response, except for further noticed. Thus, -this document excludes them from the parameter lists.** - -### Handshaking - -Clients need to use the following connection string when connecting to the Acron server: - -``` -ws://host:port/ws?id=client_id&token=client_token&version=0 -``` - -*A better approach for specifying the authentication parameters is using HTTP headers, -but the JavaScript client does not allow so. To extend compatibility, Acron forces -all users to use HTTP query parameters to supply information.* - -Parameters: - -* `id` (required): Client ID set by the administrator. -* `token` (required): Client token set by the administrator. -* `version` (default: 0): API version. Only 0 is accepted at this time. - -Responses: - -* HTTP 400 (Bad Request): If either `id` or `token` is missing, or `version` is not 0. -* HTTP 401 (Unauthorized): If either `id` is not found or `token` does not match the record. -* HTTP 101 (Switching Protocols): The handshake is complete, and the server is upgrading to -WebSocket. - -### Setting Configuration - -This allows clients to set a per-connection default configuration to execute commands. - -Clients can override the configuration temporarily when executing commands. - -Request: -```json -{ - "type": "set_config", - "id": 1, - "world": "overworld", - "pos": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "rot": { - "x": 0.0, - "y": 0.0 - }, - "name": "" -} -``` - -Parameters: - -* `.world` (enum, overworld / nether / end, overworld): The world to run commands in. -* `.pos` (vec3d, *see below*, spawn point of `.world`): The position to run commands at. - * `.x` (double, any within border limit, 0.0): X - * `.y` (double, any within border limit, 0.0): Y - * `.z` (double, any within border limit, 0.0): Z -* `.rot` (vec2f, *see below*, `0.0 0.0`): Rotation. - * `.x` (float, ?, 0.0): X - * `.z` (float, ?, 0.0): Z -* `.name` (string, any, random): Name when running commands. - -When the client connects, Acron will set the configuration to default values. - -Successful response: - -```json -{ - "type": "ok" -} -``` - -This shows that the configuration update is successful. - -### Executing Commands - -The main goal of Acron is to allow clients to run commands. A client can send -any commands, and Acron will schedule them in the background. - -Request: - -```json -{ - "type": "cmd", - "id": 1, - "cmd": "list", - "config": { - - } -} -``` - -Parameters: - -* `.cmd` (string, any valid command, required): The command to execute. It may or may not begin with `/`. -* `.config` (set_config, *see above*, current connection default configuration): Temporary configuration -when running this command. It is the same `set_config` object in the above section, but `type` and `id` -must not be supplied. - -Successful response: - -```json -{ - "type": "ok" -} -``` - -This shows that the command is scheduled. - -If the connection breaks before it is done, it is still executed without any output to the connection. - -Possible failures: - -* 403: This client is not allowed to execute this command. (Configured by rules) - -**Command output:** - -When the command prints a line, Acron will send the following response: - -```json -{ - "type": "cmd_out", - "id": 1, - "sender": "UUID", - "out": "..." -} -``` - -Parameters: - -* `.sender` (UUID, any UUID, always present): Sender UUID. -* `.out` (string, any, always present): Output. - -**Command result:** - -When the command finishes without issues (?), Acron will send the following response: - -```json -{ - "type": "cmd_result", - "id": 1, - "result": 0, - "success": true -} -``` - -All parameters always present. - -> **Note** -> -> The result completely depends on Minecraft server's response. -> It may not be reliable, and the values of `.result` and `.success` are -> undocumented. - -### Receiving Messages - -Another major part of Acron is to allow clients receive events from the server. - -Every event will have a pre-defined `type` with other custom parameters. Parameter `id` will not -present in events. - -> **Contributor Guide** -> -> Internally, all message Acron sends to clients are called events, including -> command results. - -#### Player joined - -Response: - -```json -{ - "type": "join", - "player": { - "name": "", - "uuid": "", - "pos": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "world": "end" - } -} -``` - -Parameters: - -* `.player` (entity, see below, always present): The player. - * `.name` (string, any valid Minecraft username, always present): Username. - * `.uuid` (uuid, UUID, always present): UUID. - * `.pos` (vec3d, see below, always present): The position he or she joins. - * `.x` (double, any within border limit, 0.0): X - * `.y` (double, any within border limit, 0.0): Y - * `.z` (double, any within border limit, 0.0): Z - * `.world` (enum, overworld / nether / end, not present if Acron cannot determine the world): The dimension -he or she joins. - -#### Player Disconnected - -Response: - -```json -{ - "type": "disconnect", - "player": { - "name": "", - "uuid": "", - "pos": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "world": "end" - }, - "reason": "" -} -``` - -Parameters: - -* `.player` (entity, see below, null only when the server cannot verify the user): The player. - * `.name` (string, any valid Minecraft username, always present): Username. - * `.uuid` (uuid, UUID, always present): UUID. - * `.pos` (vec3d, see below, always present): The position he or she leaves. - * `.x` (double, any within border limit, 0.0): X - * `.y` (double, any within border limit, 0.0): Y - * `.z` (double, any within border limit, 0.0): Z - * `.world` (enum, overworld / nether / end, not present if Acron cannot determine the world): The dimension - he or she leaves. -* `.reason` (string, any valid disconnect reason, always present): Disconnect reason. - -#### Player Message - -Response: - -```json -{ - "type": "message", - "player": { - "name": "", - "uuid": "", - "pos": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "world": "end" - }, - "text": "" -} -``` - -Parameters: - -* `.player` (entity, see below, always present): The player. - * `.name` (string, any valid Minecraft username, always present): Username. - * `.uuid` (uuid, UUID, always present): UUID. - * `.pos` (vec3d, see below, always present): The position he or she sends the message. - * `.x` (double, any within border limit, 0.0): X - * `.y` (double, any within border limit, 0.0): Y - * `.z` (double, any within border limit, 0.0): Z - * `.world` (enum, overworld / nether / end, not present if Acron cannot determine the world): The dimension -he or she sends the message. -* `.text` (string, any valid Minecraft message, always present): The message. - -#### Entity Death - -Response: - -```json -{ - "type": "death", - "entity": { - "name": "", - "uuid": "", - "pos": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "world": "end" - }, - "message": "" -} -``` - -Parameters: - -* `.entity` (entity, see below, always present): The entity. - * `.name` (string, any, always present): Default name or custom name of the entity. - * `.uuid` (uuid, UUID, always present): UUID. - * `.pos` (vec3d, see below, always present): The position of the entity when died. - * `.x` (double, any within border limit, 0.0): X - * `.y` (double, any within border limit, 0.0): Y - * `.z` (double, any within border limit, 0.0): Z - * `.world` (enum, overworld / nether / end, not present if Acron cannot determine the world): The dimension -of the entity when died. -* `.message` (string, any valid death message, always present): The user-readable death message. - -> **Roadmap** -> -> Parsing the death message and sending a more machine-readable message is on the roadmap. - -#### Server Lagging - -Acron will send this event when the server prints -`Can't keep up! Is the server overloaded? Running 4313ms or 86 ticks behind` to the standard output. - -Response: - -```json -{ - "type": "lagging", - "ms": 100, - "ticks": 1000 -} -``` - -Parameters: - -* `.ms` (long, >= 0, always present): Running {}ms behind. -* `.ticks` (long, >= 0, always present): Running {} ticks behind. +Acron also provides a cli tool: [acronc(1)](client/acronc) for administrators. ## Contributing @@ -569,4 +179,4 @@ the email subject. If you are sending a patch, please include `[PATCH]` in the s ## License -Acron is licensed under GPL-2.0-only except libac is licensed under LGPL-2.1-only. +Acron is licensed under GPL-2.0-only except [libacron](client/libacron) is licensed under LGPL-2.1-only. diff --git a/acron.json b/acron.json deleted file mode 100644 index 83edef5..0000000 --- a/acron.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "clients": [ - { - "id": "1", - "token": "a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3", - "rules": [ - { - "regex": ".*", - "action": "allow", - "display": true - } - ] - }, - { - "id": "2", - "token": "a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3" - } - ] -} \ No newline at end of file diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 4ea402e..0000000 --- a/build.gradle +++ /dev/null @@ -1,78 +0,0 @@ -plugins { - id 'fabric-loom' version '0.12-SNAPSHOT' - id 'maven-publish' -} - -version = project.mod_version -group = project.maven_group - -repositories { - // Add repositories to retrieve artifacts from in here. - // You should only use this when depending on other mods because - // Loom adds the essential maven repositories to download Minecraft and libraries from automatically. - // See https://docs.gradle.org/current/userguide/declaring_repositories.html - // for more information about repositories. -} - -dependencies { - // To change the versions see the gradle.properties file - minecraft "com.mojang:minecraft:${project.minecraft_version}" - mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" - modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" -} - -processResources { - inputs.property "version", project.version - filteringCharset "UTF-8" - - filesMatching("fabric.mod.json") { - expand "version": project.version - } -} - -def targetJavaVersion = 16 -tasks.withType(JavaCompile).configureEach { - // ensure that the encoding is set to UTF-8, no matter what the system default is - // this fixes some edge cases with special characters not displaying correctly - // see http://yodaconditions.net/blog/fix-for-java-file-encoding-problems-with-gradle.html - // If Javadoc is generated, this must be specified in that task too. - it.options.encoding = "UTF-8" - if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) { - it.options.release = targetJavaVersion - } -} - -java { - def javaVersion = JavaVersion.toVersion(targetJavaVersion) - if (JavaVersion.current() < javaVersion) { - toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion) - } - archivesBaseName = project.archives_base_name - // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task - // if it is present. - // If you remove this line, sources will not be generated. - withSourcesJar() -} - -jar { - from("LICENSE") { - rename { "${it}_${project.archivesBaseName}" } - } -} - -// configure the maven publication -publishing { - publications { - mavenJava(MavenPublication) { - from components.java - } - } - - // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing. - repositories { - // Add repositories to publish to here. - // Notice: This block does NOT have the same function as the block in the top level. - // The repositories here will be used for publishing your artifact, not for - // retrieving dependencies. - } -} diff --git a/gradle.properties b/gradle.properties deleted file mode 100644 index 85e12c2..0000000 --- a/gradle.properties +++ /dev/null @@ -1,11 +0,0 @@ -# Done to increase the memory available to gradle. -org.gradle.jvmargs=-Xmx1G -# Fabric Properties -# check these on https://modmuss50.me/fabric.html -minecraft_version=1.17.1 -yarn_mappings=1.17.1+build.65 -loader_version=0.14.4 -# Mod Properties -mod_version=1.0 -maven_group=moe.ymc -archives_base_name=acron diff --git a/mod/README.md b/mod/README.md new file mode 100644 index 0000000..d84a718 --- /dev/null +++ b/mod/README.md @@ -0,0 +1,403 @@ +# Acron Server-Side Mod + +## Installation and Configuration + +See [README.md](../README.md). + +## Client API + +Acron uses polymorphic JSONs when communicating with clients. Therefore, each JSON +has to contain a valid `type` parameter indicating its type: + +```json +{ + "type": "cmd", + "id": 1, + "cmd": "list" +} +``` + +### Request ordering + +To work in a full-duplex environment, each command can specify a `id` parameter. Acron will +return any results or errors with the same ID. + +Sample request: + +```json +{ + "type": "cmd", + "id": 1, + "cmd": "list" +} +``` + +The parameter `id` can be any integer, but it is the client developer's responsibility to +make it a unique value, so he or she can identify it. + +Parameter `id` defaults to -1. + +In response, any non-server-push responses (i. e. messages) will include the same `id` parameter: + +```json +{ + "type": "cmd_result", + "id": 1, + "result": 0, + "success": true +} +``` + +If the server fails to parse the request and returns an error, it will report the default ID `-2`. + +### Error Handling + +Error handling: Besides from the handshake request, which will send errors using HTTP status +codes, all faulty WebSocket requests will receive error in the following format: + +```json +{ + "type": "error", + "id": 1, + "code": 500, + "message": "Error message. Not machine-readable." +} +``` + +Parameters: + +* `.code` (int, HTTP status codes, always present): The machine-readable error code (e. g. 400 for Bad Request). +* `.message` (string, any, always present): The human-readable error message. + +Global error codes: + +* 400: The request is invalid. +* 500: The server encountered an unknown error. + +**`.type` and `.id` are included in every request / response, except for further noticed. Thus, +this document excludes them from the parameter lists.** + +### Handshaking + +Clients need to use the following connection string when connecting to the Acron server: + +``` +ws://host:port/ws?id=client_id&token=client_token&version=0 +``` + +*A better approach for specifying the authentication parameters is using HTTP headers, +but the JavaScript client does not allow so. To extend compatibility, Acron forces +all users to use HTTP query parameters to supply information.* + +Parameters: + +* `id` (required): Client ID set by the administrator. +* `token` (required): Client token set by the administrator. +* `version` (default: 0): API version. Only 0 is accepted at this time. + +Responses: + +* HTTP 400 (Bad Request): If either `id` or `token` is missing, or `version` is not 0. +* HTTP 401 (Unauthorized): If either `id` is not found or `token` does not match the record. +* HTTP 101 (Switching Protocols): The handshake is complete, and the server is upgrading to +WebSocket. + +### Setting Configuration + +This allows clients to set a per-connection default configuration to execute commands. + +Clients can override the configuration temporarily when executing commands. + +Request: +```json +{ + "type": "set_config", + "id": 1, + "world": "overworld", + "pos": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "rot": { + "x": 0.0, + "y": 0.0 + }, + "name": "" +} +``` + +Parameters: + +* `.world` (enum, overworld / nether / end, overworld): The world to run commands in. +* `.pos` (vec3d, *see below*, spawn point of `.world`): The position to run commands at. + * `.x` (double, any within border limit, 0.0): X + * `.y` (double, any within border limit, 0.0): Y + * `.z` (double, any within border limit, 0.0): Z +* `.rot` (vec2f, *see below*, `0.0 0.0`): Rotation. + * `.x` (float, ?, 0.0): X + * `.z` (float, ?, 0.0): Z +* `.name` (string, any, random): Name when running commands. + +When the client connects, Acron will set the configuration to default values. + +Successful response: + +```json +{ + "type": "ok" +} +``` + +This shows that the configuration update is successful. + +### Executing Commands + +The main goal of Acron is to allow clients to run commands. A client can send +any commands, and Acron will schedule them in the background. + +Request: + +```json +{ + "type": "cmd", + "id": 1, + "cmd": "list", + "config": { + + } +} +``` + +Parameters: + +* `.cmd` (string, any valid command, required): The command to execute. It may or may not begin with `/`. +* `.config` (set_config, *see above*, current connection default configuration): Temporary configuration +when running this command. It is the same `set_config` object in the above section, but `type` and `id` +must not be supplied. + +Successful response: + +```json +{ + "type": "ok" +} +``` + +This shows that the command is scheduled. + +If the connection breaks before it is done, it is still executed without any output to the connection. + +Possible failures: + +* 403: This client is not allowed to execute this command. (Configured by rules) + +**Command output:** + +When the command prints a line, Acron will send the following response: + +```json +{ + "type": "cmd_out", + "id": 1, + "sender": "UUID", + "out": "..." +} +``` + +Parameters: + +* `.sender` (UUID, any UUID, always present): Sender UUID. +* `.out` (string, any, always present): Output. + +**Command result:** + +When the command finishes without issues (?), Acron will send the following response: + +```json +{ + "type": "cmd_result", + "id": 1, + "result": 0, + "success": true +} +``` + +All parameters always present. + +> **Note** +> +> The result completely depends on Minecraft server's response. +> It may not be reliable, and the values of `.result` and `.success` are +> undocumented. + +### Receiving Messages + +Another major part of Acron is to allow clients receive events from the server. + +Every event will have a pre-defined `type` with other custom parameters. Parameter `id` will not +present in events. + +> **Contributor Guide** +> +> Internally, all message Acron sends to clients are called events, including +> command results. + +#### Player joined + +Response: + +```json +{ + "type": "join", + "player": { + "name": "", + "uuid": "", + "pos": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "world": "end" + } +} +``` + +Parameters: + +* `.player` (entity, see below, always present): The player. + * `.name` (string, any valid Minecraft username, always present): Username. + * `.uuid` (uuid, UUID, always present): UUID. + * `.pos` (vec3d, see below, always present): The position he or she joins. + * `.x` (double, any within border limit, 0.0): X + * `.y` (double, any within border limit, 0.0): Y + * `.z` (double, any within border limit, 0.0): Z + * `.world` (enum, overworld / nether / end, not present if Acron cannot determine the world): The dimension +he or she joins. + +#### Player Disconnected + +Response: + +```json +{ + "type": "disconnect", + "player": { + "name": "", + "uuid": "", + "pos": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "world": "end" + }, + "reason": "" +} +``` + +Parameters: + +* `.player` (entity, see below, null only when the server cannot verify the user): The player. + * `.name` (string, any valid Minecraft username, always present): Username. + * `.uuid` (uuid, UUID, always present): UUID. + * `.pos` (vec3d, see below, always present): The position he or she leaves. + * `.x` (double, any within border limit, 0.0): X + * `.y` (double, any within border limit, 0.0): Y + * `.z` (double, any within border limit, 0.0): Z + * `.world` (enum, overworld / nether / end, not present if Acron cannot determine the world): The dimension + he or she leaves. +* `.reason` (string, any valid disconnect reason, always present): Disconnect reason. + +#### Player Message + +Response: + +```json +{ + "type": "message", + "player": { + "name": "", + "uuid": "", + "pos": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "world": "end" + }, + "text": "" +} +``` + +Parameters: + +* `.player` (entity, see below, always present): The player. + * `.name` (string, any valid Minecraft username, always present): Username. + * `.uuid` (uuid, UUID, always present): UUID. + * `.pos` (vec3d, see below, always present): The position he or she sends the message. + * `.x` (double, any within border limit, 0.0): X + * `.y` (double, any within border limit, 0.0): Y + * `.z` (double, any within border limit, 0.0): Z + * `.world` (enum, overworld / nether / end, not present if Acron cannot determine the world): The dimension +he or she sends the message. +* `.text` (string, any valid Minecraft message, always present): The message. + +#### Entity Death + +Response: + +```json +{ + "type": "death", + "entity": { + "name": "", + "uuid": "", + "pos": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "world": "end" + }, + "message": "" +} +``` + +Parameters: + +* `.entity` (entity, see below, always present): The entity. + * `.name` (string, any, always present): Default name or custom name of the entity. + * `.uuid` (uuid, UUID, always present): UUID. + * `.pos` (vec3d, see below, always present): The position of the entity when died. + * `.x` (double, any within border limit, 0.0): X + * `.y` (double, any within border limit, 0.0): Y + * `.z` (double, any within border limit, 0.0): Z + * `.world` (enum, overworld / nether / end, not present if Acron cannot determine the world): The dimension +of the entity when died. +* `.message` (string, any valid death message, always present): The user-readable death message. + +> **Roadmap** +> +> Parsing the death message and sending a more machine-readable message is on the roadmap. + +#### Server Lagging + +Acron will send this event when the server prints +`Can't keep up! Is the server overloaded? Running 4313ms or 86 ticks behind` to the standard output. + +Response: + +```json +{ + "type": "lagging", + "ms": 100, + "ticks": 1000 +} +``` + +Parameters: + +* `.ms` (long, >= 0, always present): Running {}ms behind. +* `.ticks` (long, >= 0, always present): Running {} ticks behind. + diff --git a/mod/acron.json b/mod/acron.json new file mode 100644 index 0000000..83edef5 --- /dev/null +++ b/mod/acron.json @@ -0,0 +1,19 @@ +{ + "clients": [ + { + "id": "1", + "token": "a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3", + "rules": [ + { + "regex": ".*", + "action": "allow", + "display": true + } + ] + }, + { + "id": "2", + "token": "a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3" + } + ] +} \ No newline at end of file diff --git a/mod/build.gradle b/mod/build.gradle new file mode 100644 index 0000000..4ea402e --- /dev/null +++ b/mod/build.gradle @@ -0,0 +1,78 @@ +plugins { + id 'fabric-loom' version '0.12-SNAPSHOT' + id 'maven-publish' +} + +version = project.mod_version +group = project.maven_group + +repositories { + // Add repositories to retrieve artifacts from in here. + // You should only use this when depending on other mods because + // Loom adds the essential maven repositories to download Minecraft and libraries from automatically. + // See https://docs.gradle.org/current/userguide/declaring_repositories.html + // for more information about repositories. +} + +dependencies { + // To change the versions see the gradle.properties file + minecraft "com.mojang:minecraft:${project.minecraft_version}" + mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" + modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" +} + +processResources { + inputs.property "version", project.version + filteringCharset "UTF-8" + + filesMatching("fabric.mod.json") { + expand "version": project.version + } +} + +def targetJavaVersion = 16 +tasks.withType(JavaCompile).configureEach { + // ensure that the encoding is set to UTF-8, no matter what the system default is + // this fixes some edge cases with special characters not displaying correctly + // see http://yodaconditions.net/blog/fix-for-java-file-encoding-problems-with-gradle.html + // If Javadoc is generated, this must be specified in that task too. + it.options.encoding = "UTF-8" + if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) { + it.options.release = targetJavaVersion + } +} + +java { + def javaVersion = JavaVersion.toVersion(targetJavaVersion) + if (JavaVersion.current() < javaVersion) { + toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion) + } + archivesBaseName = project.archives_base_name + // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task + // if it is present. + // If you remove this line, sources will not be generated. + withSourcesJar() +} + +jar { + from("LICENSE") { + rename { "${it}_${project.archivesBaseName}" } + } +} + +// configure the maven publication +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + } + } + + // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing. + repositories { + // Add repositories to publish to here. + // Notice: This block does NOT have the same function as the block in the top level. + // The repositories here will be used for publishing your artifact, not for + // retrieving dependencies. + } +} diff --git a/mod/gradle.properties b/mod/gradle.properties new file mode 100644 index 0000000..85e12c2 --- /dev/null +++ b/mod/gradle.properties @@ -0,0 +1,11 @@ +# Done to increase the memory available to gradle. +org.gradle.jvmargs=-Xmx1G +# Fabric Properties +# check these on https://modmuss50.me/fabric.html +minecraft_version=1.17.1 +yarn_mappings=1.17.1+build.65 +loader_version=0.14.4 +# Mod Properties +mod_version=1.0 +maven_group=moe.ymc +archives_base_name=acron diff --git a/mod/settings.gradle b/mod/settings.gradle new file mode 100644 index 0000000..f91a4fe --- /dev/null +++ b/mod/settings.gradle @@ -0,0 +1,9 @@ +pluginManagement { + repositories { + maven { + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + gradlePluginPortal() + } +} diff --git a/mod/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java b/mod/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java new file mode 100644 index 0000000..51f05ff --- /dev/null +++ b/mod/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gson.typeadapters; + +import com.google.gson.*; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Adapts values whose runtime type may differ from their declaration type. This + * is necessary when a field's type is not the same type that GSON should create + * when deserializing that field. For example, consider these types: + *
   {@code
+ *   abstract class Shape {
+ *     int x;
+ *     int y;
+ *   }
+ *   class Circle extends Shape {
+ *     int radius;
+ *   }
+ *   class Rectangle extends Shape {
+ *     int width;
+ *     int height;
+ *   }
+ *   class Diamond extends Shape {
+ *     int width;
+ *     int height;
+ *   }
+ *   class Drawing {
+ *     Shape bottomShape;
+ *     Shape topShape;
+ *   }
+ * }
+ *

Without additional type information, the serialized JSON is ambiguous. Is + * the bottom shape in this drawing a rectangle or a diamond?

   {@code
+ *   {
+ *     "bottomShape": {
+ *       "width": 10,
+ *       "height": 5,
+ *       "x": 0,
+ *       "y": 0
+ *     },
+ *     "topShape": {
+ *       "radius": 2,
+ *       "x": 4,
+ *       "y": 1
+ *     }
+ *   }}
+ * This class addresses this problem by adding type information to the + * serialized JSON and honoring that type information when the JSON is + * deserialized:
   {@code
+ *   {
+ *     "bottomShape": {
+ *       "type": "Diamond",
+ *       "width": 10,
+ *       "height": 5,
+ *       "x": 0,
+ *       "y": 0
+ *     },
+ *     "topShape": {
+ *       "type": "Circle",
+ *       "radius": 2,
+ *       "x": 4,
+ *       "y": 1
+ *     }
+ *   }}
+ * Both the type field name ({@code "type"}) and the type labels ({@code + * "Rectangle"}) are configurable. + * + *

Registering Types

+ * Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field + * name to the {@link #of} factory method. If you don't supply an explicit type + * field name, {@code "type"} will be used.
   {@code
+ *   RuntimeTypeAdapterFactory shapeAdapterFactory
+ *       = RuntimeTypeAdapterFactory.of(Shape.class, "type");
+ * }
+ * Next register all of your subtypes. Every subtype must be explicitly + * registered. This protects your application from injection attacks. If you + * don't supply an explicit type label, the type's simple name will be used. + *
   {@code
+ *   shapeAdapterFactory.registerSubtype(Rectangle.class, "Rectangle");
+ *   shapeAdapterFactory.registerSubtype(Circle.class, "Circle");
+ *   shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond");
+ * }
+ * Finally, register the type adapter factory in your application's GSON builder: + *
   {@code
+ *   Gson gson = new GsonBuilder()
+ *       .registerTypeAdapterFactory(shapeAdapterFactory)
+ *       .create();
+ * }
+ * Like {@code GsonBuilder}, this API supports chaining:
   {@code
+ *   RuntimeTypeAdapterFactory shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
+ *       .registerSubtype(Rectangle.class)
+ *       .registerSubtype(Circle.class)
+ *       .registerSubtype(Diamond.class);
+ * }
+ * + *

Serialization and deserialization

+ * In order to serialize and deserialize a polymorphic object, + * you must specify the base type explicitly. + *
   {@code
+ *   Diamond diamond = new Diamond();
+ *   String json = gson.toJson(diamond, Shape.class);
+ * }
+ * And then: + *
   {@code
+ *   Shape shape = gson.fromJson(json, Shape.class);
+ * }
+ */ +public final class RuntimeTypeAdapterFactory implements TypeAdapterFactory { + private final Class baseType; + private final String typeFieldName; + private final Map> labelToSubtype = new LinkedHashMap<>(); + private final Map, String> subtypeToLabel = new LinkedHashMap<>(); + private final boolean maintainType; + + private RuntimeTypeAdapterFactory(Class baseType, String typeFieldName, boolean maintainType) { + if (typeFieldName == null || baseType == null) { + throw new NullPointerException(); + } + this.baseType = baseType; + this.typeFieldName = typeFieldName; + this.maintainType = maintainType; + } + + /** + * Creates a new runtime type adapter using for {@code baseType} using {@code + * typeFieldName} as the type field name. Type field names are case sensitive. + * {@code maintainType} flag decide if the type will be stored in pojo or not. + */ + public static RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName, boolean maintainType) { + return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, maintainType); + } + + /** + * Creates a new runtime type adapter using for {@code baseType} using {@code + * typeFieldName} as the type field name. Type field names are case sensitive. + */ + public static RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName) { + return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, false); + } + + /** + * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as + * the type field name. + */ + public static RuntimeTypeAdapterFactory of(Class baseType) { + return new RuntimeTypeAdapterFactory<>(baseType, "type", false); + } + + /** + * Registers {@code type} identified by {@code label}. Labels are case + * sensitive. + * + * @throws IllegalArgumentException if either {@code type} or {@code label} + * have already been registered on this type adapter. + */ + public RuntimeTypeAdapterFactory registerSubtype(Class type, String label) { + if (type == null || label == null) { + throw new NullPointerException(); + } + if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) { + throw new IllegalArgumentException("types and labels must be unique"); + } + labelToSubtype.put(label, type); + subtypeToLabel.put(type, label); + return this; + } + + /** + * Registers {@code type} identified by its {@link Class#getSimpleName simple + * name}. Labels are case sensitive. + * + * @throws IllegalArgumentException if either {@code type} or its simple name + * have already been registered on this type adapter. + */ + public RuntimeTypeAdapterFactory registerSubtype(Class type) { + return registerSubtype(type, type.getSimpleName()); + } + + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + // Workaround found at https://github.com/google/gson/issues/712#issuecomment-148955110 + if (null == type || !baseType.isAssignableFrom(type.getRawType())) { + // if (type.getRawType() != baseType) { + return null; + } + + final TypeAdapter jsonElementAdapter = gson.getAdapter(JsonElement.class); + final Map> labelToDelegate = new LinkedHashMap<>(); + final Map, TypeAdapter> subtypeToDelegate = new LinkedHashMap<>(); + for (Map.Entry> entry : labelToSubtype.entrySet()) { + TypeAdapter delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue())); + labelToDelegate.put(entry.getKey(), delegate); + subtypeToDelegate.put(entry.getValue(), delegate); + } + + return new TypeAdapter() { + @Override public R read(JsonReader in) throws IOException { + JsonElement jsonElement = jsonElementAdapter.read(in); + JsonElement labelJsonElement; + if (maintainType) { + labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName); + } else { + labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName); + } + + if (labelJsonElement == null) { + throw new JsonParseException("cannot deserialize " + baseType + + " because it does not define a field named " + typeFieldName); + } + String label = labelJsonElement.getAsString(); + @SuppressWarnings("unchecked") // registration requires that subtype extends T + TypeAdapter delegate = (TypeAdapter) labelToDelegate.get(label); + if (delegate == null) { + throw new JsonParseException("cannot deserialize " + baseType + " subtype named " + + label + "; did you forget to register a subtype?"); + } + return delegate.fromJsonTree(jsonElement); + } + + @Override public void write(JsonWriter out, R value) throws IOException { + Class srcType = value.getClass(); + String label = subtypeToLabel.get(srcType); + @SuppressWarnings("unchecked") // registration requires that subtype extends T + TypeAdapter delegate = (TypeAdapter) subtypeToDelegate.get(srcType); + if (delegate == null) { + throw new JsonParseException("cannot serialize " + srcType.getName() + + "; did you forget to register a subtype?"); + } + JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject(); + + if (maintainType) { + jsonElementAdapter.write(out, jsonObject); + return; + } + + JsonObject clone = new JsonObject(); + + if (jsonObject.has(typeFieldName)) { + throw new JsonParseException("cannot serialize " + srcType.getName() + + " because it already defines a field named " + typeFieldName); + } + clone.add(typeFieldName, new JsonPrimitive(label)); + + for (Map.Entry e : jsonObject.entrySet()) { + clone.add(e.getKey(), e.getValue()); + } + jsonElementAdapter.write(out, clone); + } + }.nullSafe(); + } +} diff --git a/mod/src/main/java/moe/ymc/acron/Acron.java b/mod/src/main/java/moe/ymc/acron/Acron.java new file mode 100644 index 0000000..d6f6214 --- /dev/null +++ b/mod/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/mod/src/main/java/moe/ymc/acron/MinecraftServerHolder.java b/mod/src/main/java/moe/ymc/acron/MinecraftServerHolder.java new file mode 100644 index 0000000..f522884 --- /dev/null +++ b/mod/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/mod/src/main/java/moe/ymc/acron/auth/Action.java b/mod/src/main/java/moe/ymc/acron/auth/Action.java new file mode 100644 index 0000000..17d29a3 --- /dev/null +++ b/mod/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/mod/src/main/java/moe/ymc/acron/auth/Client.java b/mod/src/main/java/moe/ymc/acron/auth/Client.java new file mode 100644 index 0000000..2124ad4 --- /dev/null +++ b/mod/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/mod/src/main/java/moe/ymc/acron/auth/PolicyChecker.java b/mod/src/main/java/moe/ymc/acron/auth/PolicyChecker.java new file mode 100644 index 0000000..2ab7b97 --- /dev/null +++ b/mod/src/main/java/moe/ymc/acron/auth/PolicyChecker.java @@ -0,0 +1,42 @@ +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) { + final String commandToMatch = command.startsWith("/") ? + command.substring(1) : + command; + for (int i = 0; i < client.rules().length; i++) { + final Rule rule = client.rules()[i]; + if (rule.cmdPattern().matcher(commandToMatch).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/mod/src/main/java/moe/ymc/acron/auth/Rule.java b/mod/src/main/java/moe/ymc/acron/auth/Rule.java new file mode 100644 index 0000000..55ad0d7 --- /dev/null +++ b/mod/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/mod/src/main/java/moe/ymc/acron/c2s/ReqCmd.java b/mod/src/main/java/moe/ymc/acron/c2s/ReqCmd.java new file mode 100644 index 0000000..6f34b07 --- /dev/null +++ b/mod/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() : + -1; + 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/mod/src/main/java/moe/ymc/acron/c2s/ReqSetConfig.java b/mod/src/main/java/moe/ymc/acron/c2s/ReqSetConfig.java new file mode 100644 index 0000000..fcddf35 --- /dev/null +++ b/mod/src/main/java/moe/ymc/acron/c2s/ReqSetConfig.java @@ -0,0 +1,63 @@ +package moe.ymc.acron.c2s; + +import com.google.gson.*; +import com.google.gson.annotations.SerializedName; +import moe.ymc.acron.common.Vec2f; +import moe.ymc.acron.common.Vec3d; +import moe.ymc.acron.common.WorldKey; +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 id; + } + + 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; + if (object.has("world")) { + world = context.deserialize(object.get("world"), WorldKey.class); + // https://stackoverflow.com/a/49574019 + if (world == null) { + throw new JsonParseException("Invalid world"); + } + } else { + world = 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(id, + world, + pos, + rot, + name); + } + } + +} \ No newline at end of file diff --git a/mod/src/main/java/moe/ymc/acron/c2s/Request.java b/mod/src/main/java/moe/ymc/acron/c2s/Request.java new file mode 100644 index 0000000..af81705 --- /dev/null +++ b/mod/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/mod/src/main/java/moe/ymc/acron/cmd/CmdOut.java b/mod/src/main/java/moe/ymc/acron/cmd/CmdOut.java new file mode 100644 index 0000000..55eadf1 --- /dev/null +++ b/mod/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.response.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/mod/src/main/java/moe/ymc/acron/cmd/CmdQueue.java b/mod/src/main/java/moe/ymc/acron/cmd/CmdQueue.java new file mode 100644 index 0000000..3c49143 --- /dev/null +++ b/mod/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/mod/src/main/java/moe/ymc/acron/cmd/CmdResConsumer.java b/mod/src/main/java/moe/ymc/acron/cmd/CmdResConsumer.java new file mode 100644 index 0000000..d22b77e --- /dev/null +++ b/mod/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.response.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/mod/src/main/java/moe/ymc/acron/cmd/CmdSrc.java b/mod/src/main/java/moe/ymc/acron/cmd/CmdSrc.java new file mode 100644 index 0000000..983b4ed --- /dev/null +++ b/mod/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/mod/src/main/java/moe/ymc/acron/common/Vec2f.java b/mod/src/main/java/moe/ymc/acron/common/Vec2f.java new file mode 100644 index 0000000..5ab3dfd --- /dev/null +++ b/mod/src/main/java/moe/ymc/acron/common/Vec2f.java @@ -0,0 +1,25 @@ +package moe.ymc.acron.common; + +import com.google.gson.*; +import com.google.gson.annotations.SerializedName; + +import java.lang.reflect.Type; + +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); + } + } +} \ No newline at end of file diff --git a/mod/src/main/java/moe/ymc/acron/common/Vec3d.java b/mod/src/main/java/moe/ymc/acron/common/Vec3d.java new file mode 100644 index 0000000..593019f --- /dev/null +++ b/mod/src/main/java/moe/ymc/acron/common/Vec3d.java @@ -0,0 +1,34 @@ +package moe.ymc.acron.common; + +import com.google.gson.*; +import com.google.gson.annotations.SerializedName; +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.Type; + +public record Vec3d(@SerializedName("x") double x, + @SerializedName("y") double y, + @SerializedName("z") double z) { + public Vec3d(@NotNull net.minecraft.util.math.Vec3d vec3d) { + this(vec3d.x, vec3d.y, vec3d.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); + } + } +} \ No newline at end of file diff --git a/mod/src/main/java/moe/ymc/acron/common/WorldKey.java b/mod/src/main/java/moe/ymc/acron/common/WorldKey.java new file mode 100644 index 0000000..fa10d54 --- /dev/null +++ b/mod/src/main/java/moe/ymc/acron/common/WorldKey.java @@ -0,0 +1,35 @@ +package moe.ymc.acron.common; + +import com.google.gson.annotations.SerializedName; +import net.minecraft.util.Identifier; +import net.minecraft.world.dimension.DimensionType; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public enum WorldKey { + @SerializedName("overworld") + OVERWORLD, + @SerializedName("nether") + NETHER, + @SerializedName("end") + END; + + private static final Logger LOGGER = LogManager.getLogger(); + + public static @Nullable WorldKey create(@NotNull Identifier identifier) { + if (identifier.equals(DimensionType.OVERWORLD_ID)) { + return OVERWORLD; + } else if (identifier.equals(DimensionType.THE_NETHER_ID)) { + return NETHER; + } else if (identifier.equals(DimensionType.THE_END_ID)) { + return END; + } else { + LOGGER.warn("Unknown world {}:{}. Returning NULL to the client.", + identifier.getNamespace(), + identifier.getPath()); + return null; + } + } +} \ No newline at end of file diff --git a/mod/src/main/java/moe/ymc/acron/config/Config.java b/mod/src/main/java/moe/ymc/acron/config/Config.java new file mode 100644 index 0000000..3749c25 --- /dev/null +++ b/mod/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/mod/src/main/java/moe/ymc/acron/config/ConfigReloadCmd.java b/mod/src/main/java/moe/ymc/acron/config/ConfigReloadCmd.java new file mode 100644 index 0000000..2774c4d --- /dev/null +++ b/mod/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/mod/src/main/java/moe/ymc/acron/config/json/Client.java b/mod/src/main/java/moe/ymc/acron/config/json/Client.java new file mode 100644 index 0000000..4d31308 --- /dev/null +++ b/mod/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/mod/src/main/java/moe/ymc/acron/config/json/Config.java b/mod/src/main/java/moe/ymc/acron/config/json/Config.java new file mode 100644 index 0000000..e8c5a83 --- /dev/null +++ b/mod/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/mod/src/main/java/moe/ymc/acron/config/json/ConfigDeserializationException.java b/mod/src/main/java/moe/ymc/acron/config/json/ConfigDeserializationException.java new file mode 100644 index 0000000..baf5b35 --- /dev/null +++ b/mod/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/mod/src/main/java/moe/ymc/acron/config/json/ConfigDeserializer.java b/mod/src/main/java/moe/ymc/acron/config/json/ConfigDeserializer.java new file mode 100644 index 0000000..e91b355 --- /dev/null +++ b/mod/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/mod/src/main/java/moe/ymc/acron/config/json/ConfigJsonObject.java b/mod/src/main/java/moe/ymc/acron/config/json/ConfigJsonObject.java new file mode 100644 index 0000000..0efd9a9 --- /dev/null +++ b/mod/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/mod/src/main/java/moe/ymc/acron/config/json/Rule.java b/mod/src/main/java/moe/ymc/acron/config/json/Rule.java new file mode 100644 index 0000000..114e17d --- /dev/null +++ b/mod/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/mod/src/main/java/moe/ymc/acron/jvav/Pair.java b/mod/src/main/java/moe/ymc/acron/jvav/Pair.java new file mode 100644 index 0000000..29b83dc --- /dev/null +++ b/mod/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/mod/src/main/java/moe/ymc/acron/mixin/CommandManagerMixin.java b/mod/src/main/java/moe/ymc/acron/mixin/CommandManagerMixin.java new file mode 100644 index 0000000..9aaed2e --- /dev/null +++ b/mod/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 AC_LOGGER = LogManager.getLogger(); + + @Shadow + @Final + private CommandDispatcher dispatcher; + + @Inject(method = "", at = @At("RETURN")) + private void onRegister(CommandManager.RegistrationEnvironment arg, CallbackInfo ci) { + AC_LOGGER.debug("onRegister"); + dispatcher.register( + literal("acron").requires(player -> player.hasPermissionLevel(4)).then( + literal("rule").then( + literal("update") + .executes(new ConfigReloadCmd())) + ) + ); + } +} diff --git a/mod/src/main/java/moe/ymc/acron/mixin/LivingEntityMixin.java b/mod/src/main/java/moe/ymc/acron/mixin/LivingEntityMixin.java new file mode 100644 index 0000000..9e16569 --- /dev/null +++ b/mod/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.EventQueue; +import moe.ymc.acron.s2c.event.EventEntityDeath; +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 AC_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) { + AC_LOGGER.debug("onDeath[{}]", + getUuid()); + EventQueue.enqueue(new EventEntityDeath(new Entity(this), + getDamageTracker().getDeathMessage().getString())); + } +} \ No newline at end of file diff --git a/mod/src/main/java/moe/ymc/acron/mixin/MinecraftDedicatedServerMixin.java b/mod/src/main/java/moe/ymc/acron/mixin/MinecraftDedicatedServerMixin.java new file mode 100644 index 0000000..32d2fbf --- /dev/null +++ b/mod/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 AC_LOGGER = LogManager.getLogger(); + + @Inject(at = @At("RETURN"), method = "") + private void init(CallbackInfo info) { + AC_LOGGER.debug("init"); + MinecraftServerHolder.setServer((MinecraftDedicatedServer) (Object) this); + } +} diff --git a/mod/src/main/java/moe/ymc/acron/mixin/MinecraftServerMixin.java b/mod/src/main/java/moe/ymc/acron/mixin/MinecraftServerMixin.java new file mode 100644 index 0000000..cb813b4 --- /dev/null +++ b/mod/src/main/java/moe/ymc/acron/mixin/MinecraftServerMixin.java @@ -0,0 +1,33 @@ +package moe.ymc.acron.mixin; + +import moe.ymc.acron.s2c.EventQueue; +import moe.ymc.acron.s2c.event.EventLagging; +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 AC_LOGGER = LogManager.getLogger(); + + @Redirect(method = "runServer()V", + remap = false, + 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) { + AC_LOGGER.debug("Lag: {}ms, {} ticks", + o1, + o2); + EventQueue.enqueue(new EventLagging((long) o1, (long) o2)); + } + } +} diff --git a/mod/src/main/java/moe/ymc/acron/mixin/ServerLoginNetworkHandlerMixin.java b/mod/src/main/java/moe/ymc/acron/mixin/ServerLoginNetworkHandlerMixin.java new file mode 100644 index 0000000..94a48b5 --- /dev/null +++ b/mod/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.EventQueue; +import moe.ymc.acron.s2c.event.EventDisconnected; +import moe.ymc.acron.s2c.event.EventPlayerJoined; +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 AC_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) { + AC_LOGGER.debug("addToServer: {}", + entity.getUuid()); + EventQueue.enqueue(new EventPlayerJoined(new Entity(entity))); + } +} diff --git a/mod/src/main/java/moe/ymc/acron/mixin/ServerNetworkIoMixin.java b/mod/src/main/java/moe/ymc/acron/mixin/ServerNetworkIoMixin.java new file mode 100644 index 0000000..f49914e --- /dev/null +++ b/mod/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 AC_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) { + AC_LOGGER.debug("Adding Acron channel."); + Lazy group; + Class channel; + if (Epoll.isAvailable() && Config.getGlobalConfig().useNativeTransport()) { + channel = EpollServerSocketChannel.class; + group = EPOLL_CHANNEL; + AC_LOGGER.info("Using native transport."); + } else { + channel = NioServerSocketChannel.class; + group = DEFAULT_CHANNEL; + AC_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/mod/src/main/java/moe/ymc/acron/mixin/ServerPlayNetworkHandlerMixin.java b/mod/src/main/java/moe/ymc/acron/mixin/ServerPlayNetworkHandlerMixin.java new file mode 100644 index 0000000..58bef78 --- /dev/null +++ b/mod/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.EventQueue; +import moe.ymc.acron.s2c.event.EventDisconnected; +import moe.ymc.acron.s2c.event.EventPlayerMessage; +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 AC_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/mod/src/main/java/moe/ymc/acron/mixin/ServerPlayerEntityMixin.java b/mod/src/main/java/moe/ymc/acron/mixin/ServerPlayerEntityMixin.java new file mode 100644 index 0000000..4c6758b --- /dev/null +++ b/mod/src/main/java/moe/ymc/acron/mixin/ServerPlayerEntityMixin.java @@ -0,0 +1,32 @@ +package moe.ymc.acron.mixin; + +import moe.ymc.acron.s2c.EventQueue; +import moe.ymc.acron.s2c.event.EventEntityDeath; +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 AC_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) { + AC_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/mod/src/main/java/moe/ymc/acron/net/AcronInitializer.java b/mod/src/main/java/moe/ymc/acron/net/AcronInitializer.java new file mode 100644 index 0000000..c9953e3 --- /dev/null +++ b/mod/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/mod/src/main/java/moe/ymc/acron/net/Attributes.java b/mod/src/main/java/moe/ymc/acron/net/Attributes.java new file mode 100644 index 0000000..ddb0f5c --- /dev/null +++ b/mod/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/mod/src/main/java/moe/ymc/acron/net/AuthHandler.java b/mod/src/main/java/moe/ymc/acron/net/AuthHandler.java new file mode 100644 index 0000000..3e42e14 --- /dev/null +++ b/mod/src/main/java/moe/ymc/acron/net/AuthHandler.java @@ -0,0 +1,98 @@ +package moe.ymc.acron.net; + +import io.netty.channel.ChannelFutureListener; +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)) + .addListener(ChannelFutureListener.CLOSE); + 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)) + .addListener(ChannelFutureListener.CLOSE); + 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)) + .addListener(ChannelFutureListener.CLOSE); + 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)) + .addListener(ChannelFutureListener.CLOSE); + return; + } + } catch (NumberFormatException ignored) { + ctx.writeAndFlush(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, + HttpResponseStatus.BAD_REQUEST)) + .addListener(ChannelFutureListener.CLOSE); + 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)) + .addListener(ChannelFutureListener.CLOSE); + 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/mod/src/main/java/moe/ymc/acron/net/ClientConfiguration.java b/mod/src/main/java/moe/ymc/acron/net/ClientConfiguration.java new file mode 100644 index 0000000..450ccd4 --- /dev/null +++ b/mod/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/mod/src/main/java/moe/ymc/acron/net/ClientIdentification.java b/mod/src/main/java/moe/ymc/acron/net/ClientIdentification.java new file mode 100644 index 0000000..1cb4375 --- /dev/null +++ b/mod/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/mod/src/main/java/moe/ymc/acron/net/HandshakeComplete.java b/mod/src/main/java/moe/ymc/acron/net/HandshakeComplete.java new file mode 100644 index 0000000..348b5e2 --- /dev/null +++ b/mod/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/mod/src/main/java/moe/ymc/acron/net/WSFrameHandler.java b/mod/src/main/java/moe/ymc/acron/net/WSFrameHandler.java new file mode 100644 index 0000000..912e73a --- /dev/null +++ b/mod/src/main/java/moe/ymc/acron/net/WSFrameHandler.java @@ -0,0 +1,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 { + 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); + } + } +} diff --git a/mod/src/main/java/moe/ymc/acron/s2c/Entity.java b/mod/src/main/java/moe/ymc/acron/s2c/Entity.java new file mode 100644 index 0000000..3e0add1 --- /dev/null +++ b/mod/src/main/java/moe/ymc/acron/s2c/Entity.java @@ -0,0 +1,26 @@ +package moe.ymc.acron.s2c; + +import com.google.gson.annotations.SerializedName; +import com.mojang.authlib.GameProfile; +import moe.ymc.acron.common.Vec3d; +import moe.ymc.acron.common.WorldKey; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; + +public record Entity(@SerializedName("name") @NotNull String name, + @SerializedName("uuid") @NotNull UUID uuid, + @SerializedName("pos") @Nullable Vec3d pos, + @SerializedName("world") @Nullable WorldKey world) { + public Entity(@NotNull net.minecraft.entity.Entity entity) { + this(entity.getName().getString(), + entity.getUuid(), + new Vec3d(entity.getPos()), + WorldKey.create(entity.world.getRegistryKey().getValue())); + } + + public Entity(@NotNull GameProfile profile) { + this(profile.getName(), profile.getId(), null, null); + } +} diff --git a/mod/src/main/java/moe/ymc/acron/s2c/Event.java b/mod/src/main/java/moe/ymc/acron/s2c/Event.java new file mode 100644 index 0000000..1abc35c --- /dev/null +++ b/mod/src/main/java/moe/ymc/acron/s2c/Event.java @@ -0,0 +1,4 @@ +package moe.ymc.acron.s2c; + +public interface Event { +} diff --git a/mod/src/main/java/moe/ymc/acron/s2c/EventQueue.java b/mod/src/main/java/moe/ymc/acron/s2c/EventQueue.java new file mode 100644 index 0000000..8c470a1 --- /dev/null +++ b/mod/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/mod/src/main/java/moe/ymc/acron/s2c/event/EventDisconnected.java b/mod/src/main/java/moe/ymc/acron/s2c/event/EventDisconnected.java new file mode 100644 index 0000000..610fc58 --- /dev/null +++ b/mod/src/main/java/moe/ymc/acron/s2c/event/EventDisconnected.java @@ -0,0 +1,12 @@ +package moe.ymc.acron.s2c.event; + +import com.google.gson.annotations.SerializedName; +import moe.ymc.acron.s2c.Entity; +import moe.ymc.acron.s2c.Event; +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/mod/src/main/java/moe/ymc/acron/s2c/event/EventEntityDeath.java b/mod/src/main/java/moe/ymc/acron/s2c/event/EventEntityDeath.java new file mode 100644 index 0000000..4735241 --- /dev/null +++ b/mod/src/main/java/moe/ymc/acron/s2c/event/EventEntityDeath.java @@ -0,0 +1,12 @@ +package moe.ymc.acron.s2c.event; + +import com.google.gson.annotations.SerializedName; +import moe.ymc.acron.s2c.Entity; +import moe.ymc.acron.s2c.Event; +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/mod/src/main/java/moe/ymc/acron/s2c/event/EventLagging.java b/mod/src/main/java/moe/ymc/acron/s2c/event/EventLagging.java new file mode 100644 index 0000000..30974df --- /dev/null +++ b/mod/src/main/java/moe/ymc/acron/s2c/event/EventLagging.java @@ -0,0 +1,9 @@ +package moe.ymc.acron.s2c.event; + +import com.google.gson.annotations.SerializedName; +import moe.ymc.acron.s2c.Event; + +public record EventLagging(@SerializedName("ms") long ms, + @SerializedName("ticks") long ticks) + implements Event { +} diff --git a/mod/src/main/java/moe/ymc/acron/s2c/event/EventPlayerJoined.java b/mod/src/main/java/moe/ymc/acron/s2c/event/EventPlayerJoined.java new file mode 100644 index 0000000..408680b --- /dev/null +++ b/mod/src/main/java/moe/ymc/acron/s2c/event/EventPlayerJoined.java @@ -0,0 +1,10 @@ +package moe.ymc.acron.s2c.event; + +import com.google.gson.annotations.SerializedName; +import moe.ymc.acron.s2c.Entity; +import moe.ymc.acron.s2c.Event; +import org.jetbrains.annotations.NotNull; + +public record EventPlayerJoined(@SerializedName("player") @NotNull Entity player) + implements Event { +} diff --git a/mod/src/main/java/moe/ymc/acron/s2c/event/EventPlayerMessage.java b/mod/src/main/java/moe/ymc/acron/s2c/event/EventPlayerMessage.java new file mode 100644 index 0000000..2769493 --- /dev/null +++ b/mod/src/main/java/moe/ymc/acron/s2c/event/EventPlayerMessage.java @@ -0,0 +1,12 @@ +package moe.ymc.acron.s2c.event; + +import com.google.gson.annotations.SerializedName; +import moe.ymc.acron.s2c.Entity; +import moe.ymc.acron.s2c.Event; +import org.jetbrains.annotations.NotNull; + +public record EventPlayerMessage(@SerializedName("player") @NotNull Entity player, + @SerializedName("text") @NotNull String text) + implements Event { + +} diff --git a/mod/src/main/java/moe/ymc/acron/s2c/response/EventCmdOut.java b/mod/src/main/java/moe/ymc/acron/s2c/response/EventCmdOut.java new file mode 100644 index 0000000..a4cb798 --- /dev/null +++ b/mod/src/main/java/moe/ymc/acron/s2c/response/EventCmdOut.java @@ -0,0 +1,13 @@ +package moe.ymc.acron.s2c.response; + +import com.google.gson.annotations.SerializedName; +import moe.ymc.acron.s2c.Event; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +public record EventCmdOut(@SerializedName("id") int id, + @SerializedName("sender") @NotNull UUID sender, + @SerializedName("out") @NotNull String out) + implements Event { +} diff --git a/mod/src/main/java/moe/ymc/acron/s2c/response/EventCmdRes.java b/mod/src/main/java/moe/ymc/acron/s2c/response/EventCmdRes.java new file mode 100644 index 0000000..8c1b6a9 --- /dev/null +++ b/mod/src/main/java/moe/ymc/acron/s2c/response/EventCmdRes.java @@ -0,0 +1,10 @@ +package moe.ymc.acron.s2c.response; + +import com.google.gson.annotations.SerializedName; +import moe.ymc.acron.s2c.Event; + +public record EventCmdRes(@SerializedName("id") int id, + @SerializedName("success") boolean success, + @SerializedName("result") int result) + implements Event { +} diff --git a/mod/src/main/java/moe/ymc/acron/s2c/response/EventError.java b/mod/src/main/java/moe/ymc/acron/s2c/response/EventError.java new file mode 100644 index 0000000..370e8f3 --- /dev/null +++ b/mod/src/main/java/moe/ymc/acron/s2c/response/EventError.java @@ -0,0 +1,22 @@ +package moe.ymc.acron.s2c.response; + +import com.google.gson.annotations.SerializedName; +import moe.ymc.acron.s2c.Event; +import org.jetbrains.annotations.Nullable; + +public record EventError(@SerializedName("id") int id, + @SerializedName("code") int code, + @SerializedName("message") @Nullable String message) + implements Event { + public enum Code { + SERVER_ERROR(500), + BAD_REQUEST(400), + FORBIDDEN(403) + ; + + public final int value; + Code(int value) { + this.value = value; + } + } +} diff --git a/mod/src/main/java/moe/ymc/acron/s2c/response/EventOk.java b/mod/src/main/java/moe/ymc/acron/s2c/response/EventOk.java new file mode 100644 index 0000000..eb8c82d --- /dev/null +++ b/mod/src/main/java/moe/ymc/acron/s2c/response/EventOk.java @@ -0,0 +1,8 @@ +package moe.ymc.acron.s2c.response; + +import com.google.gson.annotations.SerializedName; +import moe.ymc.acron.s2c.Event; + +public record EventOk(@SerializedName("id") int id) + implements Event { +} diff --git a/mod/src/main/java/moe/ymc/acron/serialization/Serializer.java b/mod/src/main/java/moe/ymc/acron/serialization/Serializer.java new file mode 100644 index 0000000..8091c25 --- /dev/null +++ b/mod/src/main/java/moe/ymc/acron/serialization/Serializer.java @@ -0,0 +1,60 @@ +package moe.ymc.acron.serialization; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParseException; +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.common.Vec2f; +import moe.ymc.acron.common.Vec3d; +import moe.ymc.acron.s2c.Event; +import moe.ymc.acron.s2c.event.*; +import moe.ymc.acron.s2c.response.EventCmdOut; +import moe.ymc.acron.s2c.response.EventCmdRes; +import moe.ymc.acron.s2c.response.EventError; +import moe.ymc.acron.s2c.response.EventOk; +import org.jetbrains.annotations.NotNull; + +public class Serializer { + @NotNull + public static Request deserialize(@NotNull TextWebSocketFrame frame) + throws JsonParseException, IllegalArgumentException, IllegalStateException { + 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(Vec3d.class, new Vec3d.Vec3dDeserializer()) + .registerTypeAdapter(Vec2f.class, new 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_out") + .registerSubtype(EventCmdRes.class, "cmd_result") + .registerSubtype(EventLagging.class, "lagging") + .registerSubtype(EventError.class, "error") + .registerSubtype(EventOk.class, "ok"); + final Gson gson = new GsonBuilder() + .registerTypeAdapterFactory(adapter) + .create(); + return new TextWebSocketFrame(gson.toJson(message, message.getClass())); + } +} diff --git a/mod/src/main/resources/acron.mixins.json b/mod/src/main/resources/acron.mixins.json new file mode 100644 index 0000000..5d0911f --- /dev/null +++ b/mod/src/main/resources/acron.mixins.json @@ -0,0 +1,21 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "moe.ymc.acron.mixin", + "compatibilityLevel": "JAVA_8", + "mixins": [ + "CommandManagerMixin", + "LivingEntityMixin", + "MinecraftDedicatedServerMixin", + "MinecraftServerMixin", + "ServerLoginNetworkHandlerMixin", + "ServerNetworkIoMixin", + "ServerPlayerEntityMixin", + "ServerPlayNetworkHandlerMixin" + ], + "client": [ + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/mod/src/main/resources/fabric.mod.json b/mod/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..999aeeb --- /dev/null +++ b/mod/src/main/resources/fabric.mod.json @@ -0,0 +1,24 @@ +{ + "schemaVersion": 1, + "id": "acron", + "version": "${version}", + "name": "Acron", + "description": "WebSocket based remote server management", + "authors": ["YuutaW"], + "contact": {}, + "license": "GPL-2.0", + "icon": "assets/acron/icon.png", + "environment": "server", + "entrypoints": { + "main": [ + "moe.ymc.acron.Acron" + ] + }, + "mixins": [ + "acron.mixins.json" + ], + "depends": { + "fabricloader": ">=0.14.4", + "minecraft": "1.17.1" + } +} diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index f91a4fe..0000000 --- a/settings.gradle +++ /dev/null @@ -1,9 +0,0 @@ -pluginManagement { - repositories { - maven { - name = 'Fabric' - url = 'https://maven.fabricmc.net/' - } - gradlePluginPortal() - } -} diff --git a/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java b/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java deleted file mode 100644 index 51f05ff..0000000 --- a/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java +++ /dev/null @@ -1,272 +0,0 @@ -/* - * Copyright (C) 2011 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.gson.typeadapters; - -import com.google.gson.*; -import com.google.gson.reflect.TypeToken; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonWriter; - -import java.io.IOException; -import java.util.LinkedHashMap; -import java.util.Map; - -/** - * Adapts values whose runtime type may differ from their declaration type. This - * is necessary when a field's type is not the same type that GSON should create - * when deserializing that field. For example, consider these types: - *
   {@code
- *   abstract class Shape {
- *     int x;
- *     int y;
- *   }
- *   class Circle extends Shape {
- *     int radius;
- *   }
- *   class Rectangle extends Shape {
- *     int width;
- *     int height;
- *   }
- *   class Diamond extends Shape {
- *     int width;
- *     int height;
- *   }
- *   class Drawing {
- *     Shape bottomShape;
- *     Shape topShape;
- *   }
- * }
- *

Without additional type information, the serialized JSON is ambiguous. Is - * the bottom shape in this drawing a rectangle or a diamond?

   {@code
- *   {
- *     "bottomShape": {
- *       "width": 10,
- *       "height": 5,
- *       "x": 0,
- *       "y": 0
- *     },
- *     "topShape": {
- *       "radius": 2,
- *       "x": 4,
- *       "y": 1
- *     }
- *   }}
- * This class addresses this problem by adding type information to the - * serialized JSON and honoring that type information when the JSON is - * deserialized:
   {@code
- *   {
- *     "bottomShape": {
- *       "type": "Diamond",
- *       "width": 10,
- *       "height": 5,
- *       "x": 0,
- *       "y": 0
- *     },
- *     "topShape": {
- *       "type": "Circle",
- *       "radius": 2,
- *       "x": 4,
- *       "y": 1
- *     }
- *   }}
- * Both the type field name ({@code "type"}) and the type labels ({@code - * "Rectangle"}) are configurable. - * - *

Registering Types

- * Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field - * name to the {@link #of} factory method. If you don't supply an explicit type - * field name, {@code "type"} will be used.
   {@code
- *   RuntimeTypeAdapterFactory shapeAdapterFactory
- *       = RuntimeTypeAdapterFactory.of(Shape.class, "type");
- * }
- * Next register all of your subtypes. Every subtype must be explicitly - * registered. This protects your application from injection attacks. If you - * don't supply an explicit type label, the type's simple name will be used. - *
   {@code
- *   shapeAdapterFactory.registerSubtype(Rectangle.class, "Rectangle");
- *   shapeAdapterFactory.registerSubtype(Circle.class, "Circle");
- *   shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond");
- * }
- * Finally, register the type adapter factory in your application's GSON builder: - *
   {@code
- *   Gson gson = new GsonBuilder()
- *       .registerTypeAdapterFactory(shapeAdapterFactory)
- *       .create();
- * }
- * Like {@code GsonBuilder}, this API supports chaining:
   {@code
- *   RuntimeTypeAdapterFactory shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
- *       .registerSubtype(Rectangle.class)
- *       .registerSubtype(Circle.class)
- *       .registerSubtype(Diamond.class);
- * }
- * - *

Serialization and deserialization

- * In order to serialize and deserialize a polymorphic object, - * you must specify the base type explicitly. - *
   {@code
- *   Diamond diamond = new Diamond();
- *   String json = gson.toJson(diamond, Shape.class);
- * }
- * And then: - *
   {@code
- *   Shape shape = gson.fromJson(json, Shape.class);
- * }
- */ -public final class RuntimeTypeAdapterFactory implements TypeAdapterFactory { - private final Class baseType; - private final String typeFieldName; - private final Map> labelToSubtype = new LinkedHashMap<>(); - private final Map, String> subtypeToLabel = new LinkedHashMap<>(); - private final boolean maintainType; - - private RuntimeTypeAdapterFactory(Class baseType, String typeFieldName, boolean maintainType) { - if (typeFieldName == null || baseType == null) { - throw new NullPointerException(); - } - this.baseType = baseType; - this.typeFieldName = typeFieldName; - this.maintainType = maintainType; - } - - /** - * Creates a new runtime type adapter using for {@code baseType} using {@code - * typeFieldName} as the type field name. Type field names are case sensitive. - * {@code maintainType} flag decide if the type will be stored in pojo or not. - */ - public static RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName, boolean maintainType) { - return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, maintainType); - } - - /** - * Creates a new runtime type adapter using for {@code baseType} using {@code - * typeFieldName} as the type field name. Type field names are case sensitive. - */ - public static RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName) { - return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, false); - } - - /** - * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as - * the type field name. - */ - public static RuntimeTypeAdapterFactory of(Class baseType) { - return new RuntimeTypeAdapterFactory<>(baseType, "type", false); - } - - /** - * Registers {@code type} identified by {@code label}. Labels are case - * sensitive. - * - * @throws IllegalArgumentException if either {@code type} or {@code label} - * have already been registered on this type adapter. - */ - public RuntimeTypeAdapterFactory registerSubtype(Class type, String label) { - if (type == null || label == null) { - throw new NullPointerException(); - } - if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) { - throw new IllegalArgumentException("types and labels must be unique"); - } - labelToSubtype.put(label, type); - subtypeToLabel.put(type, label); - return this; - } - - /** - * Registers {@code type} identified by its {@link Class#getSimpleName simple - * name}. Labels are case sensitive. - * - * @throws IllegalArgumentException if either {@code type} or its simple name - * have already been registered on this type adapter. - */ - public RuntimeTypeAdapterFactory registerSubtype(Class type) { - return registerSubtype(type, type.getSimpleName()); - } - - @Override - public TypeAdapter create(Gson gson, TypeToken type) { - // Workaround found at https://github.com/google/gson/issues/712#issuecomment-148955110 - if (null == type || !baseType.isAssignableFrom(type.getRawType())) { - // if (type.getRawType() != baseType) { - return null; - } - - final TypeAdapter jsonElementAdapter = gson.getAdapter(JsonElement.class); - final Map> labelToDelegate = new LinkedHashMap<>(); - final Map, TypeAdapter> subtypeToDelegate = new LinkedHashMap<>(); - for (Map.Entry> entry : labelToSubtype.entrySet()) { - TypeAdapter delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue())); - labelToDelegate.put(entry.getKey(), delegate); - subtypeToDelegate.put(entry.getValue(), delegate); - } - - return new TypeAdapter() { - @Override public R read(JsonReader in) throws IOException { - JsonElement jsonElement = jsonElementAdapter.read(in); - JsonElement labelJsonElement; - if (maintainType) { - labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName); - } else { - labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName); - } - - if (labelJsonElement == null) { - throw new JsonParseException("cannot deserialize " + baseType - + " because it does not define a field named " + typeFieldName); - } - String label = labelJsonElement.getAsString(); - @SuppressWarnings("unchecked") // registration requires that subtype extends T - TypeAdapter delegate = (TypeAdapter) labelToDelegate.get(label); - if (delegate == null) { - throw new JsonParseException("cannot deserialize " + baseType + " subtype named " - + label + "; did you forget to register a subtype?"); - } - return delegate.fromJsonTree(jsonElement); - } - - @Override public void write(JsonWriter out, R value) throws IOException { - Class srcType = value.getClass(); - String label = subtypeToLabel.get(srcType); - @SuppressWarnings("unchecked") // registration requires that subtype extends T - TypeAdapter delegate = (TypeAdapter) subtypeToDelegate.get(srcType); - if (delegate == null) { - throw new JsonParseException("cannot serialize " + srcType.getName() - + "; did you forget to register a subtype?"); - } - JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject(); - - if (maintainType) { - jsonElementAdapter.write(out, jsonObject); - return; - } - - JsonObject clone = new JsonObject(); - - if (jsonObject.has(typeFieldName)) { - throw new JsonParseException("cannot serialize " + srcType.getName() - + " because it already defines a field named " + typeFieldName); - } - clone.add(typeFieldName, new JsonPrimitive(label)); - - for (Map.Entry e : jsonObject.entrySet()) { - clone.add(e.getKey(), e.getValue()); - } - jsonElementAdapter.write(out, clone); - } - }.nullSafe(); - } -} diff --git a/src/main/java/moe/ymc/acron/Acron.java b/src/main/java/moe/ymc/acron/Acron.java deleted file mode 100644 index d6f6214..0000000 --- a/src/main/java/moe/ymc/acron/Acron.java +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index f522884..0000000 --- a/src/main/java/moe/ymc/acron/MinecraftServerHolder.java +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index 17d29a3..0000000 --- a/src/main/java/moe/ymc/acron/auth/Action.java +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 2124ad4..0000000 --- a/src/main/java/moe/ymc/acron/auth/Client.java +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index 2ab7b97..0000000 --- a/src/main/java/moe/ymc/acron/auth/PolicyChecker.java +++ /dev/null @@ -1,42 +0,0 @@ -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) { - final String commandToMatch = command.startsWith("/") ? - command.substring(1) : - command; - for (int i = 0; i < client.rules().length; i++) { - final Rule rule = client.rules()[i]; - if (rule.cmdPattern().matcher(commandToMatch).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 deleted file mode 100644 index 55ad0d7..0000000 --- a/src/main/java/moe/ymc/acron/auth/Rule.java +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 6f34b07..0000000 --- a/src/main/java/moe/ymc/acron/c2s/ReqCmd.java +++ /dev/null @@ -1,51 +0,0 @@ -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() : - -1; - 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 deleted file mode 100644 index fcddf35..0000000 --- a/src/main/java/moe/ymc/acron/c2s/ReqSetConfig.java +++ /dev/null @@ -1,63 +0,0 @@ -package moe.ymc.acron.c2s; - -import com.google.gson.*; -import com.google.gson.annotations.SerializedName; -import moe.ymc.acron.common.Vec2f; -import moe.ymc.acron.common.Vec3d; -import moe.ymc.acron.common.WorldKey; -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 id; - } - - 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; - if (object.has("world")) { - world = context.deserialize(object.get("world"), WorldKey.class); - // https://stackoverflow.com/a/49574019 - if (world == null) { - throw new JsonParseException("Invalid world"); - } - } else { - world = 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(id, - world, - pos, - rot, - name); - } - } - -} \ No newline at end of file diff --git a/src/main/java/moe/ymc/acron/c2s/Request.java b/src/main/java/moe/ymc/acron/c2s/Request.java deleted file mode 100644 index af81705..0000000 --- a/src/main/java/moe/ymc/acron/c2s/Request.java +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index 55eadf1..0000000 --- a/src/main/java/moe/ymc/acron/cmd/CmdOut.java +++ /dev/null @@ -1,53 +0,0 @@ -package moe.ymc.acron.cmd; - -import io.netty.channel.Channel; -import moe.ymc.acron.s2c.response.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 deleted file mode 100644 index 3c49143..0000000 --- a/src/main/java/moe/ymc/acron/cmd/CmdQueue.java +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index d22b77e..0000000 --- a/src/main/java/moe/ymc/acron/cmd/CmdResConsumer.java +++ /dev/null @@ -1,33 +0,0 @@ -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.response.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 deleted file mode 100644 index 983b4ed..0000000 --- a/src/main/java/moe/ymc/acron/cmd/CmdSrc.java +++ /dev/null @@ -1,34 +0,0 @@ -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/common/Vec2f.java b/src/main/java/moe/ymc/acron/common/Vec2f.java deleted file mode 100644 index 5ab3dfd..0000000 --- a/src/main/java/moe/ymc/acron/common/Vec2f.java +++ /dev/null @@ -1,25 +0,0 @@ -package moe.ymc.acron.common; - -import com.google.gson.*; -import com.google.gson.annotations.SerializedName; - -import java.lang.reflect.Type; - -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); - } - } -} \ No newline at end of file diff --git a/src/main/java/moe/ymc/acron/common/Vec3d.java b/src/main/java/moe/ymc/acron/common/Vec3d.java deleted file mode 100644 index 593019f..0000000 --- a/src/main/java/moe/ymc/acron/common/Vec3d.java +++ /dev/null @@ -1,34 +0,0 @@ -package moe.ymc.acron.common; - -import com.google.gson.*; -import com.google.gson.annotations.SerializedName; -import org.jetbrains.annotations.NotNull; - -import java.lang.reflect.Type; - -public record Vec3d(@SerializedName("x") double x, - @SerializedName("y") double y, - @SerializedName("z") double z) { - public Vec3d(@NotNull net.minecraft.util.math.Vec3d vec3d) { - this(vec3d.x, vec3d.y, vec3d.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); - } - } -} \ No newline at end of file diff --git a/src/main/java/moe/ymc/acron/common/WorldKey.java b/src/main/java/moe/ymc/acron/common/WorldKey.java deleted file mode 100644 index fa10d54..0000000 --- a/src/main/java/moe/ymc/acron/common/WorldKey.java +++ /dev/null @@ -1,35 +0,0 @@ -package moe.ymc.acron.common; - -import com.google.gson.annotations.SerializedName; -import net.minecraft.util.Identifier; -import net.minecraft.world.dimension.DimensionType; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public enum WorldKey { - @SerializedName("overworld") - OVERWORLD, - @SerializedName("nether") - NETHER, - @SerializedName("end") - END; - - private static final Logger LOGGER = LogManager.getLogger(); - - public static @Nullable WorldKey create(@NotNull Identifier identifier) { - if (identifier.equals(DimensionType.OVERWORLD_ID)) { - return OVERWORLD; - } else if (identifier.equals(DimensionType.THE_NETHER_ID)) { - return NETHER; - } else if (identifier.equals(DimensionType.THE_END_ID)) { - return END; - } else { - LOGGER.warn("Unknown world {}:{}. Returning NULL to the client.", - identifier.getNamespace(), - identifier.getPath()); - return null; - } - } -} \ No newline at end of file diff --git a/src/main/java/moe/ymc/acron/config/Config.java b/src/main/java/moe/ymc/acron/config/Config.java deleted file mode 100644 index 3749c25..0000000 --- a/src/main/java/moe/ymc/acron/config/Config.java +++ /dev/null @@ -1,30 +0,0 @@ -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 deleted file mode 100644 index 2774c4d..0000000 --- a/src/main/java/moe/ymc/acron/config/ConfigReloadCmd.java +++ /dev/null @@ -1,41 +0,0 @@ -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 deleted file mode 100644 index 4d31308..0000000 --- a/src/main/java/moe/ymc/acron/config/json/Client.java +++ /dev/null @@ -1,45 +0,0 @@ -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 deleted file mode 100644 index e8c5a83..0000000 --- a/src/main/java/moe/ymc/acron/config/json/Config.java +++ /dev/null @@ -1,103 +0,0 @@ -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 deleted file mode 100644 index baf5b35..0000000 --- a/src/main/java/moe/ymc/acron/config/json/ConfigDeserializationException.java +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index e91b355..0000000 --- a/src/main/java/moe/ymc/acron/config/json/ConfigDeserializer.java +++ /dev/null @@ -1,27 +0,0 @@ -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 deleted file mode 100644 index 0efd9a9..0000000 --- a/src/main/java/moe/ymc/acron/config/json/ConfigJsonObject.java +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index 114e17d..0000000 --- a/src/main/java/moe/ymc/acron/config/json/Rule.java +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index 29b83dc..0000000 --- a/src/main/java/moe/ymc/acron/jvav/Pair.java +++ /dev/null @@ -1,4 +0,0 @@ -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 deleted file mode 100644 index 9aaed2e..0000000 --- a/src/main/java/moe/ymc/acron/mixin/CommandManagerMixin.java +++ /dev/null @@ -1,38 +0,0 @@ -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 AC_LOGGER = LogManager.getLogger(); - - @Shadow - @Final - private CommandDispatcher dispatcher; - - @Inject(method = "", at = @At("RETURN")) - private void onRegister(CommandManager.RegistrationEnvironment arg, CallbackInfo ci) { - AC_LOGGER.debug("onRegister"); - dispatcher.register( - literal("acron").requires(player -> player.hasPermissionLevel(4)).then( - literal("rule").then( - literal("update") - .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 deleted file mode 100644 index 9e16569..0000000 --- a/src/main/java/moe/ymc/acron/mixin/LivingEntityMixin.java +++ /dev/null @@ -1,41 +0,0 @@ -package moe.ymc.acron.mixin; - -import moe.ymc.acron.s2c.Entity; -import moe.ymc.acron.s2c.EventQueue; -import moe.ymc.acron.s2c.event.EventEntityDeath; -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 AC_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) { - AC_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 deleted file mode 100644 index 32d2fbf..0000000 --- a/src/main/java/moe/ymc/acron/mixin/MinecraftDedicatedServerMixin.java +++ /dev/null @@ -1,21 +0,0 @@ -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 AC_LOGGER = LogManager.getLogger(); - - @Inject(at = @At("RETURN"), method = "") - private void init(CallbackInfo info) { - AC_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 deleted file mode 100644 index cb813b4..0000000 --- a/src/main/java/moe/ymc/acron/mixin/MinecraftServerMixin.java +++ /dev/null @@ -1,33 +0,0 @@ -package moe.ymc.acron.mixin; - -import moe.ymc.acron.s2c.EventQueue; -import moe.ymc.acron.s2c.event.EventLagging; -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 AC_LOGGER = LogManager.getLogger(); - - @Redirect(method = "runServer()V", - remap = false, - 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) { - AC_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 deleted file mode 100644 index 94a48b5..0000000 --- a/src/main/java/moe/ymc/acron/mixin/ServerLoginNetworkHandlerMixin.java +++ /dev/null @@ -1,47 +0,0 @@ -package moe.ymc.acron.mixin; - -import com.mojang.authlib.GameProfile; -import moe.ymc.acron.s2c.Entity; -import moe.ymc.acron.s2c.EventQueue; -import moe.ymc.acron.s2c.event.EventDisconnected; -import moe.ymc.acron.s2c.event.EventPlayerJoined; -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 AC_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) { - AC_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 deleted file mode 100644 index f49914e..0000000 --- a/src/main/java/moe/ymc/acron/mixin/ServerNetworkIoMixin.java +++ /dev/null @@ -1,65 +0,0 @@ -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 AC_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) { - AC_LOGGER.debug("Adding Acron channel."); - Lazy group; - Class channel; - if (Epoll.isAvailable() && Config.getGlobalConfig().useNativeTransport()) { - channel = EpollServerSocketChannel.class; - group = EPOLL_CHANNEL; - AC_LOGGER.info("Using native transport."); - } else { - channel = NioServerSocketChannel.class; - group = DEFAULT_CHANNEL; - AC_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 deleted file mode 100644 index 58bef78..0000000 --- a/src/main/java/moe/ymc/acron/mixin/ServerPlayNetworkHandlerMixin.java +++ /dev/null @@ -1,43 +0,0 @@ -package moe.ymc.acron.mixin; - -import moe.ymc.acron.s2c.Entity; -import moe.ymc.acron.s2c.EventQueue; -import moe.ymc.acron.s2c.event.EventDisconnected; -import moe.ymc.acron.s2c.event.EventPlayerMessage; -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 AC_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 deleted file mode 100644 index 4c6758b..0000000 --- a/src/main/java/moe/ymc/acron/mixin/ServerPlayerEntityMixin.java +++ /dev/null @@ -1,32 +0,0 @@ -package moe.ymc.acron.mixin; - -import moe.ymc.acron.s2c.EventQueue; -import moe.ymc.acron.s2c.event.EventEntityDeath; -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 AC_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) { - AC_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 deleted file mode 100644 index c9953e3..0000000 --- a/src/main/java/moe/ymc/acron/net/AcronInitializer.java +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index ddb0f5c..0000000 --- a/src/main/java/moe/ymc/acron/net/Attributes.java +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index 3e42e14..0000000 --- a/src/main/java/moe/ymc/acron/net/AuthHandler.java +++ /dev/null @@ -1,98 +0,0 @@ -package moe.ymc.acron.net; - -import io.netty.channel.ChannelFutureListener; -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)) - .addListener(ChannelFutureListener.CLOSE); - 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)) - .addListener(ChannelFutureListener.CLOSE); - 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)) - .addListener(ChannelFutureListener.CLOSE); - 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)) - .addListener(ChannelFutureListener.CLOSE); - return; - } - } catch (NumberFormatException ignored) { - ctx.writeAndFlush(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, - HttpResponseStatus.BAD_REQUEST)) - .addListener(ChannelFutureListener.CLOSE); - 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)) - .addListener(ChannelFutureListener.CLOSE); - 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 deleted file mode 100644 index 450ccd4..0000000 --- a/src/main/java/moe/ymc/acron/net/ClientConfiguration.java +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index 1cb4375..0000000 --- a/src/main/java/moe/ymc/acron/net/ClientIdentification.java +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 348b5e2..0000000 --- a/src/main/java/moe/ymc/acron/net/HandshakeComplete.java +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index 912e73a..0000000 --- a/src/main/java/moe/ymc/acron/net/WSFrameHandler.java +++ /dev/null @@ -1,174 +0,0 @@ -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); - } - } -} diff --git a/src/main/java/moe/ymc/acron/s2c/Entity.java b/src/main/java/moe/ymc/acron/s2c/Entity.java deleted file mode 100644 index 3e0add1..0000000 --- a/src/main/java/moe/ymc/acron/s2c/Entity.java +++ /dev/null @@ -1,26 +0,0 @@ -package moe.ymc.acron.s2c; - -import com.google.gson.annotations.SerializedName; -import com.mojang.authlib.GameProfile; -import moe.ymc.acron.common.Vec3d; -import moe.ymc.acron.common.WorldKey; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.UUID; - -public record Entity(@SerializedName("name") @NotNull String name, - @SerializedName("uuid") @NotNull UUID uuid, - @SerializedName("pos") @Nullable Vec3d pos, - @SerializedName("world") @Nullable WorldKey world) { - public Entity(@NotNull net.minecraft.entity.Entity entity) { - this(entity.getName().getString(), - entity.getUuid(), - new Vec3d(entity.getPos()), - WorldKey.create(entity.world.getRegistryKey().getValue())); - } - - public Entity(@NotNull GameProfile profile) { - this(profile.getName(), profile.getId(), null, null); - } -} diff --git a/src/main/java/moe/ymc/acron/s2c/Event.java b/src/main/java/moe/ymc/acron/s2c/Event.java deleted file mode 100644 index 1abc35c..0000000 --- a/src/main/java/moe/ymc/acron/s2c/Event.java +++ /dev/null @@ -1,4 +0,0 @@ -package moe.ymc.acron.s2c; - -public interface Event { -} diff --git a/src/main/java/moe/ymc/acron/s2c/EventQueue.java b/src/main/java/moe/ymc/acron/s2c/EventQueue.java deleted file mode 100644 index 8c470a1..0000000 --- a/src/main/java/moe/ymc/acron/s2c/EventQueue.java +++ /dev/null @@ -1,28 +0,0 @@ -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/s2c/event/EventDisconnected.java b/src/main/java/moe/ymc/acron/s2c/event/EventDisconnected.java deleted file mode 100644 index 610fc58..0000000 --- a/src/main/java/moe/ymc/acron/s2c/event/EventDisconnected.java +++ /dev/null @@ -1,12 +0,0 @@ -package moe.ymc.acron.s2c.event; - -import com.google.gson.annotations.SerializedName; -import moe.ymc.acron.s2c.Entity; -import moe.ymc.acron.s2c.Event; -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/event/EventEntityDeath.java b/src/main/java/moe/ymc/acron/s2c/event/EventEntityDeath.java deleted file mode 100644 index 4735241..0000000 --- a/src/main/java/moe/ymc/acron/s2c/event/EventEntityDeath.java +++ /dev/null @@ -1,12 +0,0 @@ -package moe.ymc.acron.s2c.event; - -import com.google.gson.annotations.SerializedName; -import moe.ymc.acron.s2c.Entity; -import moe.ymc.acron.s2c.Event; -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/event/EventLagging.java b/src/main/java/moe/ymc/acron/s2c/event/EventLagging.java deleted file mode 100644 index 30974df..0000000 --- a/src/main/java/moe/ymc/acron/s2c/event/EventLagging.java +++ /dev/null @@ -1,9 +0,0 @@ -package moe.ymc.acron.s2c.event; - -import com.google.gson.annotations.SerializedName; -import moe.ymc.acron.s2c.Event; - -public record EventLagging(@SerializedName("ms") long ms, - @SerializedName("ticks") long ticks) - implements Event { -} diff --git a/src/main/java/moe/ymc/acron/s2c/event/EventPlayerJoined.java b/src/main/java/moe/ymc/acron/s2c/event/EventPlayerJoined.java deleted file mode 100644 index 408680b..0000000 --- a/src/main/java/moe/ymc/acron/s2c/event/EventPlayerJoined.java +++ /dev/null @@ -1,10 +0,0 @@ -package moe.ymc.acron.s2c.event; - -import com.google.gson.annotations.SerializedName; -import moe.ymc.acron.s2c.Entity; -import moe.ymc.acron.s2c.Event; -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/event/EventPlayerMessage.java b/src/main/java/moe/ymc/acron/s2c/event/EventPlayerMessage.java deleted file mode 100644 index 2769493..0000000 --- a/src/main/java/moe/ymc/acron/s2c/event/EventPlayerMessage.java +++ /dev/null @@ -1,12 +0,0 @@ -package moe.ymc.acron.s2c.event; - -import com.google.gson.annotations.SerializedName; -import moe.ymc.acron.s2c.Entity; -import moe.ymc.acron.s2c.Event; -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/response/EventCmdOut.java b/src/main/java/moe/ymc/acron/s2c/response/EventCmdOut.java deleted file mode 100644 index a4cb798..0000000 --- a/src/main/java/moe/ymc/acron/s2c/response/EventCmdOut.java +++ /dev/null @@ -1,13 +0,0 @@ -package moe.ymc.acron.s2c.response; - -import com.google.gson.annotations.SerializedName; -import moe.ymc.acron.s2c.Event; -import org.jetbrains.annotations.NotNull; - -import java.util.UUID; - -public record EventCmdOut(@SerializedName("id") int id, - @SerializedName("sender") @NotNull UUID sender, - @SerializedName("out") @NotNull String out) - implements Event { -} diff --git a/src/main/java/moe/ymc/acron/s2c/response/EventCmdRes.java b/src/main/java/moe/ymc/acron/s2c/response/EventCmdRes.java deleted file mode 100644 index 8c1b6a9..0000000 --- a/src/main/java/moe/ymc/acron/s2c/response/EventCmdRes.java +++ /dev/null @@ -1,10 +0,0 @@ -package moe.ymc.acron.s2c.response; - -import com.google.gson.annotations.SerializedName; -import moe.ymc.acron.s2c.Event; - -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/response/EventError.java b/src/main/java/moe/ymc/acron/s2c/response/EventError.java deleted file mode 100644 index 370e8f3..0000000 --- a/src/main/java/moe/ymc/acron/s2c/response/EventError.java +++ /dev/null @@ -1,22 +0,0 @@ -package moe.ymc.acron.s2c.response; - -import com.google.gson.annotations.SerializedName; -import moe.ymc.acron.s2c.Event; -import org.jetbrains.annotations.Nullable; - -public record EventError(@SerializedName("id") int id, - @SerializedName("code") int code, - @SerializedName("message") @Nullable String message) - implements Event { - public enum Code { - SERVER_ERROR(500), - BAD_REQUEST(400), - FORBIDDEN(403) - ; - - public final int value; - Code(int value) { - this.value = value; - } - } -} diff --git a/src/main/java/moe/ymc/acron/s2c/response/EventOk.java b/src/main/java/moe/ymc/acron/s2c/response/EventOk.java deleted file mode 100644 index eb8c82d..0000000 --- a/src/main/java/moe/ymc/acron/s2c/response/EventOk.java +++ /dev/null @@ -1,8 +0,0 @@ -package moe.ymc.acron.s2c.response; - -import com.google.gson.annotations.SerializedName; -import moe.ymc.acron.s2c.Event; - -public record EventOk(@SerializedName("id") int id) - implements Event { -} diff --git a/src/main/java/moe/ymc/acron/serialization/Serializer.java b/src/main/java/moe/ymc/acron/serialization/Serializer.java deleted file mode 100644 index 8091c25..0000000 --- a/src/main/java/moe/ymc/acron/serialization/Serializer.java +++ /dev/null @@ -1,60 +0,0 @@ -package moe.ymc.acron.serialization; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonParseException; -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.common.Vec2f; -import moe.ymc.acron.common.Vec3d; -import moe.ymc.acron.s2c.Event; -import moe.ymc.acron.s2c.event.*; -import moe.ymc.acron.s2c.response.EventCmdOut; -import moe.ymc.acron.s2c.response.EventCmdRes; -import moe.ymc.acron.s2c.response.EventError; -import moe.ymc.acron.s2c.response.EventOk; -import org.jetbrains.annotations.NotNull; - -public class Serializer { - @NotNull - public static Request deserialize(@NotNull TextWebSocketFrame frame) - throws JsonParseException, IllegalArgumentException, IllegalStateException { - 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(Vec3d.class, new Vec3d.Vec3dDeserializer()) - .registerTypeAdapter(Vec2f.class, new 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_out") - .registerSubtype(EventCmdRes.class, "cmd_result") - .registerSubtype(EventLagging.class, "lagging") - .registerSubtype(EventError.class, "error") - .registerSubtype(EventOk.class, "ok"); - final Gson gson = new GsonBuilder() - .registerTypeAdapterFactory(adapter) - .create(); - return new TextWebSocketFrame(gson.toJson(message, message.getClass())); - } -} diff --git a/src/main/resources/acron.mixins.json b/src/main/resources/acron.mixins.json deleted file mode 100644 index 5d0911f..0000000 --- a/src/main/resources/acron.mixins.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "required": true, - "minVersion": "0.8", - "package": "moe.ymc.acron.mixin", - "compatibilityLevel": "JAVA_8", - "mixins": [ - "CommandManagerMixin", - "LivingEntityMixin", - "MinecraftDedicatedServerMixin", - "MinecraftServerMixin", - "ServerLoginNetworkHandlerMixin", - "ServerNetworkIoMixin", - "ServerPlayerEntityMixin", - "ServerPlayNetworkHandlerMixin" - ], - "client": [ - ], - "injectors": { - "defaultRequire": 1 - } -} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json deleted file mode 100644 index 999aeeb..0000000 --- a/src/main/resources/fabric.mod.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "schemaVersion": 1, - "id": "acron", - "version": "${version}", - "name": "Acron", - "description": "WebSocket based remote server management", - "authors": ["YuutaW"], - "contact": {}, - "license": "GPL-2.0", - "icon": "assets/acron/icon.png", - "environment": "server", - "entrypoints": { - "main": [ - "moe.ymc.acron.Acron" - ] - }, - "mixins": [ - "acron.mixins.json" - ], - "depends": { - "fabricloader": ">=0.14.4", - "minecraft": "1.17.1" - } -} -- cgit v1.2.3