8 Commits

Author SHA1 Message Date
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
1d03d3e476 Update version to v1.5.0 2022-10-06 13:27:17 +08:00
ef885af137 Impl token persistence (#19) 2022-10-06 13:27:04 +08:00
9 changed files with 158 additions and 59 deletions

View File

@@ -4,6 +4,11 @@
一个为第三方客户端开放GC命令执行接口的插件
## 使用本插件的应用
- [GrasscutterTools](https://github.com/jie65535/GrasscutterCommandGenerator) —— Windows 客户端工具
- [JGrasscutterCommand](https://github.com/jie65535/JGrasscutterCommand) —— [Mirai](https://github.com/mamoe/mirai) 插件在QQ里执行命令
- 待补充
## 服务端安装
1. 在 [Release](https://github.com/jie65535/gc-opencommand-plugin/releases) 下载 `jar`
@@ -286,4 +291,4 @@ public final class JsonResponse {
|---------|------------------|----------|
| retcode | `200` | `Int` |
| message | `Success` | `String` |
| data | `Command return` | `String` |
| data | `Command return` | `String` |

View File

@@ -4,6 +4,11 @@
A plugin that opens the GC command execution interface for third-party clients
## 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
- TODO
## Server installation
1. Download the `jar` in [Release](https://github.com/jie65535/gc-opencommand-plugin/releases)
@@ -272,4 +277,4 @@ Success
|---------|------------------|----------|
| retcode | `200` | `Int` |
| message | `Success` | `String` |
| data | `Command return` | `String` |
| data | `Command return` | `String` |

View File

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

View File

@@ -0,0 +1,42 @@
package com.github.jie65535.opencommand;
import com.github.jie65535.opencommand.model.Client;
import java.util.Date;
import java.util.Vector;
/**
* 插件持久化数据
*/
public class OpenCommandData {
/**
* 连接的客户端列表
*/
public Vector<Client> clients = new Vector<>();
/**
* 通过令牌获取客户端
* @param token 令牌
* @return 客户端对象若未找到返回null
*/
public Client getClientByToken(String token) {
for (var c : clients) {
if (c.token.equals(token))
return c;
}
return null;
}
/**
* 移除所有过期的客户端
*/
public void removeExpiredClients() {
var now = new Date();
clients.removeIf(client -> client.tokenExpireTime.before(now));
}
public void addClient(Client client) {
clients.add(client);
}
}

View File

@@ -19,6 +19,7 @@ package com.github.jie65535.opencommand;
import com.github.jie65535.opencommand.json.JsonRequest;
import com.github.jie65535.opencommand.json.JsonResponse;
import com.github.jie65535.opencommand.model.Client;
import com.github.jie65535.opencommand.socket.SocketData;
import emu.grasscutter.command.CommandMap;
import emu.grasscutter.server.http.Router;
@@ -42,17 +43,19 @@ public final class OpenCommandHandler implements Router {
javalin.post("/opencommand/api", OpenCommandHandler::handle);
}
private static final Map<String, Integer> clients = new HashMap<>();
private static final Map<String, Date> tokenExpireTime = new HashMap<>();
private static final Map<String, Integer> codes = new HashMap<>();
private static final Int2ObjectMap<Date> codeExpireTime = new Int2ObjectOpenHashMap<>();
private static final Int2ObjectMap<MessageHandler> playerMessageHandlers = new Int2ObjectOpenHashMap<>();
public static void handle(Context context) {
// Trigger cleanup action
cleanupExpiredData();
var plugin = OpenCommandPlugin.getInstance();
var config = plugin.getConfig();
var data = plugin.getData();
var now = new Date();
// Trigger cleanup action
cleanupExpiredCodes();
data.removeExpiredClients();
var req = context.bodyAsClass(JsonRequest.class);
if (req.action.equals("sendCode")) {
@@ -74,9 +77,8 @@ public final class OpenCommandHandler implements Router {
token = Utils.bytesToHex(Crypto.createSessionKey(32));
int code = Utils.randomRange(1000, 9999);
codeExpireTime.put(playerId, new Date(now.getTime() + config.codeExpirationTime_S * 1000L));
tokenExpireTime.put(token, new Date(now.getTime() + config.tempTokenExpirationTime_S * 1000L));
codes.put(token, code);
clients.put(token, playerId);
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));
}
@@ -97,7 +99,8 @@ public final class OpenCommandHandler implements Router {
return;
}
var isConsole = req.token.equals(config.consoleToken);
if (!isConsole && !clients.containsKey(req.token)) {
var client = data.getClientByToken(req.token);
if (!isConsole && client == null) {
context.json(new JsonResponse(401, "Unauthorized"));
return;
}
@@ -131,9 +134,10 @@ public final class OpenCommandHandler implements Router {
if (codes.get(req.token).equals(req.data)) {
codes.remove(req.token);
// update token expire time
tokenExpireTime.put(req.token, 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());
plugin.getLogger().info(String.format("Player %d has passed the verification, ip: %s", clients.get(req.token), context.ip()));
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"));
}
@@ -142,22 +146,26 @@ public final class OpenCommandHandler implements Router {
} else {
if (req.action.equals("command")) {
// update token expire time
tokenExpireTime.put(req.token, new Date(now.getTime() + config.tokenLastUseExpirationTime_H * 60L * 60L * 1000L));
var playerId = clients.get(req.token);
var player = plugin.getServer().getPlayerByUid(playerId);
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) {
context.json(new JsonResponse(404, "Player not found"));
return;
}
// Player MessageHandler do not support concurrency
var handler = playerMessageHandlers.get(player.getUid());
if (handler == null) {
handler = new MessageHandler();
playerMessageHandlers.put(player.getUid(), handler);
}
//noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (player) {
synchronized (handler) {
try {
var resultCollector = new MessageHandler();
player.setMessageHandler(resultCollector);
handler.setMessage("");
player.setMessageHandler(handler);
CommandMap.getInstance().invoke(player, player, command);
context.json(new JsonResponse(resultCollector.getMessage()));
context.json(new JsonResponse(handler.getMessage()));
} catch (Exception e) {
plugin.getLogger().warn("Run command failed.", e);
context.json(new JsonResponse(500, "error", e.getLocalizedMessage()));
@@ -171,18 +179,10 @@ public final class OpenCommandHandler implements Router {
context.json(new JsonResponse(403, "forbidden"));
}
private static void cleanupExpiredData() {
private static void cleanupExpiredCodes() {
var now = new Date();
codeExpireTime.int2ObjectEntrySet().removeIf(entry -> entry.getValue().before(now));
var it = tokenExpireTime.entrySet().iterator();
while (it.hasNext()) {
var entry = it.next();
if (entry.getValue().before(now)) {
it.remove();
// remove expired token
clients.remove(entry.getKey());
}
}
if (codeExpireTime.isEmpty())
codes.clear();
}
}

View File

@@ -19,6 +19,7 @@ package com.github.jie65535.opencommand;
import com.github.jie65535.opencommand.json.JsonRequest;
import com.github.jie65535.opencommand.json.JsonResponse;
import com.github.jie65535.opencommand.model.Client;
import com.github.jie65535.opencommand.socket.SocketData;
import com.github.jie65535.opencommand.socket.SocketDataWait;
import com.github.jie65535.opencommand.socket.SocketServer;
@@ -45,17 +46,17 @@ public final class OpenCommandOnlyHttpHandler implements Router {
javalin.post("/opencommand/api", OpenCommandOnlyHttpHandler::handle);
}
private static final Map<String, Integer> clients = new HashMap<>();
private static final Map<String, Date> tokenExpireTime = new HashMap<>();
private static final Map<String, Integer> codes = new HashMap<>();
private static final Int2ObjectMap<Date> codeExpireTime = new Int2ObjectOpenHashMap<>();
public static void handle(Context context) {
// Trigger cleanup action
cleanupExpiredData();
var plugin = OpenCommandPlugin.getInstance();
var config = plugin.getConfig();
var data = plugin.getData();
var now = new Date();
// Trigger cleanup action
cleanupExpiredCodes();
data.removeExpiredClients();
var req = context.bodyAsClass(JsonRequest.class);
if (req.action.equals("sendCode")) {
@@ -77,9 +78,8 @@ public final class OpenCommandOnlyHttpHandler implements Router {
token = Utils.bytesToHex(Crypto.createSessionKey(32));
int code = Utils.randomRange(1000, 9999);
codeExpireTime.put(playerId, new Date(now.getTime() + config.codeExpirationTime_S * 1000L));
tokenExpireTime.put(token, new Date(now.getTime() + config.tempTokenExpirationTime_S * 1000L));
codes.put(token, code);
clients.put(token, playerId);
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));
}
@@ -98,7 +98,8 @@ public final class OpenCommandOnlyHttpHandler implements Router {
return;
}
var isConsole = req.token.equals(config.consoleToken);
if (!isConsole && !clients.containsKey(req.token)) {
var client = data.getClientByToken(req.token);
if (!isConsole && client == null) {
context.json(new JsonResponse(401, "Unauthorized"));
return;
}
@@ -130,12 +131,12 @@ public final class OpenCommandOnlyHttpHandler implements Router {
};
SocketServer.sendPacketAndWait(server.ip, new RunConsoleCommand(req.data.toString()), wait);
var data = wait.getData();
if (data == null) {
var packet = wait.getData();
if (packet == null) {
context.json(new JsonResponse(408, "Timeout"));
return;
}
context.json(new JsonResponse(data.code, data.message, data.data));
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()));
@@ -149,9 +150,10 @@ public final class OpenCommandOnlyHttpHandler implements Router {
if (codes.get(req.token).equals(req.data)) {
codes.remove(req.token);
// update token expire time
tokenExpireTime.put(req.token, 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());
plugin.getLogger().info(String.format("Player %d has passed the verification, ip: %s", clients.get(req.token), context.ip()));
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"));
}
@@ -175,15 +177,14 @@ public final class OpenCommandOnlyHttpHandler implements Router {
};
// update token expire time
tokenExpireTime.put(req.token, new Date(now.getTime() + config.tokenLastUseExpirationTime_H * 60L * 60L * 1000L));
var playerId = clients.get(req.token);
client.tokenExpireTime = new Date(now.getTime() + config.tokenLastUseExpirationTime_H * 60L * 60L * 1000L);
var command = req.data.toString();
var player = new Player();
player.uid = playerId;
player.uid = client.playerId;
player.type = PlayerEnum.RunCommand;
player.data = command;
if (!SocketServer.sendUidPacketAndWait(playerId, player, socketDataWait)) {
if (!SocketServer.sendUidPacketAndWait(client.playerId, player, socketDataWait)) {
context.json(new JsonResponse(404, "Player Not Found."));
return;
}
@@ -201,18 +202,10 @@ public final class OpenCommandOnlyHttpHandler implements Router {
context.json(new JsonResponse(403, "forbidden"));
}
private static void cleanupExpiredData() {
private static void cleanupExpiredCodes() {
var now = new Date();
codeExpireTime.int2ObjectEntrySet().removeIf(entry -> entry.getValue().before(now));
var it = tokenExpireTime.entrySet().iterator();
while (it.hasNext()) {
var entry = it.next();
if (entry.getValue().before(now)) {
it.remove();
// remove expired token
clients.remove(entry.getKey());
}
}
if (codeExpireTime.isEmpty())
codes.clear();
}
}

View File

@@ -29,6 +29,8 @@ import emu.grasscutter.server.event.player.PlayerQuitEvent;
import emu.grasscutter.utils.JsonUtils;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
public final class OpenCommandPlugin extends Plugin {
@@ -40,6 +42,8 @@ public final class OpenCommandPlugin extends Plugin {
private OpenCommandConfig config;
private OpenCommandData data;
private Grasscutter.ServerRunMode runMode = Grasscutter.ServerRunMode.HYBRID;
@Override
@@ -47,6 +51,8 @@ public final class OpenCommandPlugin extends Plugin {
instance = this;
// 加载配置
loadConfig();
// 加载数据
loadData();
// 启动Socket
startSocket();
}
@@ -77,6 +83,7 @@ public final class OpenCommandPlugin extends Plugin {
@Override
public void onDisable() {
saveData();
getLogger().info("[OpenCommand] Disabled");
}
@@ -84,6 +91,10 @@ public final class OpenCommandPlugin extends Plugin {
return config;
}
public OpenCommandData getData() {
return data;
}
private void loadConfig() {
var configFile = new File(getDataFolder(), "config.json");
if (!configFile.exists()) {
@@ -97,7 +108,8 @@ public final class OpenCommandPlugin extends Plugin {
}
} else {
try {
config = JsonUtils.loadToClass(configFile.getAbsolutePath(), OpenCommandConfig.class);
config = JsonUtils.decode(Files.readString(configFile.toPath(), StandardCharsets.UTF_8),
OpenCommandConfig.class);
} catch (Exception exception) {
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.");
@@ -110,6 +122,32 @@ public final class OpenCommandPlugin extends Plugin {
}
}
private void loadData() {
var dataFile = new File(getDataFolder(), "data.json");
if (!dataFile.exists()) {
data = new OpenCommandData();
saveData();
} else {
try {
data = JsonUtils.decode(Files.readString(dataFile.toPath(), StandardCharsets.UTF_8),
OpenCommandData.class);
} 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.");
}
}
}
public void saveData() {
try (var file = new FileWriter(new File(getDataFolder(), "data.json"))) {
file.write(JsonUtils.encode(data));
} catch (IOException e) {
getLogger().error("[OpenCommand] Unable to write to data file.");
} catch (Exception e) {
getLogger().error("[OpenCommand] Unable to save data file.");
}
}
private void startSocket() {
if (runMode == Grasscutter.ServerRunMode.GAME_ONLY) {
getLogger().info("[OpenCommand] Starting socket client...");

View File

@@ -0,0 +1,16 @@
package com.github.jie65535.opencommand.model;
import java.util.Date;
public final class Client {
public String token;
public Integer playerId;
public Date tokenExpireTime;
public Client(String token, Integer playerId, Date tokenExpireTime) {
this.token = token;
this.playerId = playerId;
this.tokenExpireTime = tokenExpireTime;
}
}

View File

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