26 Commits

Author SHA1 Message Date
03663406de Update API Version to 2 2023-09-03 10:07:51 +08:00
de765c575e Fix console multi-line commands 2023-09-02 20:26:18 +08:00
f8a4e6f205 Fix empty command 2023-09-02 18:43:27 +08:00
6279ed2982 Update version to v1.7.0 2023-09-02 18:39:18 +08:00
1b86226951 Support multi-line commands
Update auto-generate console token
2023-09-02 18:38:59 +08:00
e9e2805738 Merge pull request #36 from JDDKCN/master
Update plugin.json
2023-09-01 13:44:08 +08:00
剧毒的KCN
d5162a6eac Update plugin.json 2023-09-01 13:41:08 +08:00
54e63d300d Fix verify api 2023-06-04 20:32:00 +08:00
a084d39b02 Update version to v1.6.1 2023-06-04 20:05:50 +08:00
6c19c09c00 Fix deserialization formatting error (#185) 2023-06-04 20:05:32 +08:00
c6e0b51ea6 Fix deserialization formatting error 2023-06-03 00:27:34 +08:00
bcb88740f1 Upgrade to 1.6.0 2023-06-03 00:11:12 +08:00
290c4fbf8c DON'T LOCK PLAYER! 2023-05-21 21:41:37 +08:00
564f4d1e56 Fix CI branch 2023-05-21 21:14:41 +08:00
66fb25aa9b Add url to enable log 2023-05-21 21:04:02 +08:00
c3c2ed08a7 Update player command response handler 2023-05-21 20:51:12 +08:00
82477321d1 Capture Request Exception 2023-05-21 19:50:23 +08:00
f19c4b8e77 Update version to v1.5.2 2023-04-15 19:11:10 +08:00
3f1ecfe8a6 Fix incomplete multiple lines reply issue (fix #28) 2023-04-15 19:10:37 +08:00
止语
79564ff41c 添加新的QQ插件 (#26) 2023-03-18 09:57:34 +08:00
ded44804d4 Fix JsonUtils compatibility issue
Fix locking player object when executing command issue
2023-02-25 16:33:42 +08:00
4b8eb490f5 Remove version prefix dev 2023-02-18 15:49:46 +08:00
70261df520 Fix deprecated method JsonUtils.loadToClass 2023-02-18 15:47:51 +08:00
5c2be0e776 Update version to v1.5.1 2023-02-18 15:46:01 +08:00
72948121d6 Update README_en-US.md 2022-10-27 11:26:23 +08:00
42c748ad2e Update README.md 2022-10-27 11:24:55 +08:00
10 changed files with 457 additions and 341 deletions

View File

@@ -5,7 +5,7 @@ on:
paths: paths:
- "**.java" - "**.java"
branches: branches:
- "main" - "master"
pull_request: pull_request:
paths: paths:
- "**.java" - "**.java"

View File

@@ -4,48 +4,32 @@
一个为第三方客户端开放GC命令执行接口的插件 一个为第三方客户端开放GC命令执行接口的插件
`1.7.0` 起可以通过 `|` 或者换行来分隔多条命令,例如:
```shell
/a 1 | /a 2
/a 3
```
调用 `ping` 响应数据将包含插件版本号。
## 使用本插件的应用
- [GrasscutterTools](https://github.com/jie65535/GrasscutterCommandGenerator) —— Windows 客户端工具
- [JGrasscutterCommand](https://github.com/jie65535/JGrasscutterCommand) —— [Mirai](https://github.com/mamoe/mirai) 插件在QQ里执行命令
- [Yunzai-GrasscutterCommand](https://github.com/Zyy-boop/Yunzai-GrasscutterCommand) —— Yunzai-bot插件在QQ里执行命令
- 待补充
## 服务端安装 ## 服务端安装
1. 在 [Release](https://github.com/jie65535/gc-opencommand-plugin/releases) 下载 `jar` 1. 在 [Release](https://github.com/jie65535/gc-opencommand-plugin/releases) 下载 `jar`
2. 放入 `plugins` 文件夹即可 2. 放入 `plugins` 文件夹即可
> 注意,如果出现以下错误:
> ```log
> INFO:PluginManager Enabling plugin: opencommand-plugin
> Exception in thread "main" java.lang.NoSuchMethodError: 'void emu.grasscutter.server.event.EventHandler.register(emu.grasscutter.plugin.Plugin)'
> at com.github.jie65535.opencommand.OpenCommandPlugin.onEnable(OpenCommandPlugin.java:49)
> at emu.grasscutter.plugin.PluginManager.lambda$enablePlugins$3(PluginManager.java:131)
> ```
> 请使用v1.2.1版本插件,因为该报错表示你的服务端是旧版!
## 控制台连接 ## 控制台连接
1. 首次启动时,会在 `plugins` 目录下生成一个 `opencommand-plugin` 目录,打开并编辑 `config.json` 1. 首次启动时,会在 `plugins` 目录下生成一个 `opencommand-plugin` 目录,打开并编辑 `config.json`
2. 设置 `consoleToken` 的值为你的连接秘钥建议使用至少32字符的长随机字符串。 2. 设置 `consoleToken` 的值为你的连接秘钥建议使用至少32字符的长随机字符串。(检测到为空时会自动生成)
3. 重新启动服务端即可生效配置 3. 重新启动服务端即可生效配置
4. 在客户端中选择控制台身份,并填写你的 `consoleToken` 即可以控制台身份运行指令 4. 在客户端中选择控制台身份,并填写你的 `consoleToken` 即可以控制台身份运行指令
## 多服务器
### 主服务器 (Dispatch)
1.`opencommand-plugin` 目录下打开 `config.json`
2. 修改 `socketPort` 值为一个未被使用的端口
3. 设置 `socketToken` 多服务器通信密钥建议使用至少32字符的长随机字符串。
4. 重新启动服务端即可生效配置
### 子服务器 (Game)
1.`opencommand-plugin` 目录下打开 `config.json`
2. 修改 `socketHost``socketPort` 值为主服务器的地址和端口
3. 设置 `socketToken` 和主服务器相同的值
4. 设置 `socketDisplayName` 值为你的服务器名称 (用途请见[下方](https://github.com/jie65535/gc-opencommand-plugin#%E8%8E%B7%E5%8F%96%E5%A4%9A%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%88%97%E8%A1%A8))
5. 重新启动服务端即可生效配置
## 构建说明
1. 克隆仓库
2. 在目录下新建 `lib` 目录
3.`grasscutter-1.1.x-dev.jar` 放入 `lib` 目录
4. `gradle build`
## 玩家使用流程 ## 玩家使用流程
1. 在客户端中填写服务地址,确认是否支持 1. 在客户端中填写服务地址,确认是否支持
@@ -60,13 +44,34 @@
3. 使用 `token` 和**4位整数验证码**发送 `verify` 校验 3. 使用 `token` 和**4位整数验证码**发送 `verify` 校验
4. 如果验证通过,可以使用该 `token` 执行 `command` 动作 4. 如果验证通过,可以使用该 `token` 执行 `command` 动作
## 插件构建说明
1. 克隆仓库
2. 在目录下新建 `lib` 目录
3.`grasscutter-1.1.x-dev.jar` 放入 `lib` 目录
4. `gradle build`
## 多服务器
### 主服务器 (Dispatch)
1.`opencommand-plugin` 目录下打开 `config.json`
2. 修改 `socketPort` 值为一个未被使用的端口
3. 设置 `socketToken` 多服务器通信密钥建议使用至少32字符的长随机字符串。
4. 重新启动服务端即可生效配置
### 子服务器 (Game)
1.`opencommand-plugin` 目录下打开 `config.json`
2. 修改 `socketHost``socketPort` 值为主服务器的地址和端口
3. 设置 `socketToken` 和主服务器相同的值
4. 设置 `socketDisplayName` 值为你的服务器名称 (用途请见[下方](https://github.com/jie65535/gc-opencommand-plugin#%E8%8E%B7%E5%8F%96%E5%A4%9A%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%88%97%E8%A1%A8))
5. 重新启动服务端即可生效配置
--- ---
## `config.json` ## `config.json`
```json ```json5
{ {
// 控制台连接令牌 // 控制台连接令牌(检测到空时会自动生成)
"consoleToken": "", "consoleToken": "",
// 验证码过期时间(秒) // 验证码过期时间(秒)
"codeExpirationTime_S": 60, "codeExpirationTime_S": 60,
@@ -253,7 +258,7 @@ public final class JsonResponse {
| message | `Success` | `String` | | message | `Success` | `String` |
| data | `{}` | `JsonObject` | | data | `{}` | `JsonObject` |
```json ```json5
{ {
"retcode": 200, "retcode": 200,
"message": "success", "message": "success",
@@ -286,4 +291,4 @@ public final class JsonResponse {
|---------|------------------|----------| |---------|------------------|----------|
| retcode | `200` | `Int` | | retcode | `200` | `Int` |
| message | `Success` | `String` | | message | `Success` | `String` |
| data | `Command return` | `String` | | data | `Command return` | `String` |

View File

@@ -4,6 +4,20 @@
A plugin that opens the GC command execution interface for third-party clients A plugin that opens the GC command execution interface for third-party clients
Since `1.7.0`, multiple commands can be separated by `|` or newline, for example:
```shell
/a 1 | /a 2
/a 3
```
Invoking `ping` the response data will contain the plugin version.
## Applications using this plug-in
- [GrasscutterTools](https://github.com/jie65535/GrasscutterCommandGenerator) —— Windows Client Tools
- [JGrasscutterCommand](https://github.com/jie65535/JGrasscutterCommand) —— [Mirai](https://github.com/mamoe/mirai) Plugin, run commands in QQ
- [Yunzai-GrasscutterCommand](https://github.com/Zyy-boop/Yunzai-GrasscutterCommand) —— Yunzai-bot plugin, execute commands in QQ
- More...
## Server installation ## Server installation
1. Download the `jar` in [Release](https://github.com/jie65535/gc-opencommand-plugin/releases) 1. Download the `jar` in [Release](https://github.com/jie65535/gc-opencommand-plugin/releases)
@@ -14,32 +28,10 @@ A plugin that opens the GC command execution interface for third-party clients
1. When starting for the first time, a `opencommand-plugin` directory will be generated under the `plugins` directory, 1. When starting for the first time, a `opencommand-plugin` directory will be generated under the `plugins` directory,
open and edit `config.json` open and edit `config.json`
2. Set the value of `consoleToken` to your connection key. It is recommended to use a long random string of at least 32 2. Set the value of `consoleToken` to your connection key. It is recommended to use a long random string of at least 32
characters. characters. (automatically generated when empty is detected)
3. Restart the server to take effect 3. Restart the server to take effect
4. Select the console identity in the client, and fill in your `consoleToken` to run the command as the console identity 4. Select the console identity in the client, and fill in your `consoleToken` to run the command as the console identity
## Multi server
### Master server (Dispatch)
1. Open `config.json` in the `opencommand-plugin` directory
2. Modify the `socketPort` value to an unused port
3. Set `sockettoken` multi server communication key. It is recommended to use a long random string of at least 32 characters.
4. Restart the server to make the configuration effective
### Sub server (Game)
1. Open `config.json` in the `opencommand-plugin` directory
2. Modify the `sockethost` and `socketport` values to the address and port of the primary server
3. Set the same value of `sockettoken` and the primary server
4. Set the `socketDisplayName` value to your server name (See below for usage [Jump](https://github.com/jie65535/gc-opencommand-plugin/blob/master/README_en-US.md#get-mulit-server-list))
5. Restart the server to make the configuration effective
## Build
1. `git clone https://github.com/jie65535/gc-opencommand-plugin`
2. `cd gc-opencommand-plugin`
3. `mkdir lib`
4. `mv path/to/grasscutter-1.x.x-dev.jar ./lib`
5. `gradle build`
## Player ## Player
1. Fill in the service address in the client to confirm whether it supports 1. Fill in the service address in the client to confirm whether it supports
@@ -55,19 +47,50 @@ A plugin that opens the GC command execution interface for third-party clients
3. Send `verify` check using `token` and **4-digit integer verification code** 3. Send `verify` check using `token` and **4-digit integer verification code**
4. If the verification is passed, you can use the `token` to execute the `command` action 4. If the verification is passed, you can use the `token` to execute the `command` action
## Build
1. `git clone https://github.com/jie65535/gc-opencommand-plugin`
2. `cd gc-opencommand-plugin`
3. `mkdir lib`
4. `mv path/to/grasscutter-1.x.x-dev.jar ./lib`
5. `gradle build`
## Multi server
### Master server (Dispatch)
1. Open `config.json` in the `opencommand-plugin` directory
2. Modify the `socketPort` value to an unused port
3. Set `sockettoken` multi server communication key. It is recommended to use a long random string of at least 32 characters.
4. Restart the server to make the configuration effective
### Sub server (Game)
1. Open `config.json` in the `opencommand-plugin` directory
2. Modify the `sockethost` and `socketport` values to the address and port of the primary server
3. Set the same value of `sockettoken` and the primary server
4. Set the `socketDisplayName` value to your server name (See below for usage [Jump](https://github.com/jie65535/gc-opencommand-plugin/blob/master/README_en-US.md#get-mulit-server-list))
5. Restart the server to make the configuration effective
--- ---
## `config.json` ## `config.json`
```json ```json5
{ {
// console connection token (automatically generated when empty is detected)
"consoleToken": "", "consoleToken": "",
// Verification code expiration time (seconds)
"codeExpirationTime_S": 60, "codeExpirationTime_S": 60,
// Temporary token expiration time (seconds)
"tempTokenExpirationTime_S": 300, "tempTokenExpirationTime_S": 300,
// Authorization token last used expiration time (hours)
"tokenLastUseExpirationTime_H": 48, "tokenLastUseExpirationTime_H": 48,
"socketPort": 5746, // Multi-server communication port
"socketToken": "", "socketPort": 5746,
"socketHost": "127.0.0.1" // Multi-server communication key
"socketToken": "",
// Multi-server Dispatch server address
"socketHost": "127.0.0.1",
// multi-server display name
"socketDisplayName": ""
} }
``` ```
@@ -239,7 +262,7 @@ Success
| message | `Success` | `String` | | message | `Success` | `String` |
| data | `{}` | `JsonObject` | | data | `{}` | `JsonObject` |
```json ```json5
{ {
"retcode": 200, "retcode": 200,
"message": "success", "message": "success",
@@ -272,4 +295,4 @@ Success
|---------|------------------|----------| |---------|------------------|----------|
| retcode | `200` | `Int` | | retcode | `200` | `Int` |
| message | `Success` | `String` | | message | `Success` | `String` |
| data | `Command return` | `String` | | data | `Command return` | `String` |

View File

@@ -4,7 +4,7 @@ plugins {
} }
group 'com.github.jie65535.opencommand' group 'com.github.jie65535.opencommand'
version 'dev-1.5.0' version '1.7.0'
sourceCompatibility = 17 sourceCompatibility = 17
targetCompatibility = 17 targetCompatibility = 17

View File

@@ -24,21 +24,52 @@ import emu.grasscutter.game.player.Player;
import emu.grasscutter.server.event.game.ReceiveCommandFeedbackEvent; import emu.grasscutter.server.event.game.ReceiveCommandFeedbackEvent;
import emu.grasscutter.server.event.player.PlayerJoinEvent; import emu.grasscutter.server.event.player.PlayerJoinEvent;
import emu.grasscutter.server.event.player.PlayerQuitEvent; import emu.grasscutter.server.event.player.PlayerQuitEvent;
import emu.grasscutter.utils.MessageHandler; import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import java.util.ArrayList; import java.util.ArrayList;
public final class EventListeners { public final class EventListeners {
private static MessageHandler consoleMessageHandler; private static StringBuilder consoleMessageHandler;
private static final Int2ObjectMap<StringBuilder> playerMessageHandlers = new Int2ObjectOpenHashMap<>();
public static void setConsoleMessageHandler(MessageHandler handler) { public static void setConsoleMessageHandler(StringBuilder handler) {
consoleMessageHandler = handler; consoleMessageHandler = handler;
} }
/**
* 获取新的玩家消息处理类
* 获取时将创建或清空消息处理器并返回实例,**在执行命令前获取!**
* @param uid 玩家uid
* @return 新的玩家消息处理类
*/
public static StringBuilder getPlayerMessageHandler(int uid) {
var handler = playerMessageHandlers.get(uid);
if (handler == null) {
handler = new StringBuilder();
playerMessageHandlers.put(uid, handler);
}
return handler;
}
/**
* 命令执行反馈事件处理
*/
public static void onCommandResponse(ReceiveCommandFeedbackEvent event) { public static void onCommandResponse(ReceiveCommandFeedbackEvent event) {
if (consoleMessageHandler != null && event.getPlayer() == null) { StringBuilder handler;
consoleMessageHandler.setMessage(event.getMessage()); if (event.getPlayer() == null) {
handler = consoleMessageHandler;
} else {
handler = playerMessageHandlers.get(event.getPlayer().getUid());
}
if (handler != null) {
if (!handler.isEmpty()) {
// New line
handler.append(System.lineSeparator());
}
handler.append(event.getMessage());
} }
} }
@@ -57,6 +88,10 @@ public final class EventListeners {
SocketClient.sendPacket(playerList); SocketClient.sendPacket(playerList);
} }
/**
* 仅游戏模式下玩家离开事件处理方法
* 用于更新玩家列表
*/
public static void onPlayerQuit(PlayerQuitEvent playerQuitEvent) { public static void onPlayerQuit(PlayerQuitEvent playerQuitEvent) {
PlayerList playerList = new PlayerList(); PlayerList playerList = new PlayerList();
playerList.player = Grasscutter.getGameServer().getPlayers().size(); playerList.player = Grasscutter.getGameServer().getPlayers().size();
@@ -70,4 +105,15 @@ public final class EventListeners {
playerList.playerList = playerNames; playerList.playerList = playerNames;
SocketClient.sendPacket(playerList); SocketClient.sendPacket(playerList);
} }
/**
* 玩家离开事件处理 2
* 用于清理内存
*/
public static void onPlayerQuit2(PlayerQuitEvent playerQuitEvent) {
var uid = playerQuitEvent.getPlayer().getUid();
if (playerMessageHandlers.containsKey(uid)) {
playerMessageHandlers.remove(uid);
}
}
} }

View File

@@ -22,9 +22,9 @@ import com.github.jie65535.opencommand.json.JsonResponse;
import com.github.jie65535.opencommand.model.Client; import com.github.jie65535.opencommand.model.Client;
import com.github.jie65535.opencommand.socket.SocketData; import com.github.jie65535.opencommand.socket.SocketData;
import emu.grasscutter.command.CommandMap; import emu.grasscutter.command.CommandMap;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.server.http.Router; import emu.grasscutter.server.http.Router;
import emu.grasscutter.utils.Crypto; import emu.grasscutter.utils.Crypto;
import emu.grasscutter.utils.MessageHandler;
import emu.grasscutter.utils.Utils; import emu.grasscutter.utils.Utils;
import io.javalin.Javalin; import io.javalin.Javalin;
import io.javalin.http.Context; import io.javalin.http.Context;
@@ -46,130 +46,143 @@ public final class OpenCommandHandler implements Router {
private static final Map<String, Integer> codes = new HashMap<>(); private static final Map<String, Integer> codes = new HashMap<>();
private static final Int2ObjectMap<Date> codeExpireTime = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap<Date> codeExpireTime = new Int2ObjectOpenHashMap<>();
public static void handle(Context context) { public static void handle(Context context) {
var plugin = OpenCommandPlugin.getInstance(); var plugin = OpenCommandPlugin.getInstance();
var config = plugin.getConfig(); try {
var data = plugin.getData(); var config = plugin.getConfig();
var now = new Date(); var data = plugin.getData();
// Trigger cleanup action var now = new Date();
cleanupExpiredCodes(); // Trigger cleanup action
data.removeExpiredClients(); cleanupExpiredCodes();
data.removeExpiredClients();
var req = context.bodyAsClass(JsonRequest.class); var req = context.bodyAsClass(JsonRequest.class);
if (req.action.equals("sendCode")) { if (req.action.equals("sendCode")) {
int playerId = (int) req.data; int playerId = (int)Double.parseDouble(req.data.toString());
var player = plugin.getServer().getPlayerByUid(playerId); var player = plugin.getServer().getPlayerByUid(playerId);
if (player == null) {
context.json(new JsonResponse(404, "Player Not Found."));
} else {
if (codeExpireTime.containsKey(playerId)) {
var expireTime = codeExpireTime.get(playerId);
if (now.before(expireTime)) {
context.json(new JsonResponse(403, "Requests are too frequent"));
return;
}
}
String token = req.token;
if (token == null || token.isEmpty())
token = Utils.bytesToHex(Crypto.createSessionKey(32));
int code = Utils.randomRange(1000, 9999);
codeExpireTime.put(playerId, new Date(now.getTime() + config.codeExpirationTime_S * 1000L));
codes.put(token, code);
data.addClient(new Client(token, playerId, new Date(now.getTime() + config.tempTokenExpirationTime_S * 1000L)));
player.dropMessage("[Open Command] Verification code: " + code);
context.json(new JsonResponse(token));
}
return;
} else if (req.action.equals("ping")) {
context.json(new JsonResponse());
return;
} else if (req.action.equals("online")) {
var p = new ArrayList<String>();
plugin.getServer().getPlayers().forEach((uid, player) -> p.add(player.getNickname()));
context.json(new JsonResponse(200, "Success", new SocketData.OnlinePlayer(p)));
return;
}
// token is required
if (req.token == null || req.token.isEmpty()) {
context.json(new JsonResponse(401, "Unauthorized"));
return;
}
var isConsole = req.token.equals(config.consoleToken);
var client = data.getClientByToken(req.token);
if (!isConsole && client == null) {
context.json(new JsonResponse(401, "Unauthorized"));
return;
}
if (isConsole) {
if (req.action.equals("verify")) {
context.json(new JsonResponse());
return;
} else if (req.action.equals("command")) {
//noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (plugin) {
try {
plugin.getLogger().info(String.format("IP: %s run command in console > %s", context.ip(), req.data));
var resultCollector = new MessageHandler();
EventListeners.setConsoleMessageHandler(resultCollector);
CommandMap.getInstance().invoke(null, null, req.data.toString());
context.json(new JsonResponse(resultCollector.getMessage()));
} catch (Exception e) {
plugin.getLogger().warn("Run command failed.", e);
EventListeners.setConsoleMessageHandler(null);
context.json(new JsonResponse(500, "error", e.getLocalizedMessage()));
}
}
return;
} else if (req.action.equals("runmode")) {
context.json(new JsonResponse(200, "Success", 0));
return;
}
} else if (codes.containsKey(req.token)) {
if (req.action.equals("verify")) {
if (codes.get(req.token).equals(req.data)) {
codes.remove(req.token);
// update token expire time
client.tokenExpireTime = new Date(now.getTime() + config.tokenLastUseExpirationTime_H * 60L * 60L * 1000L);
context.json(new JsonResponse());
plugin.getLogger().info(String.format("Player %d has passed the verification, ip: %s", client.playerId, context.ip()));
plugin.saveData();
} else {
context.json(new JsonResponse(400, "Verification failed"));
}
return;
}
} else {
if (req.action.equals("command")) {
// update token expire time
client.tokenExpireTime = new Date(now.getTime() + config.tokenLastUseExpirationTime_H * 60L * 60L * 1000L);
var player = plugin.getServer().getPlayerByUid(client.playerId);
var command = req.data.toString();
if (player == null) { if (player == null) {
context.json(new JsonResponse(404, "Player not found")); context.json(new JsonResponse(404, "Player Not Found."));
} else {
if (codeExpireTime.containsKey(playerId)) {
var expireTime = codeExpireTime.get(playerId);
if (now.before(expireTime)) {
context.json(new JsonResponse(403, "Requests are too frequent"));
return;
}
}
String token = req.token;
if (token == null || token.isEmpty())
token = Utils.bytesToHex(Crypto.createSessionKey(32));
int code = Utils.randomRange(1000, 9999);
codeExpireTime.put(playerId, new Date(now.getTime() + config.codeExpirationTime_S * 1000L));
codes.put(token, code);
data.addClient(new Client(token, playerId, new Date(now.getTime() + config.tempTokenExpirationTime_S * 1000L)));
player.dropMessage("[Open Command] Verification code: " + code);
context.json(new JsonResponse(token));
}
return;
} else if (req.action.equals("ping")) {
context.json(new JsonResponse(plugin.getVersion()));
return;
} else if (req.action.equals("online")) {
var p = new ArrayList<String>();
plugin.getServer().getPlayers().forEach((uid, player) -> p.add(player.getNickname()));
context.json(new JsonResponse(200, "Success", new SocketData.OnlinePlayer(p)));
return;
}
// token is required
if (req.token == null || req.token.isEmpty()) {
context.json(new JsonResponse(401, "Unauthorized"));
return;
}
var isConsole = req.token.equals(config.consoleToken);
var client = data.getClientByToken(req.token);
if (!isConsole && client == null) {
context.json(new JsonResponse(401, "Unauthorized"));
return;
}
if (isConsole) {
if (req.action.equals("verify")) {
context.json(new JsonResponse());
return;
} else if (req.action.equals("command")) {
//noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (plugin) {
try {
plugin.getLogger().info(String.format("IP: %s run command in console > %s", context.ip(), req.data));
var resultCollector = new StringBuilder();
EventListeners.setConsoleMessageHandler(resultCollector);
tryInvokeCommand(null, null, req.data.toString());
context.json(new JsonResponse(resultCollector.toString()));
} catch (Exception e) {
plugin.getLogger().warn("Run command failed.", e);
EventListeners.setConsoleMessageHandler(null);
context.json(new JsonResponse(500, "error", e.getLocalizedMessage()));
}
}
return;
} else if (req.action.equals("runmode")) {
context.json(new JsonResponse(200, "Success", 0));
return; return;
} }
// Player MessageHandler do not support concurrency } else if (codes.containsKey(req.token)) {
//noinspection SynchronizationOnLocalVariableOrMethodParameter if (req.action.equals("verify")) {
synchronized (player) { if (codes.get(req.token) == (int)Double.parseDouble(req.data.toString())) {
try { codes.remove(req.token);
var resultCollector = new MessageHandler(); // update token expire time
player.setMessageHandler(resultCollector); client.tokenExpireTime = new Date(now.getTime() + config.tokenLastUseExpirationTime_H * 60L * 60L * 1000L);
CommandMap.getInstance().invoke(player, player, command); context.json(new JsonResponse());
context.json(new JsonResponse(resultCollector.getMessage())); plugin.getLogger().info(String.format("Player %d has passed the verification, ip: %s", client.playerId, context.ip()));
} catch (Exception e) { plugin.saveData();
plugin.getLogger().warn("Run command failed.", e); } else {
context.json(new JsonResponse(500, "error", e.getLocalizedMessage())); context.json(new JsonResponse(400, "Verification failed"));
} finally {
player.setMessageHandler(null);
} }
return;
}
} else {
if (req.action.equals("command")) {
// update token expire time
client.tokenExpireTime = new Date(now.getTime() + config.tokenLastUseExpirationTime_H * 60L * 60L * 1000L);
var player = plugin.getServer().getPlayerByUid(client.playerId);
if (player == null) {
context.json(new JsonResponse(404, "Player not found"));
return;
}
// Player MessageHandler do not support concurrency
var handler = EventListeners.getPlayerMessageHandler(player.getUid());
//noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (handler) {
try {
handler.setLength(0);
tryInvokeCommand(player, player, req.data.toString());
context.json(new JsonResponse(handler.toString()));
} catch (Exception e) {
plugin.getLogger().warn("Run command failed.", e);
context.json(new JsonResponse(500, "error", e.getLocalizedMessage()));
}
}
return;
} }
return;
} }
context.json(new JsonResponse(403, "forbidden"));
} catch (Throwable ex) {
plugin.getLogger().error("[OpenCommand] handler error.", ex);
}
}
private static void tryInvokeCommand(Player sender, Player target, String rawMessage) {
for (var command : rawMessage.split("\n[/!]|\\|")) {
command = command.trim();
if (command.isEmpty()) continue;
if (command.charAt(0) == '/' || command.charAt(0) == '!') {
command = command.substring(1);
}
CommandMap.getInstance().invoke(sender, target, command);
} }
context.json(new JsonResponse(403, "forbidden"));
} }
private static void cleanupExpiredCodes() { private static void cleanupExpiredCodes() {

View File

@@ -51,155 +51,159 @@ public final class OpenCommandOnlyHttpHandler implements Router {
public static void handle(Context context) { public static void handle(Context context) {
var plugin = OpenCommandPlugin.getInstance(); var plugin = OpenCommandPlugin.getInstance();
var config = plugin.getConfig(); try {
var data = plugin.getData(); var config = plugin.getConfig();
var now = new Date(); var data = plugin.getData();
// Trigger cleanup action var now = new Date();
cleanupExpiredCodes(); // Trigger cleanup action
data.removeExpiredClients(); cleanupExpiredCodes();
data.removeExpiredClients();
var req = context.bodyAsClass(JsonRequest.class); var req = context.bodyAsClass(JsonRequest.class);
if (req.action.equals("sendCode")) { if (req.action.equals("sendCode")) {
int playerId = (int) req.data; int playerId = (int)Double.parseDouble(req.data.toString());
var player = SocketData.getPlayer(playerId); var player = SocketData.getPlayer(playerId);
if (player == null) { if (player == null) {
context.json(new JsonResponse(404, "Player Not Found.")); context.json(new JsonResponse(404, "Player Not Found."));
} else { } else {
if (codeExpireTime.containsKey(playerId)) { if (codeExpireTime.containsKey(playerId)) {
var expireTime = codeExpireTime.get(playerId); var expireTime = codeExpireTime.get(playerId);
if (now.before(expireTime)) { if (now.before(expireTime)) {
context.json(new JsonResponse(403, "Requests are too frequent")); context.json(new JsonResponse(403, "Requests are too frequent"));
return; return;
}
} }
String token = req.token;
if (token == null || token.isEmpty())
token = Utils.bytesToHex(Crypto.createSessionKey(32));
int code = Utils.randomRange(1000, 9999);
codeExpireTime.put(playerId, new Date(now.getTime() + config.codeExpirationTime_S * 1000L));
codes.put(token, code);
data.addClient(new Client(token, playerId, new Date(now.getTime() + config.tempTokenExpirationTime_S * 1000L)));
Player.dropMessage(playerId, "[Open Command] Verification code: " + code);
context.json(new JsonResponse(token));
} }
return;
String token = req.token; } else if (req.action.equals("ping")) {
if (token == null || token.isEmpty())
token = Utils.bytesToHex(Crypto.createSessionKey(32));
int code = Utils.randomRange(1000, 9999);
codeExpireTime.put(playerId, new Date(now.getTime() + config.codeExpirationTime_S * 1000L));
codes.put(token, code);
data.addClient(new Client(token, playerId, new Date(now.getTime() + config.tempTokenExpirationTime_S * 1000L)));
Player.dropMessage(playerId, "[Open Command] Verification code: " + code);
context.json(new JsonResponse(token));
}
return;
} else if (req.action.equals("ping")) {
context.json(new JsonResponse());
return;
} else if (req.action.equals("online")) {
context.json(new JsonResponse(200, "Success", SocketData.getOnlinePlayer()));
return;
}
// token is required
if (req.token == null || req.token.isEmpty()) {
context.json(new JsonResponse(401, "Unauthorized"));
return;
}
var isConsole = req.token.equals(config.consoleToken);
var client = data.getClientByToken(req.token);
if (!isConsole && client == null) {
context.json(new JsonResponse(401, "Unauthorized"));
return;
}
if (isConsole) {
if (req.action.equals("verify")) {
context.json(new JsonResponse()); context.json(new JsonResponse());
return; return;
} else if (req.action.equals("command")) { } else if (req.action.equals("online")) {
var server = SocketServer.getClientInfoByUuid(req.server); context.json(new JsonResponse(200, "Success", SocketData.getOnlinePlayer()));
if (server == null) {
context.json(new JsonResponse(404, "Server Not Found."));
return;
}
plugin.getLogger().info(String.format("IP: %s run command in console > %s", context.ip(), req.data));
var wait = new SocketDataWait<HttpPacket>(2000L) {
@Override
public void run() {
}
@Override
public HttpPacket initData(HttpPacket data) {
return data;
}
@Override
public void timeout() {
}
};
SocketServer.sendPacketAndWait(server.ip, new RunConsoleCommand(req.data.toString()), wait);
var packet = wait.getData();
if (packet == null) {
context.json(new JsonResponse(408, "Timeout"));
return;
}
context.json(new JsonResponse(packet.code, packet.message, packet.data));
return;
} else if (req.action.equals("server")) {
context.json(new JsonResponse(200, "Success", SocketServer.getOnlineClient()));
return;
} else if (req.action.equals("runmode")) {
context.json(new JsonResponse(200, "Success", 1));
return; return;
} }
} else if (codes.containsKey(req.token)) {
if (req.action.equals("verify")) { // token is required
if (codes.get(req.token).equals(req.data)) { if (req.token == null || req.token.isEmpty()) {
codes.remove(req.token); context.json(new JsonResponse(401, "Unauthorized"));
return;
}
var isConsole = req.token.equals(config.consoleToken);
var client = data.getClientByToken(req.token);
if (!isConsole && client == null) {
context.json(new JsonResponse(401, "Unauthorized"));
return;
}
if (isConsole) {
if (req.action.equals("verify")) {
context.json(new JsonResponse());
return;
} else if (req.action.equals("command")) {
var server = SocketServer.getClientInfoByUuid(req.server);
if (server == null) {
context.json(new JsonResponse(404, "Server Not Found."));
return;
}
plugin.getLogger().info(String.format("IP: %s run command in console > %s", context.ip(), req.data));
var wait = new SocketDataWait<HttpPacket>(2000L) {
@Override
public void run() {
}
@Override
public HttpPacket initData(HttpPacket data) {
return data;
}
@Override
public void timeout() {
}
};
SocketServer.sendPacketAndWait(server.ip, new RunConsoleCommand(req.data.toString()), wait);
var packet = wait.getData();
if (packet == null) {
context.json(new JsonResponse(408, "Timeout"));
return;
}
context.json(new JsonResponse(packet.code, packet.message, packet.data));
return;
} else if (req.action.equals("server")) {
context.json(new JsonResponse(200, "Success", SocketServer.getOnlineClient()));
return;
} else if (req.action.equals("runmode")) {
context.json(new JsonResponse(200, "Success", 1));
return;
}
} else if (codes.containsKey(req.token)) {
if (req.action.equals("verify")) {
if (codes.get(req.token) == (int)Double.parseDouble(req.data.toString())) {
codes.remove(req.token);
// update token expire time
client.tokenExpireTime = new Date(now.getTime() + config.tokenLastUseExpirationTime_H * 60L * 60L * 1000L);
context.json(new JsonResponse());
plugin.getLogger().info(String.format("Player %d has passed the verification, ip: %s", client.playerId, context.ip()));
plugin.saveData();
} else {
context.json(new JsonResponse(400, "Verification failed"));
}
return;
}
} else {
if (req.action.equals("command")) {
SocketDataWait<HttpPacket> socketDataWait = new SocketDataWait<>(1000L * 10L) {
@Override
public void run() {
}
@Override
public HttpPacket initData(HttpPacket data) {
return data;
}
@Override
public void timeout() {
}
};
// update token expire time // update token expire time
client.tokenExpireTime = new Date(now.getTime() + config.tokenLastUseExpirationTime_H * 60L * 60L * 1000L); client.tokenExpireTime = new Date(now.getTime() + config.tokenLastUseExpirationTime_H * 60L * 60L * 1000L);
context.json(new JsonResponse()); var command = req.data.toString();
plugin.getLogger().info(String.format("Player %d has passed the verification, ip: %s", client.playerId, context.ip())); var player = new Player();
plugin.saveData(); player.uid = client.playerId;
} else { player.type = PlayerEnum.RunCommand;
context.json(new JsonResponse(400, "Verification failed")); player.data = command;
}
return; if (!SocketServer.sendUidPacketAndWait(client.playerId, player, socketDataWait)) {
} context.json(new JsonResponse(404, "Player Not Found."));
} else { return;
if (req.action.equals("command")) {
SocketDataWait<HttpPacket> socketDataWait = new SocketDataWait<>(1000L * 10L) {
@Override
public void run() {
} }
@Override
public HttpPacket initData(HttpPacket data) { HttpPacket httpPacket = socketDataWait.getData();
return data; if (httpPacket == null) {
context.json(new JsonResponse(500, "error", "Wait timeout"));
return;
} }
context.json(new JsonResponse(httpPacket.code, httpPacket.message));
@Override
public void timeout() {
}
};
// update token expire time
client.tokenExpireTime = new Date(now.getTime() + config.tokenLastUseExpirationTime_H * 60L * 60L * 1000L);
var command = req.data.toString();
var player = new Player();
player.uid = client.playerId;
player.type = PlayerEnum.RunCommand;
player.data = command;
if (!SocketServer.sendUidPacketAndWait(client.playerId, player, socketDataWait)) {
context.json(new JsonResponse(404, "Player Not Found."));
return; return;
} }
HttpPacket httpPacket = socketDataWait.getData();
if (httpPacket == null) {
context.json(new JsonResponse(500, "error", "Wait timeout"));
return;
}
context.json(new JsonResponse(httpPacket.code, httpPacket.message));
return;
} }
context.json(new JsonResponse(403, "forbidden"));
} catch (Exception ex) {
plugin.getLogger().error("[OpenCommand] handler error.", ex);
} }
context.json(new JsonResponse(403, "forbidden"));
} }
private static void cleanupExpiredCodes() { private static void cleanupExpiredCodes() {

View File

@@ -26,9 +26,13 @@ import emu.grasscutter.server.event.HandlerPriority;
import emu.grasscutter.server.event.game.ReceiveCommandFeedbackEvent; import emu.grasscutter.server.event.game.ReceiveCommandFeedbackEvent;
import emu.grasscutter.server.event.player.PlayerJoinEvent; import emu.grasscutter.server.event.player.PlayerJoinEvent;
import emu.grasscutter.server.event.player.PlayerQuitEvent; import emu.grasscutter.server.event.player.PlayerQuitEvent;
import emu.grasscutter.utils.Crypto;
import emu.grasscutter.utils.JsonUtils; import emu.grasscutter.utils.JsonUtils;
import emu.grasscutter.utils.Utils;
import java.io.*; import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
public final class OpenCommandPlugin extends Plugin { public final class OpenCommandPlugin extends Plugin {
@@ -57,10 +61,16 @@ public final class OpenCommandPlugin extends Plugin {
@Override @Override
public void onEnable() { public void onEnable() {
// 监听命令执行反馈
new EventHandler<>(ReceiveCommandFeedbackEvent.class) new EventHandler<>(ReceiveCommandFeedbackEvent.class)
.priority(HandlerPriority.HIGH) .priority(HandlerPriority.HIGH)
.listener(EventListeners::onCommandResponse) .listener(EventListeners::onCommandResponse)
.register(this); .register(this);
// 监听玩家离开事件
new EventHandler<>(PlayerQuitEvent.class)
.priority(HandlerPriority.NORMAL)
.listener(EventListeners::onPlayerQuit2)
.register(this);
if (runMode == Grasscutter.ServerRunMode.GAME_ONLY) { if (runMode == Grasscutter.ServerRunMode.GAME_ONLY) {
// 仅运行游戏服务器时注册玩家加入和离开事件 // 仅运行游戏服务器时注册玩家加入和离开事件
new EventHandler<>(PlayerJoinEvent.class) new EventHandler<>(PlayerJoinEvent.class)
@@ -76,7 +86,7 @@ public final class OpenCommandPlugin extends Plugin {
} else { } else {
getHandle().addRouter(OpenCommandHandler.class); getHandle().addRouter(OpenCommandHandler.class);
} }
getLogger().info("[OpenCommand] Enabled"); getLogger().info("[OpenCommand] Enabled. https://github.com/jie65535/gc-opencommand-plugin");
} }
@Override @Override
@@ -97,21 +107,24 @@ public final class OpenCommandPlugin extends Plugin {
var configFile = new File(getDataFolder(), "config.json"); var configFile = new File(getDataFolder(), "config.json");
if (!configFile.exists()) { if (!configFile.exists()) {
config = new OpenCommandConfig(); config = new OpenCommandConfig();
try (var file = new FileWriter(configFile)) { saveConfig();
file.write(JsonUtils.encode(config));
} catch (IOException e) {
getLogger().error("[OpenCommand] Unable to write to config file.");
} catch (Exception e) {
getLogger().error("[OpenCommand] Unable to save config file.");
}
} else { } else {
try { try {
config = JsonUtils.loadToClass(configFile.getAbsolutePath(), OpenCommandConfig.class); config = JsonUtils.decode(Files.readString(configFile.toPath(), StandardCharsets.UTF_8),
OpenCommandConfig.class);
} catch (Exception exception) { } catch (Exception exception) {
config = new OpenCommandConfig(); config = new OpenCommandConfig();
getLogger().error("[OpenCommand] There was an error while trying to load the configuration from config.json. Please make sure that there are no syntax errors. If you want to start with a default configuration, delete your existing config.json."); getLogger().error("[OpenCommand] There was an error while trying to load the configuration from config.json. Please make sure that there are no syntax errors. If you want to start with a default configuration, delete your existing config.json.");
} }
} }
// 检查控制台Token
if (config.consoleToken == null || config.consoleToken.isEmpty()) {
config.consoleToken = Utils.base64Encode(Crypto.createSessionKey(24));
saveConfig();
getLogger().warn("Detected that consoleToken is empty, automatically generated Token for you as follows: {}", config.consoleToken);
}
try { try {
runMode = Grasscutter.getConfig().server.runMode; runMode = Grasscutter.getConfig().server.runMode;
} catch (Exception ex) { } catch (Exception ex) {
@@ -119,6 +132,17 @@ public final class OpenCommandPlugin extends Plugin {
} }
} }
private void saveConfig() {
var configFile = new File(getDataFolder(), "config.json");
try (var file = new FileWriter(configFile)) {
file.write(JsonUtils.encode(config));
} catch (IOException e) {
getLogger().error("[OpenCommand] Unable to write to config file.");
} catch (Exception e) {
getLogger().error("[OpenCommand] Unable to save config file.");
}
}
private void loadData() { private void loadData() {
var dataFile = new File(getDataFolder(), "data.json"); var dataFile = new File(getDataFolder(), "data.json");
if (!dataFile.exists()) { if (!dataFile.exists()) {
@@ -126,11 +150,14 @@ public final class OpenCommandPlugin extends Plugin {
saveData(); saveData();
} else { } else {
try { try {
data = JsonUtils.loadToClass(dataFile.getAbsolutePath(), OpenCommandData.class); data = JsonUtils.decode(Files.readString(dataFile.toPath(), StandardCharsets.UTF_8),
OpenCommandData.class);
} catch (Exception exception) { } catch (Exception exception) {
data = new OpenCommandData();
getLogger().error("[OpenCommand] There was an error while trying to load the data from data.json. Please make sure that there are no syntax errors. If you want to start with a default data, delete your existing data.json."); getLogger().error("[OpenCommand] There was an error while trying to load the data from data.json. Please make sure that there are no syntax errors. If you want to start with a default data, delete your existing data.json.");
} }
if (data == null) {
data = new OpenCommandData();
}
} }
} }

View File

@@ -26,7 +26,6 @@ import com.github.jie65535.opencommand.socket.packet.player.PlayerList;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.command.CommandMap; import emu.grasscutter.command.CommandMap;
import emu.grasscutter.utils.JsonUtils; import emu.grasscutter.utils.JsonUtils;
import emu.grasscutter.utils.MessageHandler;
import org.slf4j.Logger; import org.slf4j.Logger;
import java.io.IOException; import java.io.IOException;
@@ -161,18 +160,16 @@ public class SocketClient {
return; return;
} }
// Player MessageHandler do not support concurrency // Player MessageHandler do not support concurrency
var handler = EventListeners.getPlayerMessageHandler(playerData.getUid());
//noinspection SynchronizationOnLocalVariableOrMethodParameter //noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (playerData) { synchronized (handler) {
try { try {
var resultCollector = new MessageHandler(); handler.setLength(0);
playerData.setMessageHandler(resultCollector);
CommandMap.getInstance().invoke(playerData, playerData, command); CommandMap.getInstance().invoke(playerData, playerData, command);
sendPacket(new HttpPacket(200, resultCollector.getMessage()), packet.packetID); sendPacket(new HttpPacket(200, handler.toString()), packet.packetID);
} catch (Exception e) { } catch (Exception e) {
OpenCommandPlugin.getInstance().getLogger().warn("[OpenCommand] Run command failed.", e); OpenCommandPlugin.getInstance().getLogger().warn("[OpenCommand] Run command failed.", e);
sendPacket(new HttpPacket(500, "error", e.getLocalizedMessage()), packet.packetID); sendPacket(new HttpPacket(500, "error", e.getLocalizedMessage()), packet.packetID);
} finally {
playerData.setMessageHandler(null);
} }
} }
} }
@@ -192,10 +189,10 @@ public class SocketClient {
//noinspection SynchronizationOnLocalVariableOrMethodParameter //noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (plugin) { synchronized (plugin) {
try { try {
var resultCollector = new MessageHandler(); var resultCollector = new StringBuilder();
EventListeners.setConsoleMessageHandler(resultCollector); EventListeners.setConsoleMessageHandler(resultCollector);
CommandMap.getInstance().invoke(null, null, consoleCommand.command); CommandMap.getInstance().invoke(null, null, consoleCommand.command);
sendPacket(new HttpPacket(resultCollector.getMessage()), packet.packetID); sendPacket(new HttpPacket(resultCollector.toString()), packet.packetID);
} catch (Exception e) { } catch (Exception e) {
mLogger.warn("[OpenCommand] Run command failed.", e); mLogger.warn("[OpenCommand] Run command failed.", e);
EventListeners.setConsoleMessageHandler(null); EventListeners.setConsoleMessageHandler(null);

View File

@@ -1,7 +1,8 @@
{ {
"name": "opencommand-plugin", "name": "opencommand-plugin",
"description": "Open command interface for third-party clients", "description": "Open command interface for third-party clients",
"version": "dev-1.5.0", "version": "1.7.0",
"mainClass": "com.github.jie65535.opencommand.OpenCommandPlugin", "mainClass": "com.github.jie65535.opencommand.OpenCommandPlugin",
"authors": ["jie65535", "方块君"] "authors": ["jie65535", "方块君"],
"api": 2
} }