From 684ab421bda22ad830ec3dd44e854e4e202b4904 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B9=E5=9D=97=E5=90=9B?= Date: Thu, 21 Jul 2022 00:50:42 +0800 Subject: [PATCH 01/14] Add multi server support --- .gitignore | 3 + .../jie65535/opencommand/EventListeners.java | 38 ++++ .../opencommand/OpenCommandConfig.java | 3 + .../OpenCommandOnlyHttpHandler.java | 200 +++++++++++++++++ .../opencommand/OpenCommandPlugin.java | 36 +++- .../opencommand/socket/SocketClient.java | 185 ++++++++++++++++ .../opencommand/socket/SocketData.java | 30 +++ .../opencommand/socket/SocketDataWait.java | 60 ++++++ .../opencommand/socket/SocketServer.java | 204 ++++++++++++++++++ .../opencommand/socket/SocketUtils.java | 139 ++++++++++++ .../opencommand/socket/packet/BasePacket.java | 8 + .../opencommand/socket/packet/HeartBeat.java | 22 ++ .../opencommand/socket/packet/HttpPacket.java | 36 ++++ .../opencommand/socket/packet/Packet.java | 14 ++ .../opencommand/socket/packet/PacketEnum.java | 9 + .../socket/packet/player/Player.java | 31 +++ .../socket/packet/player/PlayerEnum.java | 7 + .../socket/packet/player/PlayerList.java | 28 +++ 18 files changed, 1052 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/github/jie65535/opencommand/OpenCommandOnlyHttpHandler.java create mode 100644 src/main/java/com/github/jie65535/opencommand/socket/SocketClient.java create mode 100644 src/main/java/com/github/jie65535/opencommand/socket/SocketData.java create mode 100644 src/main/java/com/github/jie65535/opencommand/socket/SocketDataWait.java create mode 100644 src/main/java/com/github/jie65535/opencommand/socket/SocketServer.java create mode 100644 src/main/java/com/github/jie65535/opencommand/socket/SocketUtils.java create mode 100644 src/main/java/com/github/jie65535/opencommand/socket/packet/BasePacket.java create mode 100644 src/main/java/com/github/jie65535/opencommand/socket/packet/HeartBeat.java create mode 100644 src/main/java/com/github/jie65535/opencommand/socket/packet/HttpPacket.java create mode 100644 src/main/java/com/github/jie65535/opencommand/socket/packet/Packet.java create mode 100644 src/main/java/com/github/jie65535/opencommand/socket/packet/PacketEnum.java create mode 100644 src/main/java/com/github/jie65535/opencommand/socket/packet/player/Player.java create mode 100644 src/main/java/com/github/jie65535/opencommand/socket/packet/player/PlayerEnum.java create mode 100644 src/main/java/com/github/jie65535/opencommand/socket/packet/player/PlayerList.java diff --git a/.gitignore b/.gitignore index aecefae..e2cd7f7 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ hs_err_pid* /lib/ +.gradle +.idea +/build \ No newline at end of file diff --git a/src/main/java/com/github/jie65535/opencommand/EventListeners.java b/src/main/java/com/github/jie65535/opencommand/EventListeners.java index 7c0bc6e..0201fe9 100644 --- a/src/main/java/com/github/jie65535/opencommand/EventListeners.java +++ b/src/main/java/com/github/jie65535/opencommand/EventListeners.java @@ -1,8 +1,17 @@ package com.github.jie65535.opencommand; +import com.github.jie65535.opencommand.socket.SocketClient; +import com.github.jie65535.opencommand.socket.SocketUtils; +import com.github.jie65535.opencommand.socket.packet.player.PlayerList; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.game.player.Player; import emu.grasscutter.server.event.game.ReceiveCommandFeedbackEvent; +import emu.grasscutter.server.event.player.PlayerJoinEvent; +import emu.grasscutter.server.event.player.PlayerQuitEvent; import emu.grasscutter.utils.MessageHandler; +import java.util.ArrayList; + public final class EventListeners { private static MessageHandler consoleMessageHandler; @@ -14,4 +23,33 @@ public final class EventListeners { consoleMessageHandler.setMessage(event.getMessage()); } } + + public static void onPlayerJoin(PlayerJoinEvent playerJoinEvent) { + PlayerList playerList = new PlayerList(); + playerList.player = Grasscutter.getGameServer().getPlayers().size(); + ArrayList playerNames = new ArrayList<>(); + playerNames.add(playerJoinEvent.getPlayer().getNickname()); + playerList.playerMap.put(playerJoinEvent.getPlayer().getUid(), playerJoinEvent.getPlayer().getNickname()); + for (Player player : Grasscutter.getGameServer().getPlayers().values()) { + playerNames.add(player.getNickname()); + playerList.playerMap.put(player.getUid(), player.getNickname()); + } + playerList.playerList = playerNames; + + SocketClient.sendPacket(playerList); + } + + public static void onPlayerQuit(PlayerQuitEvent playerQuitEvent) { + PlayerList playerList = new PlayerList(); + playerList.player = Grasscutter.getGameServer().getPlayers().size(); + ArrayList playerNames = new ArrayList<>(); + for (Player player : Grasscutter.getGameServer().getPlayers().values()) { + playerNames.add(player.getNickname()); + playerList.playerMap.put(player.getUid(), player.getNickname()); + } + playerList.playerMap.remove(playerQuitEvent.getPlayer().getUid()); + playerNames.remove(playerQuitEvent.getPlayer().getNickname()); + playerList.playerList = playerNames; + SocketClient.sendPacket(playerList); + } } diff --git a/src/main/java/com/github/jie65535/opencommand/OpenCommandConfig.java b/src/main/java/com/github/jie65535/opencommand/OpenCommandConfig.java index 5be40f4..fad781a 100644 --- a/src/main/java/com/github/jie65535/opencommand/OpenCommandConfig.java +++ b/src/main/java/com/github/jie65535/opencommand/OpenCommandConfig.java @@ -22,4 +22,7 @@ public class OpenCommandConfig { public int codeExpirationTime_S = 60; public int tempTokenExpirationTime_S = 300; public int tokenLastUseExpirationTime_H = 48; + public int socketPort = 5746; + public String socketToken = ""; + public String socketHost = "127.0.0.1"; } diff --git a/src/main/java/com/github/jie65535/opencommand/OpenCommandOnlyHttpHandler.java b/src/main/java/com/github/jie65535/opencommand/OpenCommandOnlyHttpHandler.java new file mode 100644 index 0000000..8530217 --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/OpenCommandOnlyHttpHandler.java @@ -0,0 +1,200 @@ +/* + * gc-opencommand + * Copyright (C) 2022 jie65535 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.github.jie65535.opencommand; + +import com.github.jie65535.opencommand.json.JsonRequest; +import com.github.jie65535.opencommand.json.JsonResponse; +import com.github.jie65535.opencommand.socket.SocketData; +import com.github.jie65535.opencommand.socket.SocketDataWait; +import com.github.jie65535.opencommand.socket.SocketServer; +import com.github.jie65535.opencommand.socket.packet.HttpPacket; +import com.github.jie65535.opencommand.socket.packet.player.Player; +import com.github.jie65535.opencommand.socket.packet.player.PlayerEnum; +import emu.grasscutter.command.CommandMap; +import emu.grasscutter.server.http.Router; +import emu.grasscutter.utils.Crypto; +import emu.grasscutter.utils.MessageHandler; +import emu.grasscutter.utils.Utils; +import express.Express; +import express.http.Request; +import express.http.Response; +import io.javalin.Javalin; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.FutureTask; + +public final class OpenCommandOnlyHttpHandler implements Router { + + @Override + public void applyRoutes(Express express, Javalin javalin) { + express.post("/opencommand/api", OpenCommandOnlyHttpHandler::handle); + } + + private static final Map clients = new HashMap<>(); + private static final Map tokenExpireTime = new HashMap<>(); + private static final Map codes = new HashMap<>(); + private static final Int2ObjectMap codeExpireTime = new Int2ObjectOpenHashMap<>(); + + public static void handle(Request request, Response response) { + // Trigger cleanup action + cleanupExpiredData(); + var plugin = OpenCommandPlugin.getInstance(); + var config = plugin.getConfig(); + var now = new Date(); + + var req = request.body(JsonRequest.class); + response.type("application/json"); + if (req.action.equals("sendCode")) { + int playerId = (int) req.data; + var player = SocketData.getPlayer(playerId); + if (player == null) { + response.json(new JsonResponse(404, "Player Not Found.")); + } else { + if (codeExpireTime.containsKey(playerId)) { + var expireTime = codeExpireTime.get(playerId); + if (now.before(expireTime)) { + response.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)); + tokenExpireTime.put(token, new Date(now.getTime() + config.tempTokenExpirationTime_S * 1000L)); + codes.put(token, code); + clients.put(token, playerId); + Player.dropMessage(playerId, "[Open Command] Verification code: " + code); + response.json(new JsonResponse(token)); + } + return; + } else if (req.action.equals("ping")) { + response.json(new JsonResponse()); + return; + } + + // token is required + if (req.token == null || req.token.isEmpty()) { + response.json(new JsonResponse(401, "Unauthorized")); + return; + } + var isConsole = req.token.equals(config.consoleToken); + if (!isConsole && !clients.containsKey(req.token)) { + response.json(new JsonResponse(401, "Unauthorized")); + return; + } + + if (isConsole) { + if (req.action.equals("verify")) { + response.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", request.ip(), req.data)); + var resultCollector = new MessageHandler(); + EventListeners.setConsoleMessageHandler(resultCollector); + CommandMap.getInstance().invoke(null, null, req.data.toString()); + response.json(new JsonResponse(resultCollector.getMessage())); + } catch (Exception e) { + plugin.getLogger().warn("Run command failed.", e); + EventListeners.setConsoleMessageHandler(null); + response.json(new JsonResponse(500, "error", e.getLocalizedMessage())); + } + } + 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 + tokenExpireTime.put(req.token, new Date(now.getTime() + config.tokenLastUseExpirationTime_H * 60L * 60L * 1000L)); + response.json(new JsonResponse()); + plugin.getLogger().info(String.format("Player %d has passed the verification, ip: %s", clients.get(req.token), request.ip())); + } else { + response.json(new JsonResponse(400, "Verification failed")); + } + return; + } + } else { + if (req.action.equals("command")) { + SocketDataWait socketDataWait = new SocketDataWait<>(1000L * 10L) { + @Override + public void run() {} + + @Override + public HttpPacket initData(HttpPacket data) { + return data; + } + + @Override + public void timeout() { + response.json(new JsonResponse(408, "Wait server timeout")); + } + }; + + // 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 command = req.data.toString(); + var player = new Player(); + player.uid = playerId; + player.type = PlayerEnum.RunCommand; + player.data = command; + + if (!SocketServer.sendUidPacket(playerId, player, socketDataWait)) { + response.json(new JsonResponse(404, "Player Not Found.")); + return; + } + + + HttpPacket httpPacket = socketDataWait.getData(); + if (httpPacket == null) { + response.json(new JsonResponse(500, "error", "Server connect failed.")); + return; + } + response.json(new JsonResponse(httpPacket.code, httpPacket.message)); + return; + } + } + response.json(new JsonResponse(403, "forbidden")); + } + + private static void cleanupExpiredData() { + 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()); + } + } + } +} diff --git a/src/main/java/com/github/jie65535/opencommand/OpenCommandPlugin.java b/src/main/java/com/github/jie65535/opencommand/OpenCommandPlugin.java index 39af95e..4529878 100644 --- a/src/main/java/com/github/jie65535/opencommand/OpenCommandPlugin.java +++ b/src/main/java/com/github/jie65535/opencommand/OpenCommandPlugin.java @@ -17,11 +17,15 @@ */ package com.github.jie65535.opencommand; +import com.github.jie65535.opencommand.socket.SocketClient; +import com.github.jie65535.opencommand.socket.SocketServer; import emu.grasscutter.Grasscutter; import emu.grasscutter.plugin.Plugin; import emu.grasscutter.server.event.EventHandler; import emu.grasscutter.server.event.HandlerPriority; import emu.grasscutter.server.event.game.ReceiveCommandFeedbackEvent; +import emu.grasscutter.server.event.player.PlayerJoinEvent; +import emu.grasscutter.server.event.player.PlayerQuitEvent; import java.io.File; import java.io.FileReader; @@ -47,7 +51,21 @@ public final class OpenCommandPlugin extends Plugin { .priority(HandlerPriority.HIGH) .listener(EventListeners::onCommandResponse) .register(this); - getHandle().addRouter(OpenCommandHandler.class); + if (Grasscutter.getConfig().server.runMode == Grasscutter.ServerRunMode.GAME_ONLY) { + // 仅运行游戏服务器时注册玩家加入和离开事件 + new EventHandler<>(PlayerJoinEvent.class) + .priority(HandlerPriority.HIGH) + .listener(EventListeners::onPlayerJoin) + .register(this); + new EventHandler<>(PlayerQuitEvent.class) + .priority(HandlerPriority.HIGH) + .listener(EventListeners::onPlayerQuit) + .register(this); + } else if (Grasscutter.getConfig().server.runMode == Grasscutter.ServerRunMode.DISPATCH_ONLY) { + getHandle().addRouter(OpenCommandOnlyHttpHandler.class); + } else { + getHandle().addRouter(OpenCommandHandler.class); + } getLogger().info("[OpenCommand] Enabled"); } @@ -79,5 +97,21 @@ public final class OpenCommandPlugin extends Plugin { getLogger().error("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."); } } + // 启动Socket + startSocket(); + } + + private void startSocket() { + if (Grasscutter.getConfig().server.runMode == Grasscutter.ServerRunMode.GAME_ONLY) { + getLogger().info("[OpenCommand] Starting socket client..."); + SocketClient.connectServer(); + } else if (Grasscutter.getConfig().server.runMode == Grasscutter.ServerRunMode.DISPATCH_ONLY) { + getLogger().info("[OpenCommand] Starting socket server..."); + try { + SocketServer.startServer(); + } catch (IOException e) { + getLogger().error("Unable to start socket server.", e); + } + } } } diff --git a/src/main/java/com/github/jie65535/opencommand/socket/SocketClient.java b/src/main/java/com/github/jie65535/opencommand/socket/SocketClient.java new file mode 100644 index 0000000..cf0fc14 --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/SocketClient.java @@ -0,0 +1,185 @@ +package com.github.jie65535.opencommand.socket; + +import com.github.jie65535.opencommand.OpenCommandConfig; +import com.github.jie65535.opencommand.OpenCommandPlugin; +import com.github.jie65535.opencommand.socket.packet.BasePacket; +import com.github.jie65535.opencommand.socket.packet.HeartBeat; +import com.github.jie65535.opencommand.socket.packet.HttpPacket; +import com.github.jie65535.opencommand.socket.packet.Packet; +import com.github.jie65535.opencommand.socket.packet.player.Player; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.command.CommandMap; +import emu.grasscutter.utils.MessageHandler; +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.util.Timer; +import java.util.TimerTask; + +// Socket 客户端 +public class SocketClient { + public static ClientThread clientThread; + + public static Logger mLogger; + + public static Timer timer; + + public static boolean connect = false; + + // 连接服务器 + public static void connectServer() { + OpenCommandConfig config = OpenCommandPlugin.getInstance().getConfig(); + mLogger = OpenCommandPlugin.getInstance().getLogger(); + clientThread = new ClientThread(config.socketHost, config.socketPort); + + if (timer != null) { + timer.cancel(); + } + timer = new Timer(); + timer.schedule(new SendHeartBeatPacket(), 500); + } + + // 发送数据包 + public static boolean sendPacket(BasePacket packet) { + var p = SocketUtils.getPacket(packet); + if (!clientThread.sendPacket(p)) { + mLogger.warn("[OpenCommand] Send packet to server failed"); + mLogger.info("[OpenCommand] Reconnect to server"); + connect = false; + connectServer(); + return false; + } + return true; + } + + // 发送数据包带数据包ID + public static boolean sendPacket(BasePacket packet, String packetID) { + if (!clientThread.sendPacket(SocketUtils.getPacketAndPackID(packet, packetID))) { + mLogger.warn("[OpenCommand] Send packet to server failed"); + mLogger.info("[OpenCommand] Reconnect to server"); + connect = false; + connectServer(); + return false; + } + return true; + } + + // 心跳包发送 + private static class SendHeartBeatPacket extends TimerTask { + @Override + public void run() { + if (connect) { + sendPacket(new HeartBeat("Pong")); + } + } + } + + // 数据包接收 + private static class ReceiveThread extends Thread { + private InputStream is; + private String token; + + public ReceiveThread(Socket socket) { + token = OpenCommandPlugin.getInstance().getConfig().socketToken; + try { + is = socket.getInputStream(); + } catch (IOException e) { + e.printStackTrace(); + } + start(); + } + + @Override + public void run() { + //noinspection InfiniteLoopStatement + while (true) { + String data = SocketUtils.readString(is); + Packet packet = Grasscutter.getGsonFactory().fromJson(data, Packet.class); + if (packet.token.equals(token)) { + switch (packet.type) { + // 玩家类 + case Player: + var player = Grasscutter.getGsonFactory().fromJson(packet.data, Player.class); + switch (player.type) { + // 运行命令 + case RunCommand -> { + var command = player.data; + var playerData = OpenCommandPlugin.getInstance().getServer().getPlayerByUid(player.uid); + if (playerData == null) { + sendPacket(new HttpPacket(404, "Player not found."), packet.packetID); + return; + } + // Player MessageHandler do not support concurrency + //noinspection SynchronizationOnLocalVariableOrMethodParameter + synchronized (playerData) { + try { + var resultCollector = new MessageHandler(); + playerData.setMessageHandler(resultCollector); + CommandMap.getInstance().invoke(playerData, playerData, command); + sendPacket(new HttpPacket(200, resultCollector.getMessage()), packet.packetID); + } catch (Exception e) { + OpenCommandPlugin.getInstance().getLogger().warn("Run command failed.", e); + sendPacket(new HttpPacket(500, "error", e.getLocalizedMessage()), packet.packetID); + } finally { + playerData.setMessageHandler(null); + } + } + } + // 发送信息 + case DropMessage -> { + var playerData = OpenCommandPlugin.getInstance().getServer().getPlayerByUid(player.uid); + if (playerData == null) { + return; + } + playerData.dropMessage(player.data); + } + } + break; + } + } + } + } + } + + // 客户端连接线程 + private static class ClientThread extends Thread { + private final String ip; + private final int port; + private Socket socket; + private OutputStream os; + + public ClientThread(String ip, int port) { + this.ip = ip; + this.port = port; + start(); + } + + public Socket getSocket() { + return socket; + } + + public boolean sendPacket(String string) { + return SocketUtils.writeString(os, string); + } + + @Override + public void run() { + try { + socket = new Socket(ip, port); + connect = true; + os = socket.getOutputStream(); + mLogger.info("Connect to server: " + ip + ":" + port); + new ReceiveThread(socket); + } catch (IOException e) { + connect = false; + mLogger.warn("Connect to server failed: " + ip + ":" + port); + mLogger.warn("[OpenCommand] Reconnect to server"); + connectServer(); + throw new RuntimeException(e); + } + } + } +} diff --git a/src/main/java/com/github/jie65535/opencommand/socket/SocketData.java b/src/main/java/com/github/jie65535/opencommand/socket/SocketData.java new file mode 100644 index 0000000..74affcd --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/SocketData.java @@ -0,0 +1,30 @@ +package com.github.jie65535.opencommand.socket; + +import com.github.jie65535.opencommand.socket.packet.player.PlayerList; + +import java.util.HashMap; +import java.util.concurrent.atomic.AtomicReference; + +// Socket 数据保存 +public class SocketData { + public static HashMap playerList = new HashMap<>(); + + public static PlayerList getPlayer(int uid) { + for (PlayerList player : playerList.values()) { + if (player.playerMap.get(uid) != null) { + return player; + } + } + return null; + } + + public static String getPlayerInServer(int uid) { + AtomicReference ret = new AtomicReference<>(); + playerList.forEach((key, value) -> { + if (value.playerMap.get(uid) != null) { + ret.set(key); + } + }); + return ret.get(); + } +} diff --git a/src/main/java/com/github/jie65535/opencommand/socket/SocketDataWait.java b/src/main/java/com/github/jie65535/opencommand/socket/SocketDataWait.java new file mode 100644 index 0000000..e1f8e0e --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/SocketDataWait.java @@ -0,0 +1,60 @@ +package com.github.jie65535.opencommand.socket; + +// 异步等待数据返回 +public abstract class SocketDataWait extends Thread { + public T data; + public long timeout; + public long time; + public String uid; + + /** + * 异步等待数据返回 + * @param timeout 超时时间 + */ + public SocketDataWait(long timeout) { + this.timeout = timeout; + start(); + } + + public abstract void run(); + + /** + * 数据处理 + * @param data 数据 + * @return 处理后的数据 + */ + public abstract T initData(T data); + + /** + * 超时回调 + */ + public abstract void timeout(); + + /** + * 异步设置数据 + * @param data 数据 + */ + public void setData(Object data) { + this.data = initData((T) data); + } + + /** + * 获取异步数据(此操作会一直堵塞直到获取到数据) + * @return 数据 + */ + public T getData() { + while (data == null) { + try { + time += 100; + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } + if (time > timeout) { + timeout(); + return null; + } + } + return data; + } +} diff --git a/src/main/java/com/github/jie65535/opencommand/socket/SocketServer.java b/src/main/java/com/github/jie65535/opencommand/socket/SocketServer.java new file mode 100644 index 0000000..3612cc5 --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/SocketServer.java @@ -0,0 +1,204 @@ +package com.github.jie65535.opencommand.socket; + +import com.github.jie65535.opencommand.OpenCommandPlugin; +import com.github.jie65535.opencommand.socket.packet.BasePacket; +import com.github.jie65535.opencommand.socket.packet.HttpPacket; +import com.github.jie65535.opencommand.socket.packet.Packet; +import com.github.jie65535.opencommand.socket.packet.player.PlayerList; +import emu.grasscutter.Grasscutter; +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.HashMap; +import java.util.Timer; +import java.util.TimerTask; + +// Socket 服务器 +public class SocketServer { + // 客户端超时时间 + private static final int TIMEOUT = 5000; + private static final HashMap clientList = new HashMap<>(); + + private static final HashMap clientTimeout = new HashMap<>(); + + private static Logger mLogger; + + public static void startServer() throws IOException { + int port = OpenCommandPlugin.getInstance().getConfig().socketPort; + mLogger = OpenCommandPlugin.getInstance().getLogger(); + new Timer().schedule(new SocketClientCheck(), 500); + new WaitClientConnect(port); + } + + // 向全部客户端发送数据 + public static boolean sendAllPacket(BasePacket packet) { + var p = SocketUtils.getPacket(packet); + HashMap old = (HashMap) clientList.clone(); + for (var client : old.entrySet()) { + if (!client.getValue().sendPacket(p)) { + mLogger.warn("[OpenCommand] Send packet to client {} failed", client.getKey()); + clientList.remove(client.getKey()); + } + } + return false; + } + + // 根据地址发送到相应的客户端 + public static boolean sendPacket(String address, BasePacket packet) { + var p = SocketUtils.getPacket(packet); + var client = clientList.get(address); + if (client != null) { + if (client.sendPacket(p)) { + return true; + } + mLogger.warn("[OpenCommand] Send packet to client {} failed", address); + clientList.remove(address); + } + return false; + } + + // 根据Uid发送到相应的客户端异步返回数据 + public static boolean sendUidPacket(Integer playerId, BasePacket player, SocketDataWait socketDataWait) { + var p = SocketUtils.getPacketAndPackID(player); + var clientID = SocketData.getPlayerInServer(playerId); + if (clientID == null) return false; + var client = clientList.get(clientID); + if (client != null) { + socketDataWait.uid = p.get(0); + if (!client.sendPacket(p.get(1), socketDataWait)) { + mLogger.warn("[OpenCommand] Send packet to client {} failed", clientID); + clientList.remove(clientID); + return false; + } + return true; + } + return false; + } + + // 客户端超时检测 + private static class SocketClientCheck extends TimerTask { + @Override + public void run() { + HashMap old = (HashMap) clientTimeout.clone(); + for (var client : old.entrySet()) { + var clientID = client.getKey(); + var clientTime = client.getValue(); + if (clientTime > TIMEOUT) { + mLogger.info("Client {} timeout, disconnect.", clientID); + clientList.remove(clientID); + clientTimeout.remove(clientID); + } else { + clientTimeout.put(clientID, clientTime + 500); + } + } + } + } + + // 客户端数据包处理 + private static class ClientThread extends Thread { + private final Socket socket; + private InputStream is; + private OutputStream os; + private final String address; + private final String token; + + private final HashMap> socketDataWaitList = new HashMap<>(); + + public ClientThread(Socket accept) { + socket = accept; + address = socket.getInetAddress() + ":" + socket.getPort(); + token = OpenCommandPlugin.getInstance().getConfig().socketToken; + try { + is = accept.getInputStream(); + os = accept.getOutputStream(); + } catch (IOException e) { + e.printStackTrace(); + } + start(); + } + + public Socket getSocket() { + return socket; + } + + // 发送数据包 + public boolean sendPacket(String packet) { + return SocketUtils.writeString(os, packet); + } + + // 发送异步数据包 + public boolean sendPacket(String packet, SocketDataWait socketDataWait) { + if (SocketUtils.writeString(os, packet)) { + socketDataWaitList.put(socketDataWait.uid, socketDataWait); + return true; + } else { + return false; + } + } + + @Override + public void run() { + // noinspection InfiniteLoopStatement + while (true) { + String data = SocketUtils.readString(is); + Packet packet = Grasscutter.getGsonFactory().fromJson(data, Packet.class); + if (packet.token.equals(token)) { + switch (packet.type) { + // 缓存玩家列表 + case PlayerList -> { + PlayerList playerList = Grasscutter.getGsonFactory().fromJson(packet.data, PlayerList.class); + SocketData.playerList.put(address, playerList); + } + // Http信息返回 + case HttpPacket -> { + HttpPacket httpPacket = Grasscutter.getGsonFactory().fromJson(packet.data, HttpPacket.class); + var socketWait = socketDataWaitList.get(packet.packetID); + if (socketWait == null) { + mLogger.error("HttpPacket: " + packet.packetID + " not found"); + return; + } + socketWait.setData(httpPacket); + socketDataWaitList.remove(packet.packetID); + } + // 心跳包 + case HeartBeat -> { + clientTimeout.put(address, 0); + } + } + } + } + } + } + + // 等待客户端连接 + private static class WaitClientConnect extends Thread { + ServerSocket socketServer; + + public WaitClientConnect(int port) throws IOException { + socketServer = new ServerSocket(port); + start(); + } + + @Override + public void run() { + mLogger.info("Start socket server on port " + socketServer.getLocalPort()); + // noinspection InfiniteLoopStatement + while (true) { + try { + Socket accept = socketServer.accept(); + String address = accept.getInetAddress() + ":" + accept.getPort(); + mLogger.info("Client connect: " + address); + ClientThread clientThread = new ClientThread(accept); + clientList.put(address, clientThread); + clientTimeout.put(address, 0); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } +} diff --git a/src/main/java/com/github/jie65535/opencommand/socket/SocketUtils.java b/src/main/java/com/github/jie65535/opencommand/socket/SocketUtils.java new file mode 100644 index 0000000..74a5faa --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/SocketUtils.java @@ -0,0 +1,139 @@ +package com.github.jie65535.opencommand.socket; + +import com.github.jie65535.opencommand.OpenCommandPlugin; +import com.github.jie65535.opencommand.socket.packet.BasePacket; +import com.github.jie65535.opencommand.socket.packet.Packet; +import emu.grasscutter.Grasscutter; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +// Socket 工具类 +public class SocketUtils { + + /** + * 获取打包后的数据包 + * @param bPacket 数据包 + * @return 打包后的数据包 + */ + public static String getPacket(BasePacket bPacket) { + Packet packet = new Packet(); + packet.token = OpenCommandPlugin.getInstance().getConfig().socketToken; + packet.type = bPacket.getType(); + packet.data = bPacket.getPacket(); + packet.packetID = UUID.randomUUID().toString(); + return Grasscutter.getGsonFactory().toJson(packet); + } + + /** + * 获取打包后的数据包 + * @param bPacket BasePacket + * @return list[0] 是包ID, list[1] 是数据包 + */ + public static List getPacketAndPackID(BasePacket bPacket) { + Packet packet = new Packet(); + packet.token = OpenCommandPlugin.getInstance().getConfig().socketToken; + packet.type = bPacket.getType(); + packet.data = bPacket.getPacket(); + packet.packetID = UUID.randomUUID().toString(); + + List list = new ArrayList<>(); + list.add(packet.packetID); + list.add(Grasscutter.getGsonFactory().toJson(packet)); + return list; + } + + /** + * 获取打包后的数据包 + * @param bPacket 数据包 + * @param packetID 数据包ID + * @return 打包后的数据包 + */ + public static String getPacketAndPackID(BasePacket bPacket, String packetID) { + Packet packet = new Packet(); + packet.token = OpenCommandPlugin.getInstance().getConfig().socketToken; + packet.type = bPacket.getType(); + packet.data = bPacket.getPacket(); + packet.packetID = packetID; + return Grasscutter.getGsonFactory().toJson(packet); + } + + /** + * 读整数 + * @param is 输入流 + * @return 整数 + */ + public static int readInt(InputStream is) { + int[] values = new int[4]; + try { + for (int i = 0; i < 4; i++) { + values[i] = is.read(); + } + } catch (IOException e) { + e.printStackTrace(); + } + + return values[0]<<24 | values[1]<<16 | values[2]<<8 | values[3]; + } + + /** + * 写整数 + * @param os 输出流 + * @param value 整数 + */ + public static void writeInt(OutputStream os, int value) { + int[] values = new int[4]; + values[0] = (value>>24)&0xFF; + values[1] = (value>>16)&0xFF; + values[2] = (value>>8)&0xFF; + values[3] = (value)&0xFF; + + try{ + for (int i = 0; i < 4; i++) { + os.write(values[i]); + } + }catch (IOException e){ + e.printStackTrace(); + } + } + + /** + * 读字符串 + * @param is 输入流 + * @return 字符串 + */ + public static String readString(InputStream is) { + int len = readInt(is); + byte[] sByte = new byte[len]; + try { + is.read(sByte); + } catch (IOException e) { + e.printStackTrace(); + } + String s = new String(sByte); + return s; + } + + /** + * 写字符串 + * @param os 输出流 + * @param s 字符串 + * @return 是否成功 + */ + public static boolean writeString(OutputStream os,String s) { + try { + byte[] bytes = s.getBytes(); + int len = bytes.length; + writeInt(os,len); + os.write(bytes); + return true; + } catch (IOException e) { + e.printStackTrace(); + } + return false; + } +} diff --git a/src/main/java/com/github/jie65535/opencommand/socket/packet/BasePacket.java b/src/main/java/com/github/jie65535/opencommand/socket/packet/BasePacket.java new file mode 100644 index 0000000..a04d977 --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/packet/BasePacket.java @@ -0,0 +1,8 @@ +package com.github.jie65535.opencommand.socket.packet; + +// 基本数据包 +public abstract class BasePacket { + public abstract String getPacket(); + + public abstract PacketEnum getType(); +} diff --git a/src/main/java/com/github/jie65535/opencommand/socket/packet/HeartBeat.java b/src/main/java/com/github/jie65535/opencommand/socket/packet/HeartBeat.java new file mode 100644 index 0000000..d109e40 --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/packet/HeartBeat.java @@ -0,0 +1,22 @@ +package com.github.jie65535.opencommand.socket.packet; + +import emu.grasscutter.Grasscutter; + +// 心跳包 +public class HeartBeat extends BasePacket { + public String ping; + + public HeartBeat(String ping) { + this.ping = ping; + } + + @Override + public String getPacket() { + return Grasscutter.getGsonFactory().toJson(this); + } + + @Override + public PacketEnum getType() { + return PacketEnum.HeartBeat; + } +} diff --git a/src/main/java/com/github/jie65535/opencommand/socket/packet/HttpPacket.java b/src/main/java/com/github/jie65535/opencommand/socket/packet/HttpPacket.java new file mode 100644 index 0000000..5cc5a46 --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/packet/HttpPacket.java @@ -0,0 +1,36 @@ +package com.github.jie65535.opencommand.socket.packet; + +import emu.grasscutter.Grasscutter; + +// http返回数据 +public class HttpPacket extends BasePacket { + public int code; + public String message; + public String data; + + public HttpPacket(int code, String message, String data) { + this.code = code; + this.message = message; + this.data = data; + } + + public HttpPacket(int code, String message) { + this.code = code; + this.message = message; + } + + @Override + public String getPacket() { + return Grasscutter.getGsonFactory().toJson(this); + } + + @Override + public PacketEnum getType() { + return PacketEnum.HttpPacket; + } + + @Override + public String toString() { + return "HttpPacket [code=" + code + ", message=" + message + ", data=" + data + "]"; + } +} diff --git a/src/main/java/com/github/jie65535/opencommand/socket/packet/Packet.java b/src/main/java/com/github/jie65535/opencommand/socket/packet/Packet.java new file mode 100644 index 0000000..bc45524 --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/packet/Packet.java @@ -0,0 +1,14 @@ +package com.github.jie65535.opencommand.socket.packet; + +// 数据包结构 +public class Packet { + public String token; + public PacketEnum type; + public String data; + public String packetID; + + @Override + public String toString() { + return "Packet [token=" + token + ", type=" + type + ", data=" + data + ", packetID=" + packetID + "]"; + } +} diff --git a/src/main/java/com/github/jie65535/opencommand/socket/packet/PacketEnum.java b/src/main/java/com/github/jie65535/opencommand/socket/packet/PacketEnum.java new file mode 100644 index 0000000..128be71 --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/packet/PacketEnum.java @@ -0,0 +1,9 @@ +package com.github.jie65535.opencommand.socket.packet; + +// 数据包类型列表 +public enum PacketEnum { + PlayerList, + Player, + HttpPacket, + HeartBeat +} diff --git a/src/main/java/com/github/jie65535/opencommand/socket/packet/player/Player.java b/src/main/java/com/github/jie65535/opencommand/socket/packet/player/Player.java new file mode 100644 index 0000000..ee07fd8 --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/packet/player/Player.java @@ -0,0 +1,31 @@ +package com.github.jie65535.opencommand.socket.packet.player; + +import com.github.jie65535.opencommand.socket.SocketServer; +import com.github.jie65535.opencommand.socket.packet.BasePacket; +import com.github.jie65535.opencommand.socket.packet.PacketEnum; +import emu.grasscutter.Grasscutter; + +// 玩家操作类 +public class Player extends BasePacket { + public PlayerEnum type; + public int uid; + public String data; + + @Override + public String getPacket() { + return Grasscutter.getGsonFactory().toJson(this); + } + + @Override + public PacketEnum getType() { + return PacketEnum.Player; + } + + public static void dropMessage(int uid, String str) { + Player p = new Player(); + p.type = PlayerEnum.DropMessage; + p.uid = uid; + p.data = str; + SocketServer.sendAllPacket(p); + } +} diff --git a/src/main/java/com/github/jie65535/opencommand/socket/packet/player/PlayerEnum.java b/src/main/java/com/github/jie65535/opencommand/socket/packet/player/PlayerEnum.java new file mode 100644 index 0000000..155d45e --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/packet/player/PlayerEnum.java @@ -0,0 +1,7 @@ +package com.github.jie65535.opencommand.socket.packet.player; + +// 玩家操作列表 +public enum PlayerEnum { + DropMessage, + RunCommand +} \ No newline at end of file diff --git a/src/main/java/com/github/jie65535/opencommand/socket/packet/player/PlayerList.java b/src/main/java/com/github/jie65535/opencommand/socket/packet/player/PlayerList.java new file mode 100644 index 0000000..a2782dc --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/packet/player/PlayerList.java @@ -0,0 +1,28 @@ +package com.github.jie65535.opencommand.socket.packet.player; + +import com.github.jie65535.opencommand.socket.packet.BasePacket; +import com.github.jie65535.opencommand.socket.packet.PacketEnum; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.game.player.Player; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +// 玩家列表信息 +public class PlayerList extends BasePacket { + public int player = -1; + public List playerList = new ArrayList<>(); + public Map playerMap = new HashMap<>(); + + @Override + public String getPacket() { + return Grasscutter.getGsonFactory().toJson(this); + } + + @Override + public PacketEnum getType() { + return PacketEnum.PlayerList; + } +} From 8b91e50d029d4fcc58f7131c82bea9ec23d87575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B9=E5=9D=97=E5=90=9B?= Date: Thu, 21 Jul 2022 20:30:26 +0800 Subject: [PATCH 02/14] Update README --- README.md | 8 +++++++- README_en-US.md | 5 ++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6337adf..63537ac 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,13 @@ // 临时令牌过期时间(秒) "tempTokenExpirationTime_S": 300, // 授权令牌最后使用过期时间(小时) - "tokenLastUseExpirationTime_H": 48 + "tokenLastUseExpirationTime_H": 48, + // 多服务器通信端口 + "socketPort": 5746, + // 多服务器通信密钥 + "socketToken": "", + // 多服务器Dispatch地址 + "socketHost": "127.0.0.1" } ``` diff --git a/README_en-US.md b/README_en-US.md index 6997b53..9722184 100644 --- a/README_en-US.md +++ b/README_en-US.md @@ -41,7 +41,10 @@ A plugin that opens the GC command execution interface for third-party clients "consoleToken": "", "codeExpirationTime_S": 60, "tempTokenExpirationTime_S": 300, - "tokenLastUseExpirationTime_H": 48 + "tokenLastUseExpirationTime_H": 48, + "socketPort": 5746, + "socketToken": "", + "socketHost": "127.0.0.1" } ``` From 3bc9b0ab14baf23e195dab97290579a50327b376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B9=E5=9D=97=E5=90=9B?= Date: Thu, 21 Jul 2022 20:34:56 +0800 Subject: [PATCH 03/14] Fix client disconnect not clean player list --- .../com/github/jie65535/opencommand/socket/SocketServer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/github/jie65535/opencommand/socket/SocketServer.java b/src/main/java/com/github/jie65535/opencommand/socket/SocketServer.java index 3612cc5..1b41131 100644 --- a/src/main/java/com/github/jie65535/opencommand/socket/SocketServer.java +++ b/src/main/java/com/github/jie65535/opencommand/socket/SocketServer.java @@ -91,6 +91,7 @@ public class SocketServer { mLogger.info("Client {} timeout, disconnect.", clientID); clientList.remove(clientID); clientTimeout.remove(clientID); + SocketData.playerList.remove(clientID); } else { clientTimeout.put(clientID, clientTime + 500); } From 67f3eb180d94203b6073390e15c95513a8f64055 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B9=E5=9D=97=E5=90=9B?= Date: Thu, 21 Jul 2022 21:17:24 +0800 Subject: [PATCH 04/14] Change return type --- .../com/github/jie65535/opencommand/socket/SocketData.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/github/jie65535/opencommand/socket/SocketData.java b/src/main/java/com/github/jie65535/opencommand/socket/SocketData.java index 74affcd..c9cfb64 100644 --- a/src/main/java/com/github/jie65535/opencommand/socket/SocketData.java +++ b/src/main/java/com/github/jie65535/opencommand/socket/SocketData.java @@ -9,10 +9,10 @@ import java.util.concurrent.atomic.AtomicReference; public class SocketData { public static HashMap playerList = new HashMap<>(); - public static PlayerList getPlayer(int uid) { + public static String getPlayer(int uid) { for (PlayerList player : playerList.values()) { if (player.playerMap.get(uid) != null) { - return player; + return player.playerMap.get(uid); } } return null; From 8030ef8034316068a2205eb9298fdfa464a96b50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B9=E5=9D=97=E5=90=9B?= Date: Fri, 22 Jul 2022 14:44:22 +0800 Subject: [PATCH 05/14] Prevent some exceptions --- .../opencommand/socket/SocketClient.java | 80 ++++++++++--------- .../opencommand/socket/SocketServer.java | 54 +++++++------ 2 files changed, 73 insertions(+), 61 deletions(-) diff --git a/src/main/java/com/github/jie65535/opencommand/socket/SocketClient.java b/src/main/java/com/github/jie65535/opencommand/socket/SocketClient.java index cf0fc14..3f63331 100644 --- a/src/main/java/com/github/jie65535/opencommand/socket/SocketClient.java +++ b/src/main/java/com/github/jie65535/opencommand/socket/SocketClient.java @@ -96,49 +96,53 @@ public class SocketClient { public void run() { //noinspection InfiniteLoopStatement while (true) { - String data = SocketUtils.readString(is); - Packet packet = Grasscutter.getGsonFactory().fromJson(data, Packet.class); - if (packet.token.equals(token)) { - switch (packet.type) { - // 玩家类 - case Player: - var player = Grasscutter.getGsonFactory().fromJson(packet.data, Player.class); - switch (player.type) { - // 运行命令 - case RunCommand -> { - var command = player.data; - var playerData = OpenCommandPlugin.getInstance().getServer().getPlayerByUid(player.uid); - if (playerData == null) { - sendPacket(new HttpPacket(404, "Player not found."), packet.packetID); - return; - } - // Player MessageHandler do not support concurrency - //noinspection SynchronizationOnLocalVariableOrMethodParameter - synchronized (playerData) { - try { - var resultCollector = new MessageHandler(); - playerData.setMessageHandler(resultCollector); - CommandMap.getInstance().invoke(playerData, playerData, command); - sendPacket(new HttpPacket(200, resultCollector.getMessage()), packet.packetID); - } catch (Exception e) { - OpenCommandPlugin.getInstance().getLogger().warn("Run command failed.", e); - sendPacket(new HttpPacket(500, "error", e.getLocalizedMessage()), packet.packetID); - } finally { - playerData.setMessageHandler(null); + try { + String data = SocketUtils.readString(is); + Packet packet = Grasscutter.getGsonFactory().fromJson(data, Packet.class); + if (packet.token.equals(token)) { + switch (packet.type) { + // 玩家类 + case Player: + var player = Grasscutter.getGsonFactory().fromJson(packet.data, Player.class); + switch (player.type) { + // 运行命令 + case RunCommand -> { + var command = player.data; + var playerData = OpenCommandPlugin.getInstance().getServer().getPlayerByUid(player.uid); + if (playerData == null) { + sendPacket(new HttpPacket(404, "Player not found."), packet.packetID); + return; + } + // Player MessageHandler do not support concurrency + //noinspection SynchronizationOnLocalVariableOrMethodParameter + synchronized (playerData) { + try { + var resultCollector = new MessageHandler(); + playerData.setMessageHandler(resultCollector); + CommandMap.getInstance().invoke(playerData, playerData, command); + sendPacket(new HttpPacket(200, resultCollector.getMessage()), packet.packetID); + } catch (Exception e) { + OpenCommandPlugin.getInstance().getLogger().warn("Run command failed.", e); + sendPacket(new HttpPacket(500, "error", e.getLocalizedMessage()), packet.packetID); + } finally { + playerData.setMessageHandler(null); + } } } - } - // 发送信息 - case DropMessage -> { - var playerData = OpenCommandPlugin.getInstance().getServer().getPlayerByUid(player.uid); - if (playerData == null) { - return; + // 发送信息 + case DropMessage -> { + var playerData = OpenCommandPlugin.getInstance().getServer().getPlayerByUid(player.uid); + if (playerData == null) { + return; + } + playerData.dropMessage(player.data); } - playerData.dropMessage(player.data); } - } - break; + break; + } } + } catch (Throwable e) { + e.printStackTrace(); } } } diff --git a/src/main/java/com/github/jie65535/opencommand/socket/SocketServer.java b/src/main/java/com/github/jie65535/opencommand/socket/SocketServer.java index 1b41131..b602a87 100644 --- a/src/main/java/com/github/jie65535/opencommand/socket/SocketServer.java +++ b/src/main/java/com/github/jie65535/opencommand/socket/SocketServer.java @@ -24,7 +24,6 @@ public class SocketServer { private static final HashMap clientList = new HashMap<>(); private static final HashMap clientTimeout = new HashMap<>(); - private static Logger mLogger; public static void startServer() throws IOException { @@ -145,31 +144,40 @@ public class SocketServer { public void run() { // noinspection InfiniteLoopStatement while (true) { - String data = SocketUtils.readString(is); - Packet packet = Grasscutter.getGsonFactory().fromJson(data, Packet.class); - if (packet.token.equals(token)) { - switch (packet.type) { - // 缓存玩家列表 - case PlayerList -> { - PlayerList playerList = Grasscutter.getGsonFactory().fromJson(packet.data, PlayerList.class); - SocketData.playerList.put(address, playerList); - } - // Http信息返回 - case HttpPacket -> { - HttpPacket httpPacket = Grasscutter.getGsonFactory().fromJson(packet.data, HttpPacket.class); - var socketWait = socketDataWaitList.get(packet.packetID); - if (socketWait == null) { - mLogger.error("HttpPacket: " + packet.packetID + " not found"); - return; + try { + String data = SocketUtils.readString(is); + Packet packet = Grasscutter.getGsonFactory().fromJson(data, Packet.class); + if (packet.token.equals(token)) { + switch (packet.type) { + // 缓存玩家列表 + case PlayerList -> { + PlayerList playerList = Grasscutter.getGsonFactory().fromJson(packet.data, PlayerList.class); + SocketData.playerList.put(address, playerList); + } + // Http信息返回 + case HttpPacket -> { + HttpPacket httpPacket = Grasscutter.getGsonFactory().fromJson(packet.data, HttpPacket.class); + var socketWait = socketDataWaitList.get(packet.packetID); + if (socketWait == null) { + mLogger.error("HttpPacket: " + packet.packetID + " not found"); + return; + } + socketWait.setData(httpPacket); + socketDataWaitList.remove(packet.packetID); + } + // 心跳包 + case HeartBeat -> { + clientTimeout.put(address, 0); } - socketWait.setData(httpPacket); - socketDataWaitList.remove(packet.packetID); - } - // 心跳包 - case HeartBeat -> { - clientTimeout.put(address, 0); } } + } catch (Throwable e) { + e.printStackTrace(); + mLogger.error("[OpenCommand] Client {} disconnect.", address); + clientList.remove(address); + clientTimeout.remove(address); + SocketData.playerList.remove(address); + break; } } } From e62b1d49673d69ada5d41c216728258d4758ac81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B9=E5=9D=97=E5=90=9B?= Date: Fri, 22 Jul 2022 21:21:12 +0800 Subject: [PATCH 06/14] Change the server authentication method --- .../opencommand/socket/SocketClient.java | 114 ++++++++++-------- .../opencommand/socket/SocketServer.java | 73 ++++++----- .../opencommand/socket/packet/AuthPacket.java | 21 ++++ .../opencommand/socket/packet/PacketEnum.java | 1 + 4 files changed, 130 insertions(+), 79 deletions(-) create mode 100644 src/main/java/com/github/jie65535/opencommand/socket/packet/AuthPacket.java diff --git a/src/main/java/com/github/jie65535/opencommand/socket/SocketClient.java b/src/main/java/com/github/jie65535/opencommand/socket/SocketClient.java index 3f63331..e07ecba 100644 --- a/src/main/java/com/github/jie65535/opencommand/socket/SocketClient.java +++ b/src/main/java/com/github/jie65535/opencommand/socket/SocketClient.java @@ -2,10 +2,7 @@ package com.github.jie65535.opencommand.socket; import com.github.jie65535.opencommand.OpenCommandConfig; import com.github.jie65535.opencommand.OpenCommandPlugin; -import com.github.jie65535.opencommand.socket.packet.BasePacket; -import com.github.jie65535.opencommand.socket.packet.HeartBeat; -import com.github.jie65535.opencommand.socket.packet.HttpPacket; -import com.github.jie65535.opencommand.socket.packet.Packet; +import com.github.jie65535.opencommand.socket.packet.*; import com.github.jie65535.opencommand.socket.packet.player.Player; import emu.grasscutter.Grasscutter; import emu.grasscutter.command.CommandMap; @@ -29,8 +26,11 @@ public class SocketClient { public static boolean connect = false; + public static ReceiveThread receiveThread; + // 连接服务器 public static void connectServer() { + if (connect) return; OpenCommandConfig config = OpenCommandPlugin.getInstance().getConfig(); mLogger = OpenCommandPlugin.getInstance().getLogger(); clientThread = new ClientThread(config.socketHost, config.socketPort); @@ -80,10 +80,9 @@ public class SocketClient { // 数据包接收 private static class ReceiveThread extends Thread { private InputStream is; - private String token; + private boolean exit = false; public ReceiveThread(Socket socket) { - token = OpenCommandPlugin.getInstance().getConfig().socketToken; try { is = socket.getInputStream(); } catch (IOException e) { @@ -97,55 +96,63 @@ public class SocketClient { //noinspection InfiniteLoopStatement while (true) { try { + if (exit) { + return; + } String data = SocketUtils.readString(is); Packet packet = Grasscutter.getGsonFactory().fromJson(data, Packet.class); - if (packet.token.equals(token)) { - switch (packet.type) { - // 玩家类 - case Player: - var player = Grasscutter.getGsonFactory().fromJson(packet.data, Player.class); - switch (player.type) { - // 运行命令 - case RunCommand -> { - var command = player.data; - var playerData = OpenCommandPlugin.getInstance().getServer().getPlayerByUid(player.uid); - if (playerData == null) { - sendPacket(new HttpPacket(404, "Player not found."), packet.packetID); - return; - } - // Player MessageHandler do not support concurrency - //noinspection SynchronizationOnLocalVariableOrMethodParameter - synchronized (playerData) { - try { - var resultCollector = new MessageHandler(); - playerData.setMessageHandler(resultCollector); - CommandMap.getInstance().invoke(playerData, playerData, command); - sendPacket(new HttpPacket(200, resultCollector.getMessage()), packet.packetID); - } catch (Exception e) { - OpenCommandPlugin.getInstance().getLogger().warn("Run command failed.", e); - sendPacket(new HttpPacket(500, "error", e.getLocalizedMessage()), packet.packetID); - } finally { - playerData.setMessageHandler(null); - } - } + switch (packet.type) { + // 玩家类 + case Player: + var player = Grasscutter.getGsonFactory().fromJson(packet.data, Player.class); + switch (player.type) { + // 运行命令 + case RunCommand -> { + var command = player.data; + var playerData = OpenCommandPlugin.getInstance().getServer().getPlayerByUid(player.uid); + if (playerData == null) { + sendPacket(new HttpPacket(404, "Player not found."), packet.packetID); + return; } - // 发送信息 - case DropMessage -> { - var playerData = OpenCommandPlugin.getInstance().getServer().getPlayerByUid(player.uid); - if (playerData == null) { - return; + // Player MessageHandler do not support concurrency + //noinspection SynchronizationOnLocalVariableOrMethodParameter + synchronized (playerData) { + try { + var resultCollector = new MessageHandler(); + playerData.setMessageHandler(resultCollector); + CommandMap.getInstance().invoke(playerData, playerData, command); + sendPacket(new HttpPacket(200, resultCollector.getMessage()), packet.packetID); + } catch (Exception e) { + OpenCommandPlugin.getInstance().getLogger().warn("[OpenCommand] Run command failed.", e); + sendPacket(new HttpPacket(500, "error", e.getLocalizedMessage()), packet.packetID); + } finally { + playerData.setMessageHandler(null); } - playerData.dropMessage(player.data); } } - break; - } + // 发送信息 + case DropMessage -> { + var playerData = OpenCommandPlugin.getInstance().getServer().getPlayerByUid(player.uid); + if (playerData == null) { + return; + } + playerData.dropMessage(player.data); + } + } + break; } } catch (Throwable e) { e.printStackTrace(); + if (!sendPacket(new HeartBeat("Pong"))) { + return; + } } } } + + public void exit() { + exit = true; + } } // 客户端连接线程 @@ -172,17 +179,26 @@ public class SocketClient { @Override public void run() { try { - socket = new Socket(ip, port); connect = true; + if (receiveThread != null) { + receiveThread.exit(); + } + + socket = new Socket(ip, port); os = socket.getOutputStream(); - mLogger.info("Connect to server: " + ip + ":" + port); - new ReceiveThread(socket); + mLogger.info("[OpenCommand]Connect to server: " + ip + ":" + port); + SocketClient.sendPacket(new AuthPacket(OpenCommandPlugin.getInstance().getConfig().socketToken)); + receiveThread = new ReceiveThread(socket); } catch (IOException e) { connect = false; - mLogger.warn("Connect to server failed: " + ip + ":" + port); - mLogger.warn("[OpenCommand] Reconnect to server"); + mLogger.warn("[OpenCommand]Connect to server failed: " + ip + ":" + port); + mLogger.warn("[OpenCommand] Retry connecting to the server after 15 seconds"); + try { + Thread.sleep(15000); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } connectServer(); - throw new RuntimeException(e); } } } diff --git a/src/main/java/com/github/jie65535/opencommand/socket/SocketServer.java b/src/main/java/com/github/jie65535/opencommand/socket/SocketServer.java index b602a87..fa36092 100644 --- a/src/main/java/com/github/jie65535/opencommand/socket/SocketServer.java +++ b/src/main/java/com/github/jie65535/opencommand/socket/SocketServer.java @@ -1,9 +1,7 @@ package com.github.jie65535.opencommand.socket; import com.github.jie65535.opencommand.OpenCommandPlugin; -import com.github.jie65535.opencommand.socket.packet.BasePacket; -import com.github.jie65535.opencommand.socket.packet.HttpPacket; -import com.github.jie65535.opencommand.socket.packet.Packet; +import com.github.jie65535.opencommand.socket.packet.*; import com.github.jie65535.opencommand.socket.packet.player.PlayerList; import emu.grasscutter.Grasscutter; import org.slf4j.Logger; @@ -87,7 +85,7 @@ public class SocketServer { var clientID = client.getKey(); var clientTime = client.getValue(); if (clientTime > TIMEOUT) { - mLogger.info("Client {} timeout, disconnect.", clientID); + mLogger.info("[OpenCommand] Client {} timeout, disconnect.", clientID); clientList.remove(clientID); clientTimeout.remove(clientID); SocketData.playerList.remove(clientID); @@ -105,6 +103,7 @@ public class SocketServer { private OutputStream os; private final String address; private final String token; + private boolean auth = false; private final HashMap> socketDataWaitList = new HashMap<>(); @@ -147,28 +146,44 @@ public class SocketServer { try { String data = SocketUtils.readString(is); Packet packet = Grasscutter.getGsonFactory().fromJson(data, Packet.class); - if (packet.token.equals(token)) { - switch (packet.type) { - // 缓存玩家列表 - case PlayerList -> { - PlayerList playerList = Grasscutter.getGsonFactory().fromJson(packet.data, PlayerList.class); - SocketData.playerList.put(address, playerList); - } - // Http信息返回 - case HttpPacket -> { - HttpPacket httpPacket = Grasscutter.getGsonFactory().fromJson(packet.data, HttpPacket.class); - var socketWait = socketDataWaitList.get(packet.packetID); - if (socketWait == null) { - mLogger.error("HttpPacket: " + packet.packetID + " not found"); - return; - } - socketWait.setData(httpPacket); - socketDataWaitList.remove(packet.packetID); - } - // 心跳包 - case HeartBeat -> { - clientTimeout.put(address, 0); + if (packet.type == PacketEnum.AuthPacket) { + AuthPacket authPacket = Grasscutter.getGsonFactory().fromJson(data, AuthPacket.class); + if (authPacket.token.equals(token)) { + auth = true; + mLogger.info("[OpenCommand] Client {} auth success", address); + clientList.put(address, this); + clientTimeout.put(address, 0); + } else { + mLogger.warn("[OpenCommand] Client {} auth failed", address); + socket.close(); + return; + } + } + if (!auth) { + mLogger.warn("[OpenCommand] Client {} auth failed", address); + socket.close(); + return; + } + switch (packet.type) { + // 缓存玩家列表 + case PlayerList -> { + PlayerList playerList = Grasscutter.getGsonFactory().fromJson(packet.data, PlayerList.class); + SocketData.playerList.put(address, playerList); + } + // Http信息返回 + case HttpPacket -> { + HttpPacket httpPacket = Grasscutter.getGsonFactory().fromJson(packet.data, HttpPacket.class); + var socketWait = socketDataWaitList.get(packet.packetID); + if (socketWait == null) { + mLogger.error("[OpenCommand] HttpPacket: " + packet.packetID + " not found"); + return; } + socketWait.setData(httpPacket); + socketDataWaitList.remove(packet.packetID); + } + // 心跳包 + case HeartBeat -> { + clientTimeout.put(address, 0); } } } catch (Throwable e) { @@ -194,16 +209,14 @@ public class SocketServer { @Override public void run() { - mLogger.info("Start socket server on port " + socketServer.getLocalPort()); + mLogger.info("[OpenCommand] Start socket server on port " + socketServer.getLocalPort()); // noinspection InfiniteLoopStatement while (true) { try { Socket accept = socketServer.accept(); String address = accept.getInetAddress() + ":" + accept.getPort(); - mLogger.info("Client connect: " + address); - ClientThread clientThread = new ClientThread(accept); - clientList.put(address, clientThread); - clientTimeout.put(address, 0); + mLogger.info("[OpenCommand] Client connect: " + address); + new ClientThread(accept); } catch (IOException e) { e.printStackTrace(); } diff --git a/src/main/java/com/github/jie65535/opencommand/socket/packet/AuthPacket.java b/src/main/java/com/github/jie65535/opencommand/socket/packet/AuthPacket.java new file mode 100644 index 0000000..d244018 --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/packet/AuthPacket.java @@ -0,0 +1,21 @@ +package com.github.jie65535.opencommand.socket.packet; + +import emu.grasscutter.Grasscutter; + +public class AuthPacket extends BasePacket { + public String token; + + public AuthPacket(String token) { + this.token = token; + } + + @Override + public String getPacket() { + return Grasscutter.getGsonFactory().toJson(this); + } + + @Override + public PacketEnum getType() { + return PacketEnum.AuthPacket; + } +} diff --git a/src/main/java/com/github/jie65535/opencommand/socket/packet/PacketEnum.java b/src/main/java/com/github/jie65535/opencommand/socket/packet/PacketEnum.java index 128be71..dadb1ff 100644 --- a/src/main/java/com/github/jie65535/opencommand/socket/packet/PacketEnum.java +++ b/src/main/java/com/github/jie65535/opencommand/socket/packet/PacketEnum.java @@ -5,5 +5,6 @@ public enum PacketEnum { PlayerList, Player, HttpPacket, + AuthPacket, HeartBeat } From a4048d4fd10f3afa646e3e90df3a7fe3d79d2ffd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B9=E5=9D=97=E5=90=9B?= Date: Fri, 22 Jul 2022 22:08:02 +0800 Subject: [PATCH 07/14] Add auto sending player list --- .../opencommand/socket/SocketClient.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/main/java/com/github/jie65535/opencommand/socket/SocketClient.java b/src/main/java/com/github/jie65535/opencommand/socket/SocketClient.java index e07ecba..692092b 100644 --- a/src/main/java/com/github/jie65535/opencommand/socket/SocketClient.java +++ b/src/main/java/com/github/jie65535/opencommand/socket/SocketClient.java @@ -4,6 +4,7 @@ import com.github.jie65535.opencommand.OpenCommandConfig; import com.github.jie65535.opencommand.OpenCommandPlugin; import com.github.jie65535.opencommand.socket.packet.*; import com.github.jie65535.opencommand.socket.packet.player.Player; +import com.github.jie65535.opencommand.socket.packet.player.PlayerList; import emu.grasscutter.Grasscutter; import emu.grasscutter.command.CommandMap; import emu.grasscutter.utils.MessageHandler; @@ -13,6 +14,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; +import java.util.ArrayList; import java.util.Timer; import java.util.TimerTask; @@ -40,6 +42,7 @@ public class SocketClient { } timer = new Timer(); timer.schedule(new SendHeartBeatPacket(), 500); + timer.schedule(new SendPlayerListPacket(), 1000); } // 发送数据包 @@ -77,6 +80,23 @@ public class SocketClient { } } + private static class SendPlayerListPacket extends TimerTask { + @Override + public void run() { + if (connect) { + PlayerList playerList = new PlayerList(); + playerList.player = Grasscutter.getGameServer().getPlayers().size(); + ArrayList playerNames = new ArrayList<>(); + for (emu.grasscutter.game.player.Player player : Grasscutter.getGameServer().getPlayers().values()) { + playerNames.add(player.getNickname()); + playerList.playerMap.put(player.getUid(), player.getNickname()); + } + playerList.playerList = playerNames; + sendPacket(playerList); + } + } + } + // 数据包接收 private static class ReceiveThread extends Thread { private InputStream is; From 37dcd0f1a730fce01a2d296d80bb762ee1c569f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B9=E5=9D=97=E5=90=9B?= Date: Fri, 22 Jul 2022 23:03:25 +0800 Subject: [PATCH 08/14] Update README --- README.md | 94 +++++++++++++++++++++++++++++++++++++--------- README_en-US.md | 99 ++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 154 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 63537ac..3599114 100644 --- a/README.md +++ b/README.md @@ -88,26 +88,84 @@ public final class JsonResponse { ``` ### Actions 动作 -#### `ping` -data = null +#### `测试连接` -#### `sendCode` ##### Request -data = uid (int) -##### Response -data = token (string) -#### `verify` 要求 `token` -##### Request -data = code (int) -##### Response -###### Success: -code = 200 -###### Verification failed: -code = 400 +| 请求参数 | 请求数据 | 类型 | +| ------- | --------- | ------ | +| action | `ping` |`String`| -#### `command` 要求 `token` -##### Request -data = command (string) ##### Response -data = message (string) + +| 返回参数 | 返回数据 | 类型 | +| ------- | --------- | ------ | +| retcode | `200` |`String`| +| message | `success` |`String`| +| data | `null` |`null` | + +#### `发送验证码` + +##### Request + +| 请求参数 | 请求数据 | 类型 | +| ------- | --------- | ------ | +| action | `sendCode`|`String`| +| data | `uid` |`Int` | + +##### Response + +| 返回参数 | 返回数据 | 类型 | +| ------- | --------- | ------ | +| retcode | `200` |`String`| +| message | `success` |`String`| +| data | `token` |`String`| + + +#### `验证验证码` + +##### Request + +| 请求参数 | 请求数据 | 类型 | +| ------- | --------- | ------ | +| action | `verify` |`String`| +| token | `token` |`String`| +| data | `code` |`Int` | + +##### Response + +成功 + +| 返回参数 | 返回数据 | 类型 | +| ------- | --------- | ------ | +| retcode | `200` |`String`| +| message | `success` |`String`| +| data | `null` | `null` | + +失败 + +| 返回参数 | 返回数据 | 类型 | +| ------- | -------------------- | ------ | +| retcode | `400` |`String`| +| message | `Verification failed`|`String`| +| data | `null` |`null` | + +#### `执行命令` + +##### Request + +| 请求参数 | 请求数据 | 类型 | +| ------- | ----------- | ------ | +| action | `command` |`String`| +| token | `token` |`String`| +| data | `command` |`String`| + +##### Response + +成功 + +| 返回参数 | 返回数据 | 类型 | +| ------- | ---------------- | ------ | +| retcode | `200` |`String`| +| message | `success` |`String`| +| data | `Command return` |`String`| \ No newline at end of file diff --git a/README_en-US.md b/README_en-US.md index 9722184..a1369db 100644 --- a/README_en-US.md +++ b/README_en-US.md @@ -73,27 +73,84 @@ public final class JsonResponse { } ``` -## Actions -### `ping` -data = null +### Actions +#### `Test connect` -### `sendCode` -#### Request -data = uid (int) -#### Response -data = token (string) +##### Request -### `verify`: Requires `token` -#### Request -data = code (int) -#### Response -##### Success: -code = 200 -##### Verification failed: -code = 400 +| Request | Request data | type | +| ------- | -------------- | ------ | +| action | `ping` |`String`| -### `command`: Requires `token` -#### Request -data = command (string) -#### Response -data = message (string) +##### Response + +| Response | Response data | type | +| -------- | --------------- | ------ | +| retcode | `200` |`String`| +| message | `success` |`String`| +| data | `null` |`null` | + +#### `Send code` + +##### Request + +| Request | Request data | type | +| ------- | -------------- | ------ | +| action | `sendCode` |`String`| +| data | `uid` |`Int` | + +##### Response + +| Response | Response data | type | +| -------- | --------------- | ------ | +| retcode | `200` |`String`| +| message | `success` |`String`| +| data | `token` |`String`| + +#### `Verify code` + +##### Request + +| Request | Request data | type | +| ------- | -------------- | ------ | +| action | `verify` |`String`| +| token | `token` |`String`| +| data | `code` |`Int` | + +##### Response + +Success + +| Response | Response data | type | +| -------- | -------------- | ------ | +| retcode | `200` |`String`| +| message | `success` |`String`| +| data | `null` | `null` | + +Failed + +| Response | Response data | type | +| -------- | -------------------- | ------ | +| retcode | `400` |`String`| +| message | `Verification failed`|`String`| +| data | `null` |`null` | + +#### `Run command` + +##### Request + +| Request | Request data | type | +| ------- | -------------- | ------ | +| action | `command` |`String`| +| token | `token` |`String`| +| data | `command` |`String`| + +##### Response + +Success + +| Response | Response data | type | +| -------- | ------------------ | ------ | +| retcode | `200` |`String`| +| message | `success` |`String`| +| data | `Command return` |`String`| \ No newline at end of file From 4d08acd084ccae98b1a524f6e2237fc9b9f07e6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B9=E5=9D=97=E5=90=9B?= Date: Tue, 26 Jul 2022 19:14:49 +0800 Subject: [PATCH 09/14] Add multi server run console command --- README.md | 195 +++++++++++++----- README_en-US.md | 176 ++++++++++++---- .../jie65535/opencommand/EventListeners.java | 2 + .../opencommand/OpenCommandConfig.java | 1 + .../opencommand/OpenCommandHandler.java | 10 + .../OpenCommandOnlyHttpHandler.java | 56 +++-- .../opencommand/OpenCommandPlugin.java | 15 +- .../opencommand/json/JsonRequest.java | 1 + .../opencommand/json/JsonResponse.java | 2 +- .../opencommand/socket/ClientInfo.java | 14 ++ .../opencommand/socket/SocketClient.java | 39 +++- .../opencommand/socket/SocketData.java | 18 ++ .../opencommand/socket/SocketServer.java | 54 ++++- .../opencommand/socket/SocketUtils.java | 34 +-- .../opencommand/socket/packet/AuthPacket.java | 9 +- .../opencommand/socket/packet/HttpPacket.java | 7 +- .../opencommand/socket/packet/Packet.java | 3 +- .../opencommand/socket/packet/PacketEnum.java | 1 + .../socket/packet/RunConsoleCommand.java | 21 ++ 19 files changed, 509 insertions(+), 149 deletions(-) create mode 100644 src/main/java/com/github/jie65535/opencommand/socket/ClientInfo.java create mode 100644 src/main/java/com/github/jie65535/opencommand/socket/packet/RunConsoleCommand.java diff --git a/README.md b/README.md index 3599114..0a53932 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,10 @@ 一个为第三方客户端开放GC命令执行接口的插件 ## 服务端安装 + 1. 在 [Release](https://github.com/jie65535/gc-opencommand-plugin/releases) 下载 `jar` 2. 放入 `plugins` 文件夹即可 + > 注意,如果出现以下错误: > ```log > INFO:PluginManager Enabling plugin: opencommand-plugin @@ -17,24 +19,28 @@ > 请使用v1.2.1版本插件,因为该报错表示你的服务端是旧版! ## 控制台连接 + 1. 首次启动时,会在 `plugins` 目录下生成一个 `opencommand-plugin` 目录,打开并编辑 `config.json` 2. 设置 `consoleToken` 的值为你的连接秘钥,建议使用至少32字符的长随机字符串。 3. 重新启动服务端即可生效配置 4. 在客户端中选择控制台身份,并填写你的 `consoleToken` 即可以控制台身份运行指令 ## 构建说明 + 1. 克隆仓库 2. 在目录下新建 `lib` 目录 3. 将 `grasscutter-1.1.x-dev.jar` 放入 `lib` 目录 4. `gradle build` ## 玩家使用流程 + 1. 在客户端中填写服务地址,确认是否支持 2. 填写UID,发送验证码 3. 将游戏内收到的**4位整数验证码**填入客户端校验 4. 享受便利! ## 客户端请求流程 + 1. `ping` 确认是否支持 `opencommand` 插件 2. `sendCode` 向指定玩家发送验证码(1分钟内不允许重发),保存返回的 `token` 3. 使用 `token` 和**4位整数验证码**发送 `verify` 校验 @@ -43,6 +49,7 @@ --- ## `config.json` + ```json { // 控制台连接令牌 @@ -58,114 +65,210 @@ // 多服务器通信密钥 "socketToken": "", // 多服务器Dispatch地址 - "socketHost": "127.0.0.1" + "socketHost": "127.0.0.1", + // 多服务器显示名称 + "socketDisplayName": "" } ``` - ## API `/opencommand/api` + 示例 + ``` https://127.0.0.1/opencommand/api ``` ### Request 请求 + ```java public final class JsonRequest { public String token = ""; public String action = ""; + public String server = ""; public Object data = null; } ``` ### Response 响应 + ```java public final class JsonResponse { public int retcode = 200; - public String message = "success"; + public String message = "Success"; public Object data; } ``` ### Actions 动作 + #### `测试连接` ##### Request -| 请求参数 | 请求数据 | 类型 | -| ------- | --------- | ------ | -| action | `ping` |`String`| +| 请求参数 | 请求数据 | 类型 | +|--------|--------|----------| +| action | `ping` | `String` | ##### Response -| 返回参数 | 返回数据 | 类型 | -| ------- | --------- | ------ | -| retcode | `200` |`String`| -| message | `success` |`String`| -| data | `null` |`null` | +| 返回参数 | 返回数据 | 类型 | +|---------|-----------|----------| +| retcode | `200` | `Int` | +| message | `Success` | `String` | +| data | `null` | `null` | + + +#### `获取在线玩家` + +##### Request + +| 请求参数 | 请求数据 | 类型 | +|--------|----------|----------| +| action | `online` | `String` | + +##### Response + +| 返回参数 | 返回数据 | 类型 | +|---------|---------------------------------|--------------| +| retcode | `200` | `Int` | +| message | `Success` | `String` | +| data | `{"count": 0, playerList": []}` | `JsonObject` | #### `发送验证码` ##### Request -| 请求参数 | 请求数据 | 类型 | -| ------- | --------- | ------ | -| action | `sendCode`|`String`| -| data | `uid` |`Int` | +| 请求参数 | 请求数据 | 类型 | +|--------|------------|----------| +| action | `sendCode` | `String` | +| data | `uid` | `Int` | ##### Response -| 返回参数 | 返回数据 | 类型 | -| ------- | --------- | ------ | -| retcode | `200` |`String`| -| message | `success` |`String`| -| data | `token` |`String`| - +| 返回参数 | 返回数据 | 类型 | +|---------|-----------|----------| +| retcode | `200` | `Int` | +| message | `Success` | `String` | +| data | `token` | `String` | #### `验证验证码` ##### Request -| 请求参数 | 请求数据 | 类型 | -| ------- | --------- | ------ | -| action | `verify` |`String`| -| token | `token` |`String`| -| data | `code` |`Int` | +| 请求参数 | 请求数据 | 类型 | +|--------|----------|----------| +| action | `verify` | `String` | +| token | `token` | `String` | +| data | `code` | `Int` | ##### Response 成功 -| 返回参数 | 返回数据 | 类型 | -| ------- | --------- | ------ | -| retcode | `200` |`String`| -| message | `success` |`String`| -| data | `null` | `null` | +| 返回参数 | 返回数据 | 类型 | +|---------|-----------|----------| +| retcode | `200` | `Int` | +| message | `Success` | `String` | +| data | `null` | `null` | 失败 -| 返回参数 | 返回数据 | 类型 | -| ------- | -------------------- | ------ | -| retcode | `400` |`String`| -| message | `Verification failed`|`String`| -| data | `null` |`null` | +| 返回参数 | 返回数据 | 类型 | +|---------|-----------------------|----------| +| retcode | `400` | `Int` | +| message | `Verification failed` | `String` | +| data | `null` | `null` | #### `执行命令` ##### Request -| 请求参数 | 请求数据 | 类型 | -| ------- | ----------- | ------ | -| action | `command` |`String`| -| token | `token` |`String`| -| data | `command` |`String`| +| 请求参数 | 请求数据 | 类型 | +|--------|-----------|----------| +| action | `command` | `String` | +| token | `token` | `String` | +| data | `command` | `String` | ##### Response 成功 -| 返回参数 | 返回数据 | 类型 | -| ------- | ---------------- | ------ | -| retcode | `200` |`String`| -| message | `success` |`String`| -| data | `Command return` |`String`| \ No newline at end of file +| 返回参数 | 返回数据 | 类型 | +|---------|------------------|----------| +| retcode | `200` | `Int` | +| message | `Success` | `String` | +| data | `Command return` | `String` | + +### 执行控制台命令 + +#### `获取运行模式` + +##### Request + +| 请求参数 | 请求数据 | 类型 | +|--------|-----------|----------| +| action | `runmode` | `String` | +| token | `token` | `String` | + +##### Response + +成功 + +| 返回参数 | 返回数据 | 类型 | +|---------|-----------------------|----------| +| retcode | `200` | `Int` | +| message | `Success` | `String` | +| data | `1 (多服务器) / 0 (单服务器)` | `Int` | + +#### `获取多服务器列表` + +##### Request + +| 请求参数 | 请求数据 | 类型 | +|--------|----------|----------| +| action | `server` | `String` | +| token | `token` | `String` | + +##### Response + +成功 + +| 返回参数 | 返回数据 | 类型 | +|---------|-----------|--------------| +| retcode | `200` | `Int` | +| message | `Success` | `String` | +| data | `{}` | `JsonObject` | + +```json +{ + "retcode": 200, + "message": "success", + "data": { + // 服务器 UUID + "13d82d0d-c7d9-47dd-830c-76588006ef6e": "2.8.0 服务器", + "e6b83224-a761-4023-be57-e054c5bb823a": "2.8.0 开发服务器" + } +} +``` + +#### `执行命令` + +##### Request + +| 请求参数 | 请求数据 | 类型 | +|--------|-----------|----------| +| action | `command` | `String` | +| token | `token` | `String` | +| server | `UUID` | `String` | +| data | `command` | `String` | + +##### Response + +成功 + +| 返回参数 | 返回数据 | 类型 | +|---------|------------------|----------| +| retcode | `200` | `Int` | +| message | `Success` | `String` | +| data | `Command return` | `String` | \ No newline at end of file diff --git a/README_en-US.md b/README_en-US.md index a1369db..afda1ab 100644 --- a/README_en-US.md +++ b/README_en-US.md @@ -60,6 +60,7 @@ https://127.0.0.1/opencommand/api public final class JsonRequest { public String token = ""; public String action = ""; + public Seting server = ""; public Object data = null; } ``` @@ -68,7 +69,7 @@ public final class JsonRequest { ```java public final class JsonResponse { public int retcode = 200; - public String message = "success"; + public String message = "Success"; public Object data; } ``` @@ -78,79 +79,168 @@ public final class JsonResponse { ##### Request -| Request | Request data | type | -| ------- | -------------- | ------ | -| action | `ping` |`String`| +| Request | Request data | type | +|---------|--------------|----------| +| action | `ping` | `String` | ##### Response -| Response | Response data | type | -| -------- | --------------- | ------ | -| retcode | `200` |`String`| -| message | `success` |`String`| -| data | `null` |`null` | +| Response | Response data | type | +|----------|---------------|----------| +| retcode | `200` | `String` | +| message | `Success` | `String` | +| data | `null` | `null` | + +#### `Get online players` + +##### Request + +| Request | Request data | type | +|---------|--------------|----------| +| action | `online` | `String` | + +##### Response + +| Response | Response data | type | +|----------|---------------------------------|--------------| +| retcode | `200` | `String` | +| message | `Success` | `String` | +| data | `{"count": 0, playerList": []}` | `JsonObject` | #### `Send code` ##### Request -| Request | Request data | type | -| ------- | -------------- | ------ | -| action | `sendCode` |`String`| -| data | `uid` |`Int` | +| Request | Request data | type | +|---------|--------------|----------| +| action | `sendCode` | `String` | +| data | `uid` | `Int` | ##### Response -| Response | Response data | type | -| -------- | --------------- | ------ | -| retcode | `200` |`String`| -| message | `success` |`String`| -| data | `token` |`String`| +| Response | Response data | type | +|----------|---------------|----------| +| retcode | `200` | `String` | +| message | `Success` | `String` | +| data | `token` | `String` | #### `Verify code` ##### Request -| Request | Request data | type | -| ------- | -------------- | ------ | -| action | `verify` |`String`| -| token | `token` |`String`| -| data | `code` |`Int` | +| Request | Request data | type | +|---------|--------------|----------| +| action | `verify` | `String` | +| token | `token` | `String` | +| data | `code` | `Int` | ##### Response Success -| Response | Response data | type | -| -------- | -------------- | ------ | -| retcode | `200` |`String`| -| message | `success` |`String`| -| data | `null` | `null` | +| Response | Response data | type | +|----------|---------------|----------| +| retcode | `200` | `String` | +| message | `Success` | `String` | +| data | `null` | `null` | Failed -| Response | Response data | type | -| -------- | -------------------- | ------ | -| retcode | `400` |`String`| -| message | `Verification failed`|`String`| -| data | `null` |`null` | +| Response | Response data | type | +|----------|-----------------------|----------| +| retcode | `400` | `String` | +| message | `Verification failed` | `String` | +| data | `null` | `null` | #### `Run command` ##### Request -| Request | Request data | type | -| ------- | -------------- | ------ | -| action | `command` |`String`| -| token | `token` |`String`| -| data | `command` |`String`| +| Request | Request data | type | +|---------|--------------|----------| +| action | `command` | `String` | +| token | `token` | `String` | +| data | `command` | `String` | ##### Response Success -| Response | Response data | type | -| -------- | ------------------ | ------ | -| retcode | `200` |`String`| -| message | `success` |`String`| -| data | `Command return` |`String`| \ No newline at end of file +| Response | Response data | type | +|----------|------------------|----------| +| retcode | `200` | `String` | +| message | `Success` | `String` | +| data | `Command return` | `String` | + +### Run console command + +#### `Get run mode` + +##### Request + +| Request | Request data | Type | +|---------|--------------|----------| +| action | `runmode` | `String` | +| token | `token` | `String` | + +##### Response + +Success + +| Request | Response data | Type | +|---------|----------------------------------------|----------| +| retcode | `200` | `Int` | +| message | `Success` | `String` | +| data | `1 (Multi server) / 0 (Single server)` | `Int` | + +#### `Get mulit server list` + +##### Request + +| Request | Request data | Type | +|---------|--------------|----------| +| action | `server` | `String` | +| token | `token` | `String` | + +##### Response + +Success + +| Request | Response data | Type | +|---------|---------------|--------------| +| retcode | `200` | `Int` | +| message | `Success` | `String` | +| data | `{}` | `JsonObject` | + +```json +{ + "retcode": 200, + "message": "success", + "data": { + // Server UUID + "13d82d0d-c7d9-47dd-830c-76588006ef6e": "2.8.0 Server", + "e6b83224-a761-4023-be57-e054c5bb823a": "2.8.0 Dev server" + } +} +``` + +#### `Run command` + +##### Request + +| Request | Request data | Type | +|---------|--------------|----------| +| action | `command` | `String` | +| token | `token` | `String` | +| server | `UUID` | `String` | +| data | `command` | `String` | + +##### Response + +Success + +| Request | Response data | Type | +|---------|------------------|----------| +| retcode | `200` | `Int` | +| message | `Success` | `String` | +| data | `Command return` | `String` | \ No newline at end of file diff --git a/src/main/java/com/github/jie65535/opencommand/EventListeners.java b/src/main/java/com/github/jie65535/opencommand/EventListeners.java index 0201fe9..df27331 100644 --- a/src/main/java/com/github/jie65535/opencommand/EventListeners.java +++ b/src/main/java/com/github/jie65535/opencommand/EventListeners.java @@ -15,9 +15,11 @@ import java.util.ArrayList; public final class EventListeners { private static MessageHandler consoleMessageHandler; + public static void setConsoleMessageHandler(MessageHandler handler) { consoleMessageHandler = handler; } + public static void onCommandResponse(ReceiveCommandFeedbackEvent event) { if (consoleMessageHandler != null && event.getPlayer() == null) { consoleMessageHandler.setMessage(event.getMessage()); diff --git a/src/main/java/com/github/jie65535/opencommand/OpenCommandConfig.java b/src/main/java/com/github/jie65535/opencommand/OpenCommandConfig.java index fad781a..766bb5d 100644 --- a/src/main/java/com/github/jie65535/opencommand/OpenCommandConfig.java +++ b/src/main/java/com/github/jie65535/opencommand/OpenCommandConfig.java @@ -25,4 +25,5 @@ public class OpenCommandConfig { public int socketPort = 5746; public String socketToken = ""; public String socketHost = "127.0.0.1"; + public String socketDisplayName = ""; } diff --git a/src/main/java/com/github/jie65535/opencommand/OpenCommandHandler.java b/src/main/java/com/github/jie65535/opencommand/OpenCommandHandler.java index 13677b3..5b2a92a 100644 --- a/src/main/java/com/github/jie65535/opencommand/OpenCommandHandler.java +++ b/src/main/java/com/github/jie65535/opencommand/OpenCommandHandler.java @@ -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.socket.SocketData; import emu.grasscutter.command.CommandMap; import emu.grasscutter.server.http.Router; import emu.grasscutter.utils.Crypto; @@ -31,6 +32,7 @@ import io.javalin.Javalin; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Map; @@ -85,6 +87,11 @@ public final class OpenCommandHandler implements Router { } else if (req.action.equals("ping")) { response.json(new JsonResponse()); return; + } else if (req.action.equals("online")) { + var p = new ArrayList(); + plugin.getServer().getPlayers().forEach((uid, player) -> p.add(player.getNickname())); + response.json(new JsonResponse(200, "Success", new SocketData.OnlinePlayer(p))); + return; } // token is required @@ -118,6 +125,9 @@ public final class OpenCommandHandler implements Router { } } return; + } else if (req.action.equals("runmode")) { + response.json(new JsonResponse(200, "Success", 0)); + return; } } else if (codes.containsKey(req.token)) { if (req.action.equals("verify")) { diff --git a/src/main/java/com/github/jie65535/opencommand/OpenCommandOnlyHttpHandler.java b/src/main/java/com/github/jie65535/opencommand/OpenCommandOnlyHttpHandler.java index 8530217..db21c84 100644 --- a/src/main/java/com/github/jie65535/opencommand/OpenCommandOnlyHttpHandler.java +++ b/src/main/java/com/github/jie65535/opencommand/OpenCommandOnlyHttpHandler.java @@ -23,6 +23,7 @@ import com.github.jie65535.opencommand.socket.SocketData; import com.github.jie65535.opencommand.socket.SocketDataWait; import com.github.jie65535.opencommand.socket.SocketServer; import com.github.jie65535.opencommand.socket.packet.HttpPacket; +import com.github.jie65535.opencommand.socket.packet.RunConsoleCommand; import com.github.jie65535.opencommand.socket.packet.player.Player; import com.github.jie65535.opencommand.socket.packet.player.PlayerEnum; import emu.grasscutter.command.CommandMap; @@ -92,6 +93,9 @@ public final class OpenCommandOnlyHttpHandler implements Router { } else if (req.action.equals("ping")) { response.json(new JsonResponse()); return; + } else if (req.action.equals("online")) { + response.json(new JsonResponse(200, "Success", SocketData.getOnlinePlayer())); + return; } // token is required @@ -110,20 +114,40 @@ public final class OpenCommandOnlyHttpHandler implements Router { response.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", request.ip(), req.data)); - var resultCollector = new MessageHandler(); - EventListeners.setConsoleMessageHandler(resultCollector); - CommandMap.getInstance().invoke(null, null, req.data.toString()); - response.json(new JsonResponse(resultCollector.getMessage())); - } catch (Exception e) { - plugin.getLogger().warn("Run command failed.", e); - EventListeners.setConsoleMessageHandler(null); - response.json(new JsonResponse(500, "error", e.getLocalizedMessage())); - } + var server = SocketServer.getClientInfoByUuid(req.server); + if (server == null) { + response.json(new JsonResponse(404, "Server Not Found.")); + return; } + plugin.getLogger().info(String.format("IP: %s run command in console > %s", request.ip(), req.data)); + var wait = new SocketDataWait(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 data = wait.getData(); + if (data == null) { + response.json(new JsonResponse(408, "Timeout")); + return; + } + response.json(new JsonResponse(data.code, data.message, data.data)); + return; + } else if (req.action.equals("server")) { + response.json(new JsonResponse(200, "Success", SocketServer.getOnlineClient())); + return; + } else if (req.action.equals("runmode")) { + response.json(new JsonResponse(200, "Success", 1)); return; } } else if (codes.containsKey(req.token)) { @@ -143,7 +167,8 @@ public final class OpenCommandOnlyHttpHandler implements Router { if (req.action.equals("command")) { SocketDataWait socketDataWait = new SocketDataWait<>(1000L * 10L) { @Override - public void run() {} + public void run() { + } @Override public HttpPacket initData(HttpPacket data) { @@ -152,7 +177,6 @@ public final class OpenCommandOnlyHttpHandler implements Router { @Override public void timeout() { - response.json(new JsonResponse(408, "Wait server timeout")); } }; @@ -173,7 +197,7 @@ public final class OpenCommandOnlyHttpHandler implements Router { HttpPacket httpPacket = socketDataWait.getData(); if (httpPacket == null) { - response.json(new JsonResponse(500, "error", "Server connect failed.")); + response.json(new JsonResponse(500, "error", "Wait timeout")); return; } response.json(new JsonResponse(httpPacket.code, httpPacket.message)); diff --git a/src/main/java/com/github/jie65535/opencommand/OpenCommandPlugin.java b/src/main/java/com/github/jie65535/opencommand/OpenCommandPlugin.java index 4529878..6337e37 100644 --- a/src/main/java/com/github/jie65535/opencommand/OpenCommandPlugin.java +++ b/src/main/java/com/github/jie65535/opencommand/OpenCommandPlugin.java @@ -35,7 +35,10 @@ import java.io.IOException; public final class OpenCommandPlugin extends Plugin { private static OpenCommandPlugin instance; - public static OpenCommandPlugin getInstance() { return instance; } + + public static OpenCommandPlugin getInstance() { + return instance; + } private OpenCommandConfig config; @@ -82,19 +85,19 @@ public final class OpenCommandPlugin extends Plugin { var configFile = new File(getDataFolder(), "config.json"); if (!configFile.exists()) { config = new OpenCommandConfig(); - try (var file = new FileWriter(configFile)){ + try (var file = new FileWriter(configFile)) { file.write(Grasscutter.getGsonFactory().toJson(config)); } catch (IOException e) { - getLogger().error("Unable to write to config file."); + getLogger().error("[OpenCommand] Unable to write to config file."); } catch (Exception e) { - getLogger().error("Unable to save config file."); + getLogger().error("[OpenCommand] Unable to save config file."); } } else { try (var file = new FileReader(configFile)) { config = Grasscutter.getGsonFactory().fromJson(file, OpenCommandConfig.class); } catch (Exception exception) { config = new OpenCommandConfig(); - getLogger().error("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."); } } // 启动Socket @@ -110,7 +113,7 @@ public final class OpenCommandPlugin extends Plugin { try { SocketServer.startServer(); } catch (IOException e) { - getLogger().error("Unable to start socket server.", e); + getLogger().error("[OpenCommand] Unable to start socket server.", e); } } } diff --git a/src/main/java/com/github/jie65535/opencommand/json/JsonRequest.java b/src/main/java/com/github/jie65535/opencommand/json/JsonRequest.java index e88e426..ef8dd9f 100644 --- a/src/main/java/com/github/jie65535/opencommand/json/JsonRequest.java +++ b/src/main/java/com/github/jie65535/opencommand/json/JsonRequest.java @@ -20,5 +20,6 @@ package com.github.jie65535.opencommand.json; public final class JsonRequest { public String token = ""; public String action = ""; + public String server = ""; public Object data = null; } \ No newline at end of file diff --git a/src/main/java/com/github/jie65535/opencommand/json/JsonResponse.java b/src/main/java/com/github/jie65535/opencommand/json/JsonResponse.java index 5a37837..1163142 100644 --- a/src/main/java/com/github/jie65535/opencommand/json/JsonResponse.java +++ b/src/main/java/com/github/jie65535/opencommand/json/JsonResponse.java @@ -19,7 +19,7 @@ package com.github.jie65535.opencommand.json; public final class JsonResponse { public int retcode = 200; - public String message = "success"; + public String message = "Success"; public Object data; public JsonResponse() { diff --git a/src/main/java/com/github/jie65535/opencommand/socket/ClientInfo.java b/src/main/java/com/github/jie65535/opencommand/socket/ClientInfo.java new file mode 100644 index 0000000..df004f7 --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/ClientInfo.java @@ -0,0 +1,14 @@ +package com.github.jie65535.opencommand.socket; + +public class ClientInfo { + + public final String uuid; + public final SocketServer.ClientThread clientThread; + public final String ip; + + public ClientInfo(String uuid, String ip, SocketServer.ClientThread clientThread) { + this.uuid = uuid; + this.clientThread = clientThread; + this.ip = ip; + } +} diff --git a/src/main/java/com/github/jie65535/opencommand/socket/SocketClient.java b/src/main/java/com/github/jie65535/opencommand/socket/SocketClient.java index 692092b..92245e5 100644 --- a/src/main/java/com/github/jie65535/opencommand/socket/SocketClient.java +++ b/src/main/java/com/github/jie65535/opencommand/socket/SocketClient.java @@ -1,5 +1,6 @@ package com.github.jie65535.opencommand.socket; +import com.github.jie65535.opencommand.EventListeners; import com.github.jie65535.opencommand.OpenCommandConfig; import com.github.jie65535.opencommand.OpenCommandPlugin; import com.github.jie65535.opencommand.socket.packet.*; @@ -33,6 +34,14 @@ public class SocketClient { // 连接服务器 public static void connectServer() { if (connect) return; + if (clientThread != null) { + mLogger.warn("[OpenCommand] Retry connecting to the server after 15 seconds"); + try { + Thread.sleep(15000); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + } OpenCommandConfig config = OpenCommandPlugin.getInstance().getConfig(); mLogger = OpenCommandPlugin.getInstance().getLogger(); clientThread = new ClientThread(config.socketHost, config.socketPort); @@ -160,6 +169,24 @@ public class SocketClient { } } break; + case RunConsoleCommand: + var consoleCommand = Grasscutter.getGsonFactory().fromJson(packet.data, RunConsoleCommand.class); + var plugin = OpenCommandPlugin.getInstance(); + //noinspection SynchronizationOnLocalVariableOrMethodParameter + synchronized (plugin) { + try { + var resultCollector = new MessageHandler(); + EventListeners.setConsoleMessageHandler(resultCollector); + CommandMap.getInstance().invoke(null, null, consoleCommand.command); + sendPacket(new HttpPacket(resultCollector.getMessage()), packet.packetID); + } catch (Exception e) { + mLogger.warn("[OpenCommand] Run command failed.", e); + EventListeners.setConsoleMessageHandler(null); + sendPacket(new HttpPacket(500, "error", e.getLocalizedMessage()), packet.packetID); + } finally { + EventListeners.setConsoleMessageHandler(null); + } + } } } catch (Throwable e) { e.printStackTrace(); @@ -206,18 +233,12 @@ public class SocketClient { socket = new Socket(ip, port); os = socket.getOutputStream(); - mLogger.info("[OpenCommand]Connect to server: " + ip + ":" + port); - SocketClient.sendPacket(new AuthPacket(OpenCommandPlugin.getInstance().getConfig().socketToken)); + mLogger.info("[OpenCommand] Connect to server: " + ip + ":" + port); + SocketClient.sendPacket(new AuthPacket(OpenCommandPlugin.getInstance().getConfig().socketToken, OpenCommandPlugin.getInstance().getConfig().socketDisplayName)); receiveThread = new ReceiveThread(socket); } catch (IOException e) { connect = false; - mLogger.warn("[OpenCommand]Connect to server failed: " + ip + ":" + port); - mLogger.warn("[OpenCommand] Retry connecting to the server after 15 seconds"); - try { - Thread.sleep(15000); - } catch (InterruptedException ex) { - throw new RuntimeException(ex); - } + mLogger.warn("[OpenCommand] Connect to server failed: " + ip + ":" + port); connectServer(); } } diff --git a/src/main/java/com/github/jie65535/opencommand/socket/SocketData.java b/src/main/java/com/github/jie65535/opencommand/socket/SocketData.java index c9cfb64..b05ba6b 100644 --- a/src/main/java/com/github/jie65535/opencommand/socket/SocketData.java +++ b/src/main/java/com/github/jie65535/opencommand/socket/SocketData.java @@ -2,6 +2,7 @@ package com.github.jie65535.opencommand.socket; import com.github.jie65535.opencommand.socket.packet.player.PlayerList; +import java.util.ArrayList; import java.util.HashMap; import java.util.concurrent.atomic.AtomicReference; @@ -27,4 +28,21 @@ public class SocketData { }); return ret.get(); } + + public static OnlinePlayer getOnlinePlayer() { + ArrayList player = new ArrayList<>(); + playerList.forEach((address, playerMap) -> playerMap.playerMap.forEach((uid, name) -> player.add(name))); + return new OnlinePlayer(player); + } + + public static class OnlinePlayer { + public int count; + public ArrayList playerList; + + public OnlinePlayer(ArrayList playerList) { + this.playerList = playerList; + this.count = playerList.size(); + } + + } } diff --git a/src/main/java/com/github/jie65535/opencommand/socket/SocketServer.java b/src/main/java/com/github/jie65535/opencommand/socket/SocketServer.java index fa36092..4c8d192 100644 --- a/src/main/java/com/github/jie65535/opencommand/socket/SocketServer.java +++ b/src/main/java/com/github/jie65535/opencommand/socket/SocketServer.java @@ -14,16 +14,34 @@ import java.net.Socket; import java.util.HashMap; import java.util.Timer; import java.util.TimerTask; +import java.util.UUID; // Socket 服务器 public class SocketServer { // 客户端超时时间 private static final int TIMEOUT = 5000; - private static final HashMap clientList = new HashMap<>(); + private static final HashMap clientList = new HashMap<>(); private static final HashMap clientTimeout = new HashMap<>(); private static Logger mLogger; + public static HashMap getOnlineClient() { + HashMap onlineClient = new HashMap<>(); + for (var key : clientList.entrySet()) { + onlineClient.put(key.getValue().uuid, key.getValue().clientThread.getDisplayName()); + } + return onlineClient; + } + + public static ClientInfo getClientInfoByUuid(String uuid) { + for (var key : clientList.entrySet()) { + if (key.getValue().uuid.equals(uuid)) { + return key.getValue(); + } + } + return null; + } + public static void startServer() throws IOException { int port = OpenCommandPlugin.getInstance().getConfig().socketPort; mLogger = OpenCommandPlugin.getInstance().getLogger(); @@ -34,7 +52,7 @@ public class SocketServer { // 向全部客户端发送数据 public static boolean sendAllPacket(BasePacket packet) { var p = SocketUtils.getPacket(packet); - HashMap old = (HashMap) clientList.clone(); + HashMap old = (HashMap) clientList.clone(); for (var client : old.entrySet()) { if (!client.getValue().sendPacket(p)) { mLogger.warn("[OpenCommand] Send packet to client {} failed", client.getKey()); @@ -49,7 +67,21 @@ public class SocketServer { var p = SocketUtils.getPacket(packet); var client = clientList.get(address); if (client != null) { - if (client.sendPacket(p)) { + if (client.clientThread.sendPacket(p)) { + return true; + } + mLogger.warn("[OpenCommand] Send packet to client {} failed", address); + clientList.remove(address); + } + return false; + } + + public static boolean sendPacketAndWait(String address, BasePacket packet, SocketDataWait wait) { + var p = SocketUtils.getPacketAndPackID(packet); + var client = clientList.get(address); + if (client != null) { + wait.uid = p.get(0); + if (client.clientThread.sendPacket(p.get(1), wait)) { return true; } mLogger.warn("[OpenCommand] Send packet to client {} failed", address); @@ -66,7 +98,7 @@ public class SocketServer { var client = clientList.get(clientID); if (client != null) { socketDataWait.uid = p.get(0); - if (!client.sendPacket(p.get(1), socketDataWait)) { + if (!client.clientThread.sendPacket(p.get(1), socketDataWait)) { mLogger.warn("[OpenCommand] Send packet to client {} failed", clientID); clientList.remove(clientID); return false; @@ -97,13 +129,14 @@ public class SocketServer { } // 客户端数据包处理 - private static class ClientThread extends Thread { + static class ClientThread extends Thread { private final Socket socket; private InputStream is; private OutputStream os; private final String address; private final String token; private boolean auth = false; + private String displayName = ""; private final HashMap> socketDataWaitList = new HashMap<>(); @@ -147,11 +180,12 @@ public class SocketServer { String data = SocketUtils.readString(is); Packet packet = Grasscutter.getGsonFactory().fromJson(data, Packet.class); if (packet.type == PacketEnum.AuthPacket) { - AuthPacket authPacket = Grasscutter.getGsonFactory().fromJson(data, AuthPacket.class); + AuthPacket authPacket = Grasscutter.getGsonFactory().fromJson(packet.data, AuthPacket.class); if (authPacket.token.equals(token)) { auth = true; - mLogger.info("[OpenCommand] Client {} auth success", address); - clientList.put(address, this); + displayName = authPacket.displayName; + mLogger.info("[OpenCommand] Client {} auth success, name: {}", address, displayName); + clientList.put(address, new ClientInfo(UUID.randomUUID().toString(), address, this)); clientTimeout.put(address, 0); } else { mLogger.warn("[OpenCommand] Client {} auth failed", address); @@ -196,6 +230,10 @@ public class SocketServer { } } } + + public String getDisplayName() { + return displayName; + } } // 等待客户端连接 diff --git a/src/main/java/com/github/jie65535/opencommand/socket/SocketUtils.java b/src/main/java/com/github/jie65535/opencommand/socket/SocketUtils.java index 74a5faa..79ad9e9 100644 --- a/src/main/java/com/github/jie65535/opencommand/socket/SocketUtils.java +++ b/src/main/java/com/github/jie65535/opencommand/socket/SocketUtils.java @@ -17,12 +17,12 @@ public class SocketUtils { /** * 获取打包后的数据包 + * * @param bPacket 数据包 * @return 打包后的数据包 */ public static String getPacket(BasePacket bPacket) { Packet packet = new Packet(); - packet.token = OpenCommandPlugin.getInstance().getConfig().socketToken; packet.type = bPacket.getType(); packet.data = bPacket.getPacket(); packet.packetID = UUID.randomUUID().toString(); @@ -31,12 +31,12 @@ public class SocketUtils { /** * 获取打包后的数据包 + * * @param bPacket BasePacket * @return list[0] 是包ID, list[1] 是数据包 */ public static List getPacketAndPackID(BasePacket bPacket) { Packet packet = new Packet(); - packet.token = OpenCommandPlugin.getInstance().getConfig().socketToken; packet.type = bPacket.getType(); packet.data = bPacket.getPacket(); packet.packetID = UUID.randomUUID().toString(); @@ -49,13 +49,13 @@ public class SocketUtils { /** * 获取打包后的数据包 - * @param bPacket 数据包 + * + * @param bPacket 数据包 * @param packetID 数据包ID * @return 打包后的数据包 */ public static String getPacketAndPackID(BasePacket bPacket, String packetID) { Packet packet = new Packet(); - packet.token = OpenCommandPlugin.getInstance().getConfig().socketToken; packet.type = bPacket.getType(); packet.data = bPacket.getPacket(); packet.packetID = packetID; @@ -64,6 +64,7 @@ public class SocketUtils { /** * 读整数 + * * @param is 输入流 * @return 整数 */ @@ -77,32 +78,34 @@ public class SocketUtils { e.printStackTrace(); } - return values[0]<<24 | values[1]<<16 | values[2]<<8 | values[3]; + return values[0] << 24 | values[1] << 16 | values[2] << 8 | values[3]; } /** * 写整数 - * @param os 输出流 + * + * @param os 输出流 * @param value 整数 */ public static void writeInt(OutputStream os, int value) { int[] values = new int[4]; - values[0] = (value>>24)&0xFF; - values[1] = (value>>16)&0xFF; - values[2] = (value>>8)&0xFF; - values[3] = (value)&0xFF; + values[0] = (value >> 24) & 0xFF; + values[1] = (value >> 16) & 0xFF; + values[2] = (value >> 8) & 0xFF; + values[3] = (value) & 0xFF; - try{ + try { for (int i = 0; i < 4; i++) { os.write(values[i]); } - }catch (IOException e){ + } catch (IOException e) { e.printStackTrace(); } } /** * 读字符串 + * * @param is 输入流 * @return 字符串 */ @@ -120,15 +123,16 @@ public class SocketUtils { /** * 写字符串 + * * @param os 输出流 - * @param s 字符串 + * @param s 字符串 * @return 是否成功 */ - public static boolean writeString(OutputStream os,String s) { + public static boolean writeString(OutputStream os, String s) { try { byte[] bytes = s.getBytes(); int len = bytes.length; - writeInt(os,len); + writeInt(os, len); os.write(bytes); return true; } catch (IOException e) { diff --git a/src/main/java/com/github/jie65535/opencommand/socket/packet/AuthPacket.java b/src/main/java/com/github/jie65535/opencommand/socket/packet/AuthPacket.java index d244018..768beea 100644 --- a/src/main/java/com/github/jie65535/opencommand/socket/packet/AuthPacket.java +++ b/src/main/java/com/github/jie65535/opencommand/socket/packet/AuthPacket.java @@ -4,8 +4,10 @@ import emu.grasscutter.Grasscutter; public class AuthPacket extends BasePacket { public String token; + public String displayName; - public AuthPacket(String token) { + public AuthPacket(String token, String displayName) { + this.displayName = displayName; this.token = token; } @@ -18,4 +20,9 @@ public class AuthPacket extends BasePacket { public PacketEnum getType() { return PacketEnum.AuthPacket; } + + @Override + public String toString() { + return "AuthPacket [token=" + token + ", displayName=" + displayName + "]"; + } } diff --git a/src/main/java/com/github/jie65535/opencommand/socket/packet/HttpPacket.java b/src/main/java/com/github/jie65535/opencommand/socket/packet/HttpPacket.java index 5cc5a46..4e81e7d 100644 --- a/src/main/java/com/github/jie65535/opencommand/socket/packet/HttpPacket.java +++ b/src/main/java/com/github/jie65535/opencommand/socket/packet/HttpPacket.java @@ -4,8 +4,8 @@ import emu.grasscutter.Grasscutter; // http返回数据 public class HttpPacket extends BasePacket { - public int code; - public String message; + public int code = 200; + public String message = "Success"; public String data; public HttpPacket(int code, String message, String data) { @@ -18,6 +18,9 @@ public class HttpPacket extends BasePacket { this.code = code; this.message = message; } + public HttpPacket(String data) { + this.data = data; + } @Override public String getPacket() { diff --git a/src/main/java/com/github/jie65535/opencommand/socket/packet/Packet.java b/src/main/java/com/github/jie65535/opencommand/socket/packet/Packet.java index bc45524..6fe303a 100644 --- a/src/main/java/com/github/jie65535/opencommand/socket/packet/Packet.java +++ b/src/main/java/com/github/jie65535/opencommand/socket/packet/Packet.java @@ -2,13 +2,12 @@ package com.github.jie65535.opencommand.socket.packet; // 数据包结构 public class Packet { - public String token; public PacketEnum type; public String data; public String packetID; @Override public String toString() { - return "Packet [token=" + token + ", type=" + type + ", data=" + data + ", packetID=" + packetID + "]"; + return "Packet [type=" + type + ", data=" + data + ", packetID=" + packetID + "]"; } } diff --git a/src/main/java/com/github/jie65535/opencommand/socket/packet/PacketEnum.java b/src/main/java/com/github/jie65535/opencommand/socket/packet/PacketEnum.java index dadb1ff..54a8fdf 100644 --- a/src/main/java/com/github/jie65535/opencommand/socket/packet/PacketEnum.java +++ b/src/main/java/com/github/jie65535/opencommand/socket/packet/PacketEnum.java @@ -6,5 +6,6 @@ public enum PacketEnum { Player, HttpPacket, AuthPacket, + RunConsoleCommand, HeartBeat } diff --git a/src/main/java/com/github/jie65535/opencommand/socket/packet/RunConsoleCommand.java b/src/main/java/com/github/jie65535/opencommand/socket/packet/RunConsoleCommand.java new file mode 100644 index 0000000..48d8d5a --- /dev/null +++ b/src/main/java/com/github/jie65535/opencommand/socket/packet/RunConsoleCommand.java @@ -0,0 +1,21 @@ +package com.github.jie65535.opencommand.socket.packet; + +import emu.grasscutter.Grasscutter; + +public class RunConsoleCommand extends BasePacket { + public String command; + + public RunConsoleCommand(String command) { + this.command = command; + } + + @Override + public String getPacket() { + return Grasscutter.getGsonFactory().toJson(this); + } + + @Override + public PacketEnum getType() { + return PacketEnum.RunConsoleCommand; + } +} From 6aa19ce86094e9411e514f36c6459d29044c0129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B9=E5=9D=97=E5=90=9B?= Date: Tue, 26 Jul 2022 19:20:42 +0800 Subject: [PATCH 10/14] Add github actions --- .github/workflows/build.yml | 47 +++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..5b5ea69 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,47 @@ +name: "Build" +on: + workflow_dispatch: ~ + push: + paths: + - "**.java" + branches: + - "main" + pull_request: + paths: + - "**.java" + types: + - opened + - synchronize + - reopened +jobs: + Build-Server-Jar: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup Java + uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: '17' + - name: Cache gradle files + uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + ./.gradle/loom-cache + key: ${{ runner.os }}-gradle-${{ hashFiles('*.gradle', 'gradle.properties', '**/*.accesswidener') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Download latest grasscutter jar + run: wget https://nightly.link/Grasscutters/Grasscutter/workflows/build/development/Grasscutter.zip && mkdir lib && unzip Grasscutter.zip -d lib + + - name: Run Gradle + run: ./gradlew jar + + - name: Upload build + uses: actions/upload-artifact@v3 + with: + name: mojoconsole + path: opencommand*.jar \ No newline at end of file From 4712e2cd73066223e35a75ab5cfa0c1b73c21932 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B9=E5=9D=97=E5=90=9B?= Date: Tue, 26 Jul 2022 19:23:10 +0800 Subject: [PATCH 11/14] Fix run permission --- .github/workflows/build.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5b5ea69..2a190f3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,7 +14,7 @@ on: - synchronize - reopened jobs: - Build-Server-Jar: + Build-Jar: runs-on: ubuntu-latest steps: - name: Checkout @@ -37,6 +37,9 @@ jobs: - name: Download latest grasscutter jar run: wget https://nightly.link/Grasscutters/Grasscutter/workflows/build/development/Grasscutter.zip && mkdir lib && unzip Grasscutter.zip -d lib + - name: Change permission + run: chmod +x gradlew + - name: Run Gradle run: ./gradlew jar From 3ed8e233bcf4aa807440b5cd14681f71317d7a24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B9=E5=9D=97=E5=90=9B?= Date: Tue, 26 Jul 2022 19:30:31 +0800 Subject: [PATCH 12/14] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0a53932..aa6cbac 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ "socketPort": 5746, // 多服务器通信密钥 "socketToken": "", - // 多服务器Dispatch地址 + // 多服务器Dispatch服务器地址 "socketHost": "127.0.0.1", // 多服务器显示名称 "socketDisplayName": "" From dc5cbcc981f85af6d390df6069824386e88d750d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B9=E5=9D=97=E5=90=9B?= Date: Tue, 26 Jul 2022 19:45:07 +0800 Subject: [PATCH 13/14] Update build.yml --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2a190f3..3f02b11 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,5 +46,5 @@ jobs: - name: Upload build uses: actions/upload-artifact@v3 with: - name: mojoconsole + name: open-command path: opencommand*.jar \ No newline at end of file From 6db7c8919696ea7253c9299ea3179868d2ddbd71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B9=E5=9D=97=E5=90=9B?= Date: Tue, 26 Jul 2022 19:52:13 +0800 Subject: [PATCH 14/14] Update build.yml --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3f02b11..b16fd78 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,5 +46,5 @@ jobs: - name: Upload build uses: actions/upload-artifact@v3 with: - name: open-command + name: opencommand path: opencommand*.jar \ No newline at end of file