diff --git a/gc-plugin/src/main/java/com/mojo/consoleplus/AuthHandler.java b/gc-plugin/src/main/java/com/mojo/consoleplus/AuthHandler.java index 856b2b0..7c1b7b4 100644 --- a/gc-plugin/src/main/java/com/mojo/consoleplus/AuthHandler.java +++ b/gc-plugin/src/main/java/com/mojo/consoleplus/AuthHandler.java @@ -17,6 +17,14 @@ public class AuthHandler { signatureStub = stub; } + public String getSignature() { + return signatureStub; + } + + public void setSignature(String signature) { + signatureStub = signature; + } + public Boolean auth(int uid, long expire, String dg) { return digestUid(uid+":"+expire).equals(dg); } diff --git a/gc-plugin/src/main/java/com/mojo/consoleplus/ConsolePlus.java b/gc-plugin/src/main/java/com/mojo/consoleplus/ConsolePlus.java index b39708e..e4c67ee 100644 --- a/gc-plugin/src/main/java/com/mojo/consoleplus/ConsolePlus.java +++ b/gc-plugin/src/main/java/com/mojo/consoleplus/ConsolePlus.java @@ -1,5 +1,7 @@ package com.mojo.consoleplus; +import com.mojo.consoleplus.socket.SocketClient; +import com.mojo.consoleplus.socket.SocketServer; import emu.grasscutter.Grasscutter; import emu.grasscutter.command.CommandMap; import emu.grasscutter.plugin.Plugin; @@ -12,20 +14,33 @@ import java.io.InputStreamReader; import com.google.gson.Gson; import com.mojo.consoleplus.command.PluginCommand; +import emu.grasscutter.server.event.EventHandler; +import emu.grasscutter.server.event.HandlerPriority; +import emu.grasscutter.server.event.player.PlayerJoinEvent; +import emu.grasscutter.server.event.player.PlayerQuitEvent; import io.javalin.http.staticfiles.Location; import emu.grasscutter.plugin.PluginConfig; import static emu.grasscutter.Configuration.PLUGIN; import static emu.grasscutter.Configuration.HTTP_POLICIES; import com.mojo.consoleplus.config.MojoConfig; +import org.slf4j.Logger; public class ConsolePlus extends Plugin{ public static MojoConfig config = MojoConfig.loadConfig(); public static String versionTag; public static AuthHandler authHandler; + public static Logger logger; + public static ConsolePlus instance; + + public static ConsolePlus getInstance() { + return instance; + } @Override public void onLoad() { + instance = this; + logger = getLogger(); try (InputStream in = getClass().getResourceAsStream("/plugin.json"); BufferedReader reader = new BufferedReader(new InputStreamReader(in))) { Gson gson = new Gson(); @@ -56,10 +71,27 @@ public class ConsolePlus extends Plugin{ return; } } - Grasscutter.getHttpServer().addRouter(RequestHandler.class); + + authHandler = new AuthHandler(); + + if (Grasscutter.config.server.runMode == Grasscutter.ServerRunMode.DISPATCH_ONLY) { + SocketServer.startServer(); + Grasscutter.getHttpServer().addRouter(RequestOnlyHttpHandler.class); + } else if (Grasscutter.config.server.runMode == Grasscutter.ServerRunMode.GAME_ONLY) { + SocketClient.connectServer(); + 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 { + Grasscutter.getHttpServer().addRouter(RequestHandler.class); + } CommandMap.getInstance().registerCommand("mojoconsole", new PluginCommand()); this.getLogger().info("[MojoConsole] enabled. Version: " + versionTag); - authHandler = new AuthHandler(); } @Override diff --git a/gc-plugin/src/main/java/com/mojo/consoleplus/EventListeners.java b/gc-plugin/src/main/java/com/mojo/consoleplus/EventListeners.java new file mode 100644 index 0000000..1a0c55a --- /dev/null +++ b/gc-plugin/src/main/java/com/mojo/consoleplus/EventListeners.java @@ -0,0 +1,40 @@ +package com.mojo.consoleplus; + +import com.mojo.consoleplus.socket.SocketClient; +import com.mojo.consoleplus.socket.packet.player.PlayerList; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.server.event.player.PlayerJoinEvent; +import emu.grasscutter.server.event.player.PlayerQuitEvent; + +import java.util.ArrayList; + +public class EventListeners { + 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/gc-plugin/src/main/java/com/mojo/consoleplus/RequestOnlyHttpHandler.java b/gc-plugin/src/main/java/com/mojo/consoleplus/RequestOnlyHttpHandler.java new file mode 100644 index 0000000..9ab625a --- /dev/null +++ b/gc-plugin/src/main/java/com/mojo/consoleplus/RequestOnlyHttpHandler.java @@ -0,0 +1,144 @@ +package com.mojo.consoleplus; + +import com.mojo.consoleplus.command.PluginCommand; +import com.mojo.consoleplus.forms.RequestAuth; +import com.mojo.consoleplus.forms.RequestJson; +import com.mojo.consoleplus.forms.ResponseAuth; +import com.mojo.consoleplus.forms.ResponseJson; +import com.mojo.consoleplus.socket.SocketData; +import com.mojo.consoleplus.socket.SocketDataWait; +import com.mojo.consoleplus.socket.SocketServer; +import com.mojo.consoleplus.socket.packet.HttpPacket; +import com.mojo.consoleplus.socket.packet.OtpPacket; +import com.mojo.consoleplus.socket.packet.player.Player; +import com.mojo.consoleplus.socket.packet.player.PlayerEnum; +import com.mojo.consoleplus.socket.packet.player.PlayerList; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.server.http.Router; +import express.Express; +import express.http.Request; +import express.http.Response; +import io.javalin.Javalin; + +import java.io.IOException; +import java.text.DecimalFormat; +import java.util.Map; +import java.util.Random; + +import static java.lang.Integer.parseInt; +import static java.lang.Long.parseLong; + + +public final class RequestOnlyHttpHandler implements Router { + // private static final Gson gson = new GsonBuilder().setPrettyPrinting().create(); + + @Override public void applyRoutes(Express app, Javalin handle) { + app.post("/mojoplus/api", RequestOnlyHttpHandler::processRequest); + app.post("/mojoplus/auth", RequestOnlyHttpHandler::requestKey); + } + + + public static void processRequest(Request req, Response res) throws IOException { + RequestJson request = req.body(RequestJson.class); + res.type("application/json"); + String player = null; + int uid = -1; + + if (request.k2 != null) { // version 2 token + long expire; + String hashDigest; + uid = parseInt(request.k2.split(":")[0]); + expire = parseLong(request.k2.split(":")[1]); + hashDigest = request.k2.split(":")[2]; + if (ConsolePlus.authHandler.auth(uid, expire, hashDigest)){ + player = SocketData.getPlayer(uid); + } + } + + if (player != null) { + SocketDataWait wait = null; + switch (request.request){ + case "invoke": + wait = new SocketDataWait<>(2000) { + @Override + public void run() {} + @Override + public HttpPacket initData(HttpPacket data) { + return data; + } + + @Override + public void timeout() { + res.json(new ResponseJson("timeout", 500)); + } + }; + try{ + // TODO: Enable execut commands to third party + Player p = new Player(); + p.type = PlayerEnum.RunCommand; + p.uid = uid; + p.data = request.payload; + SocketServer.sendUidPacket(uid, p, wait); + } catch (Exception e) { + res.json(new ResponseJson("error", 500, e.getStackTrace().toString())); + break; + } + case "ping": + // res.json(new ResponseJson("success", 200)); + if (wait == null) { + res.json(new ResponseJson("success", 200, null)); + } else { + var data = wait.getData(); + if (data == null) { + res.json(new ResponseJson("timeout", 500)); + } else { + res.json(new ResponseJson(data.message, data.code, data.data)); + } + } + break; + default: + res.json(new ResponseJson("400 Bad Request", 400)); + break; + } + return; + } + + res.json(new ResponseJson("403 Forbidden", 403)); + } + + public static void requestKey(Request req, Response res) throws IOException { + RequestAuth request = req.body(RequestAuth.class); + if (request.otp != null && !request.otp.equals("")) { + if (PluginCommand.getInstance().tickets.get(request.otp) == null) { + res.json(new ResponseAuth(404, "Not found", null)); + return; + } + String key = SocketData.tickets.get(request.otp).key; + if (key == null){ + res.json(new ResponseAuth(403, "Not ready yet", null)); + } else { + SocketData.tickets.remove(request.otp); + res.json(new ResponseAuth(200, "", key)); + } + return; + } else if (request.uid != 0) { + String otp = new DecimalFormat("000000").format(new Random().nextInt(999999)); + while (PluginCommand.getInstance().tickets.containsKey(otp)){ + otp = new DecimalFormat("000000").format(new Random().nextInt(999999)); + } + String targetPlayer = SocketData.getPlayer(request.uid); + if (targetPlayer == null){ + res.json(new ResponseAuth(404, "Not found", null)); + return; + } + var otpPacket = new OtpPacket(request.uid, otp, System.currentTimeMillis() / 1000 + 300, true); + if (!SocketServer.sendPacket(SocketData.getPlayerInServer(request.uid), otpPacket)) { + res.json(new ResponseAuth(500, "Send otp to server failed.", null)); + return; + } + SocketData.tickets.put(otp, otpPacket); + res.json(new ResponseAuth(201, "Code generated", otp)); + return; + } + } +} diff --git a/gc-plugin/src/main/java/com/mojo/consoleplus/command/PluginCommand.java b/gc-plugin/src/main/java/com/mojo/consoleplus/command/PluginCommand.java index d6bbabc..d0f5e63 100644 --- a/gc-plugin/src/main/java/com/mojo/consoleplus/command/PluginCommand.java +++ b/gc-plugin/src/main/java/com/mojo/consoleplus/command/PluginCommand.java @@ -6,6 +6,8 @@ import java.util.HashMap; import java.util.List; import java.util.Random; +import com.mojo.consoleplus.socket.SocketClient; +import com.mojo.consoleplus.socket.packet.OtpPacket; import emu.grasscutter.command.Command; import emu.grasscutter.command.CommandHandler; import emu.grasscutter.game.mail.Mail; @@ -59,7 +61,9 @@ public class PluginCommand implements CommandHandler { } CommandHandler.sendMessage(sender, ConsolePlus.config.responseMessageThird.replace("{{OTP}}", otp)); flushTicket(); - tickets.put(otp, new Ticket(sender, targetPlayer, System.currentTimeMillis()/ 1000 + 300)); + var time = System.currentTimeMillis()/ 1000 + 300; + SocketClient.sendPacket(new OtpPacket(targetPlayer.getUid(), otp, time, false)); + tickets.put(otp, new Ticket(sender, targetPlayer, time)); return; } String link_type = "webview"; @@ -140,6 +144,7 @@ public class PluginCommand implements CommandHandler { for (String otp : tickets.keySet()) { if (curtime > tickets.get(otp).expire) { tickets.remove(otp); + SocketClient.sendPacket(new OtpPacket(otp)); } } } diff --git a/gc-plugin/src/main/java/com/mojo/consoleplus/config/MojoConfig.java b/gc-plugin/src/main/java/com/mojo/consoleplus/config/MojoConfig.java index d7fc0b2..f9ba709 100644 --- a/gc-plugin/src/main/java/com/mojo/consoleplus/config/MojoConfig.java +++ b/gc-plugin/src/main/java/com/mojo/consoleplus/config/MojoConfig.java @@ -18,6 +18,9 @@ public class MojoConfig { public String responseMessageThird = "[MojoConsole] You are trying to obtain link for third-party user, please ask him/her send \"/mojo {{OTP}}\" to server in-game"; public String responseMessageError = "[MojoConsole] Invalid argument."; public String responseMessageSuccess = "[MojoConsole] Success!"; + public String socketToken = ""; + public int socketPort = 7812; + public String socketHost = "127.0.0.1"; static public class MailTemplate { public String title = "Mojo Console Link"; diff --git a/gc-plugin/src/main/java/com/mojo/consoleplus/socket/SocketClient.java b/gc-plugin/src/main/java/com/mojo/consoleplus/socket/SocketClient.java new file mode 100644 index 0000000..01972a3 --- /dev/null +++ b/gc-plugin/src/main/java/com/mojo/consoleplus/socket/SocketClient.java @@ -0,0 +1,231 @@ +package com.mojo.consoleplus.socket; + +import com.mojo.consoleplus.ConsolePlus; +import com.mojo.consoleplus.command.PluginCommand; +import com.mojo.consoleplus.config.MojoConfig; +import com.mojo.consoleplus.socket.packet.*; +import com.mojo.consoleplus.socket.packet.player.Player; +import com.mojo.consoleplus.socket.packet.player.PlayerList; +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.ArrayList; +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 ReceiveThread receiveThread; + + // 连接服务器 + public static void connectServer() { + if (connect) return; + MojoConfig config = ConsolePlus.config; + mLogger = ConsolePlus.logger; + clientThread = new ClientThread(config.socketHost, config.socketPort); + + if (timer != null) { + timer.cancel(); + } + timer = new Timer(); + timer.schedule(new SendHeartBeatPacket(), 500); + timer.schedule(new SendPlayerListPacket(), 1000); + } + + // 发送数据包 + public static boolean sendPacket(BasePacket packet) { + var p = SocketUtils.getPacket(packet); + if (!clientThread.sendPacket(p)) { + mLogger.warn("[Mojo Console] Send packet to server failed"); + mLogger.info("[Mojo Console] 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("[Mojo Console] Send packet to server failed"); + mLogger.info("[Mojo Console] 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 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; + private boolean exit; + + public ReceiveThread(Socket socket) { + try { + is = socket.getInputStream(); + } catch (IOException e) { + e.printStackTrace(); + } + start(); + } + + @Override + public void run() { + //noinspection InfiniteLoopStatement + while (true) { + try { + if (exit) return; + String data = SocketUtils.readString(is); + Packet packet = Grasscutter.getGsonFactory().fromJson(data, Packet.class); + 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 = ConsolePlus.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) { + mLogger.warn("Run command failed.", e); + sendPacket(new HttpPacket(500, "error", e.getLocalizedMessage()), packet.packetID); + } finally { + playerData.setMessageHandler(null); + } + } + } + // 发送信息 + case DropMessage -> { + var playerData = ConsolePlus.getInstance().getServer().getPlayerByUid(player.uid); + if (playerData == null) { + return; + } + playerData.dropMessage(player.data); + } + } + break; + case OtpPacket: + var otpPacket = Grasscutter.getGsonFactory().fromJson(packet.data, OtpPacket.class); + PluginCommand.getInstance().tickets.put(otpPacket.otp, new PluginCommand.Ticket(Grasscutter.getGameServer().getPlayerByUid(otpPacket.uid), otpPacket.expire, otpPacket.api)); + case Signature: + var signaturePacket = Grasscutter.getGsonFactory().fromJson(packet.data, SignaturePacket.class); + ConsolePlus.authHandler.setSignature(signaturePacket.signature); + break; + } + } catch (Throwable e) { + e.printStackTrace(); + if (!sendPacket(new HeartBeat("Pong"))) { + return; + } + } + } + } + + public void exit() { + exit = true; + } + } + + // 客户端连接线程 + 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 { + if (receiveThread != null) { + receiveThread.exit(); + } + + socket = new Socket(ip, port); + connect = true; + os = socket.getOutputStream(); + mLogger.info("[Mojo Console] Connect to server: " + ip + ":" + port); + SocketClient.sendPacket(new AuthPacket(ConsolePlus.config.socketToken)); + receiveThread = new ReceiveThread(socket); + } catch (IOException e) { + connect = false; + mLogger.warn("[Mojo Console] Connect to server failed: " + ip + ":" + port); + mLogger.warn("[Mojo Console] Retry connecting to the server after 15 seconds"); + try { + Thread.sleep(15000); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + connectServer(); + } + } + } +} diff --git a/gc-plugin/src/main/java/com/mojo/consoleplus/socket/SocketData.java b/gc-plugin/src/main/java/com/mojo/consoleplus/socket/SocketData.java new file mode 100644 index 0000000..29e0c59 --- /dev/null +++ b/gc-plugin/src/main/java/com/mojo/consoleplus/socket/SocketData.java @@ -0,0 +1,34 @@ +package com.mojo.consoleplus.socket; + +import com.mojo.consoleplus.socket.packet.OtpPacket; +import com.mojo.consoleplus.socket.packet.player.PlayerList; +import org.luaj.vm2.ast.Str; + +import java.util.HashMap; +import java.util.concurrent.atomic.AtomicReference; + +// Socket 数据保存 +public class SocketData { + public static HashMap playerList = new HashMap<>(); + + public static HashMap tickets = new HashMap<>(); + + public static String getPlayer(int uid) { + for (PlayerList player : playerList.values()) { + if (player.playerMap.get(uid) != null) { + return player.playerMap.get(uid); + } + } + 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/gc-plugin/src/main/java/com/mojo/consoleplus/socket/SocketDataWait.java b/gc-plugin/src/main/java/com/mojo/consoleplus/socket/SocketDataWait.java new file mode 100644 index 0000000..df87cf7 --- /dev/null +++ b/gc-plugin/src/main/java/com/mojo/consoleplus/socket/SocketDataWait.java @@ -0,0 +1,60 @@ +package com.mojo.consoleplus.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/gc-plugin/src/main/java/com/mojo/consoleplus/socket/SocketServer.java b/gc-plugin/src/main/java/com/mojo/consoleplus/socket/SocketServer.java new file mode 100644 index 0000000..4ae389a --- /dev/null +++ b/gc-plugin/src/main/java/com/mojo/consoleplus/socket/SocketServer.java @@ -0,0 +1,239 @@ +package com.mojo.consoleplus.socket; + +import com.mojo.consoleplus.ConsolePlus; +import com.mojo.consoleplus.socket.packet.*; +import com.mojo.consoleplus.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() { + try { + int port = ConsolePlus.config.socketPort; + mLogger = ConsolePlus.logger; + new Timer().schedule(new SocketClientCheck(), 500); + new WaitClientConnect(port); + } catch (Throwable e) { + mLogger.error("[Mojo Console] Socket server start failed", e); + } + } + + // 向全部客户端发送数据 + 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("[Mojo Console] 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("[Mojo Console] 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("[Mojo Console] 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("[Mojo Console] Client {} timeout, disconnect.", clientID); + clientList.remove(clientID); + clientTimeout.remove(clientID); + SocketData.playerList.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 boolean auth = false; + + private final HashMap> socketDataWaitList = new HashMap<>(); + + public ClientThread(Socket accept) { + socket = accept; + address = socket.getInetAddress() + ":" + socket.getPort(); + token = ConsolePlus.config.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) { + try { + String data = SocketUtils.readString(is); + Packet packet = Grasscutter.getGsonFactory().fromJson(data, Packet.class); + if (packet.type == PacketEnum.AuthPacket) { + AuthPacket authPacket = Grasscutter.getGsonFactory().fromJson(packet.data, AuthPacket.class); + if (authPacket.token.equals(token)) { + mLogger.info("[Mojo Console] Client {} auth success.", address); + auth = true; + clientList.put(address, this); + clientTimeout.put(address, 0); + sendPacket(SocketUtils.getPacket(new SignaturePacket(ConsolePlus.authHandler.getSignature()))); + } else { + mLogger.error("[Mojo Console] AuthPacket: {} auth filed.", address); + socket.close(); + return; + } + } + if (!auth) { + mLogger.error("[Mojo Console] AuthPacket: {} not auth", 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("[Mojo Console] HttpPacket: {} not found", packet.packetID); + return; + } + socketWait.setData(httpPacket); + socketDataWaitList.remove(packet.packetID); + } + case OtpPacket -> { + OtpPacket otpPacket = Grasscutter.getGsonFactory().fromJson(packet.data, OtpPacket.class); + if (otpPacket.remove) { + SocketData.tickets.remove(otpPacket.otp); + } else { + SocketData.tickets.put(otpPacket.otp, otpPacket); + } + } + // 心跳包 + case HeartBeat -> { + clientTimeout.put(address, 0); + } + } + } catch (Throwable e) { + e.printStackTrace(); + mLogger.error("[Mojo Console] Client {} disconnect.", address); + clientList.remove(address); + clientTimeout.remove(address); + SocketData.playerList.remove(address); + return; + } + } + } + } + + // 等待客户端连接 + 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("[Mojo Console] Start socket server on port " + socketServer.getLocalPort()); + // noinspection InfiniteLoopStatement + while (true) { + try { + Socket accept = socketServer.accept(); + String address = accept.getInetAddress() + ":" + accept.getPort(); + mLogger.info("[Mojo Console] Client connect: " + address); + new ClientThread(accept); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } +} diff --git a/gc-plugin/src/main/java/com/mojo/consoleplus/socket/SocketUtils.java b/gc-plugin/src/main/java/com/mojo/consoleplus/socket/SocketUtils.java new file mode 100644 index 0000000..e009ae2 --- /dev/null +++ b/gc-plugin/src/main/java/com/mojo/consoleplus/socket/SocketUtils.java @@ -0,0 +1,136 @@ +package com.mojo.consoleplus.socket; + +import com.mojo.consoleplus.ConsolePlus; +import com.mojo.consoleplus.socket.packet.BasePacket; +import com.mojo.consoleplus.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.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.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.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/gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/AuthPacket.java b/gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/AuthPacket.java new file mode 100644 index 0000000..de22b6f --- /dev/null +++ b/gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/AuthPacket.java @@ -0,0 +1,21 @@ +package com.mojo.consoleplus.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/gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/BasePacket.java b/gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/BasePacket.java new file mode 100644 index 0000000..51f5d90 --- /dev/null +++ b/gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/BasePacket.java @@ -0,0 +1,8 @@ +package com.mojo.consoleplus.socket.packet; + +// 基本数据包 +public abstract class BasePacket { + public abstract String getPacket(); + + public abstract PacketEnum getType(); +} diff --git a/gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/HeartBeat.java b/gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/HeartBeat.java new file mode 100644 index 0000000..2dc57e4 --- /dev/null +++ b/gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/HeartBeat.java @@ -0,0 +1,22 @@ +package com.mojo.consoleplus.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/gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/HttpPacket.java b/gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/HttpPacket.java new file mode 100644 index 0000000..3cc9a63 --- /dev/null +++ b/gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/HttpPacket.java @@ -0,0 +1,36 @@ +package com.mojo.consoleplus.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/gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/OtpPacket.java b/gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/OtpPacket.java new file mode 100644 index 0000000..0a80ac5 --- /dev/null +++ b/gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/OtpPacket.java @@ -0,0 +1,35 @@ +package com.mojo.consoleplus.socket.packet; + +import emu.grasscutter.Grasscutter; + +public class OtpPacket extends BasePacket { + public int uid; + public String otp; + public long expire; + public Boolean api; + public String key; + + public boolean remove = false; + + public OtpPacket(int uid, String opt, long expire, Boolean api) { + this.uid = uid; + this.expire = expire; + this.api = api; + this.otp = opt; + } + + public OtpPacket(String opt) { + this.otp = opt; + remove = true; + } + + @Override + public String getPacket() { + return Grasscutter.getGsonFactory().toJson(this); + } + + @Override + public PacketEnum getType() { + return PacketEnum.OtpPacket; + } +} diff --git a/gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/Packet.java b/gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/Packet.java new file mode 100644 index 0000000..0d03031 --- /dev/null +++ b/gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/Packet.java @@ -0,0 +1,13 @@ +package com.mojo.consoleplus.socket.packet; + +// 数据包结构 +public class Packet { + public PacketEnum type; + public String data; + public String packetID; + + @Override + public String toString() { + return "Packet [type=" + type + ", data=" + data + ", packetID=" + packetID + "]"; + } +} diff --git a/gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/PacketEnum.java b/gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/PacketEnum.java new file mode 100644 index 0000000..49569a8 --- /dev/null +++ b/gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/PacketEnum.java @@ -0,0 +1,12 @@ +package com.mojo.consoleplus.socket.packet; + +// 数据包类型列表 +public enum PacketEnum { + PlayerList, + Player, + HttpPacket, + HeartBeat, + Signature, + OtpPacket, + AuthPacket +} diff --git a/gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/SignaturePacket.java b/gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/SignaturePacket.java new file mode 100644 index 0000000..92926cf --- /dev/null +++ b/gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/SignaturePacket.java @@ -0,0 +1,21 @@ +package com.mojo.consoleplus.socket.packet; + +import emu.grasscutter.Grasscutter; + +public class SignaturePacket extends BasePacket { + public String signature; + + public SignaturePacket(String signature) { + this.signature = signature; + } + + @Override + public String getPacket() { + return Grasscutter.getGsonFactory().toJson(this); + } + + @Override + public PacketEnum getType() { + return PacketEnum.Signature; + } +} diff --git a/gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/player/Player.java b/gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/player/Player.java new file mode 100644 index 0000000..f380de5 --- /dev/null +++ b/gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/player/Player.java @@ -0,0 +1,31 @@ +package com.mojo.consoleplus.socket.packet.player; + +import com.mojo.consoleplus.socket.SocketServer; +import com.mojo.consoleplus.socket.packet.BasePacket; +import com.mojo.consoleplus.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/gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/player/PlayerEnum.java b/gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/player/PlayerEnum.java new file mode 100644 index 0000000..01063c9 --- /dev/null +++ b/gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/player/PlayerEnum.java @@ -0,0 +1,7 @@ +package com.mojo.consoleplus.socket.packet.player; + +// 玩家操作列表 +public enum PlayerEnum { + DropMessage, + RunCommand +} \ No newline at end of file diff --git a/gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/player/PlayerList.java b/gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/player/PlayerList.java new file mode 100644 index 0000000..f869a39 --- /dev/null +++ b/gc-plugin/src/main/java/com/mojo/consoleplus/socket/packet/player/PlayerList.java @@ -0,0 +1,32 @@ +package com.mojo.consoleplus.socket.packet.player; + +import com.mojo.consoleplus.socket.packet.BasePacket; +import com.mojo.consoleplus.socket.packet.PacketEnum; +import emu.grasscutter.Grasscutter; + +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; + } + + @Override + public String toString() { + return "PlayerList [player=" + player + ", playerList=" + playerList + ", playerMap=" + playerMap + "]"; + } +}