Add multi server support

This commit is contained in:
方块君 2022-07-22 22:18:05 +08:00
parent aafb6b1430
commit b51006edd0
22 changed files with 1173 additions and 3 deletions

View File

@ -17,6 +17,14 @@ public class AuthHandler {
signatureStub = stub; signatureStub = stub;
} }
public String getSignature() {
return signatureStub;
}
public void setSignature(String signature) {
signatureStub = signature;
}
public Boolean auth(int uid, long expire, String dg) { public Boolean auth(int uid, long expire, String dg) {
return digestUid(uid+":"+expire).equals(dg); return digestUid(uid+":"+expire).equals(dg);
} }

View File

@ -1,5 +1,7 @@
package com.mojo.consoleplus; package com.mojo.consoleplus;
import com.mojo.consoleplus.socket.SocketClient;
import com.mojo.consoleplus.socket.SocketServer;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.command.CommandMap; import emu.grasscutter.command.CommandMap;
import emu.grasscutter.plugin.Plugin; import emu.grasscutter.plugin.Plugin;
@ -12,20 +14,33 @@ import java.io.InputStreamReader;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.mojo.consoleplus.command.PluginCommand; 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 io.javalin.http.staticfiles.Location;
import emu.grasscutter.plugin.PluginConfig; import emu.grasscutter.plugin.PluginConfig;
import static emu.grasscutter.Configuration.PLUGIN; import static emu.grasscutter.Configuration.PLUGIN;
import static emu.grasscutter.Configuration.HTTP_POLICIES; import static emu.grasscutter.Configuration.HTTP_POLICIES;
import com.mojo.consoleplus.config.MojoConfig; import com.mojo.consoleplus.config.MojoConfig;
import org.slf4j.Logger;
public class ConsolePlus extends Plugin{ public class ConsolePlus extends Plugin{
public static MojoConfig config = MojoConfig.loadConfig(); public static MojoConfig config = MojoConfig.loadConfig();
public static String versionTag; public static String versionTag;
public static AuthHandler authHandler; public static AuthHandler authHandler;
public static Logger logger;
public static ConsolePlus instance;
public static ConsolePlus getInstance() {
return instance;
}
@Override @Override
public void onLoad() { public void onLoad() {
instance = this;
logger = getLogger();
try (InputStream in = getClass().getResourceAsStream("/plugin.json"); try (InputStream in = getClass().getResourceAsStream("/plugin.json");
BufferedReader reader = new BufferedReader(new InputStreamReader(in))) { BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
Gson gson = new Gson(); Gson gson = new Gson();
@ -56,10 +71,27 @@ public class ConsolePlus extends Plugin{
return; 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()); CommandMap.getInstance().registerCommand("mojoconsole", new PluginCommand());
this.getLogger().info("[MojoConsole] enabled. Version: " + versionTag); this.getLogger().info("[MojoConsole] enabled. Version: " + versionTag);
authHandler = new AuthHandler();
} }
@Override @Override

View File

@ -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<String> 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<String> 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);
}
}

View File

@ -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<HttpPacket> 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;
}
}
}

View File

@ -6,6 +6,8 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Random; 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.Command;
import emu.grasscutter.command.CommandHandler; import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.mail.Mail; import emu.grasscutter.game.mail.Mail;
@ -59,7 +61,9 @@ public class PluginCommand implements CommandHandler {
} }
CommandHandler.sendMessage(sender, ConsolePlus.config.responseMessageThird.replace("{{OTP}}", otp)); CommandHandler.sendMessage(sender, ConsolePlus.config.responseMessageThird.replace("{{OTP}}", otp));
flushTicket(); 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; return;
} }
String link_type = "webview"; String link_type = "webview";
@ -140,6 +144,7 @@ public class PluginCommand implements CommandHandler {
for (String otp : tickets.keySet()) { for (String otp : tickets.keySet()) {
if (curtime > tickets.get(otp).expire) { if (curtime > tickets.get(otp).expire) {
tickets.remove(otp); tickets.remove(otp);
SocketClient.sendPacket(new OtpPacket(otp));
} }
} }
} }

View File

@ -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 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 responseMessageError = "[MojoConsole] Invalid argument.";
public String responseMessageSuccess = "[MojoConsole] Success!"; public String responseMessageSuccess = "[MojoConsole] Success!";
public String socketToken = "";
public int socketPort = 7812;
public String socketHost = "127.0.0.1";
static public class MailTemplate { static public class MailTemplate {
public String title = "Mojo Console Link"; public String title = "Mojo Console Link";

View File

@ -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<String> 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();
}
}
}
}

View File

@ -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<String, PlayerList> playerList = new HashMap<>();
public static HashMap<String, OtpPacket> 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<String> ret = new AtomicReference<>();
playerList.forEach((key, value) -> {
if (value.playerMap.get(uid) != null) {
ret.set(key);
}
});
return ret.get();
}
}

View File

@ -0,0 +1,60 @@
package com.mojo.consoleplus.socket;
// 异步等待数据返回
public abstract class SocketDataWait<T> 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;
}
}

View File

@ -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<String, ClientThread> clientList = new HashMap<>();
private static final HashMap<String, Integer> 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<String, ClientThread> old = (HashMap<String, ClientThread>) 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<String, Integer> old = (HashMap<String, Integer>) 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<String, SocketDataWait<?>> 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();
}
}
}
}
}

View File

@ -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<String> getPacketAndPackID(BasePacket bPacket) {
Packet packet = new Packet();
packet.type = bPacket.getType();
packet.data = bPacket.getPacket();
packet.packetID = UUID.randomUUID().toString();
List<String> 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;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,8 @@
package com.mojo.consoleplus.socket.packet;
// 基本数据包
public abstract class BasePacket {
public abstract String getPacket();
public abstract PacketEnum getType();
}

View File

@ -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;
}
}

View File

@ -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 + "]";
}
}

View File

@ -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;
}
}

View File

@ -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 + "]";
}
}

View File

@ -0,0 +1,12 @@
package com.mojo.consoleplus.socket.packet;
// 数据包类型列表
public enum PacketEnum {
PlayerList,
Player,
HttpPacket,
HeartBeat,
Signature,
OtpPacket,
AuthPacket
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,7 @@
package com.mojo.consoleplus.socket.packet.player;
// 玩家操作列表
public enum PlayerEnum {
DropMessage,
RunCommand
}

View File

@ -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<String> playerList = new ArrayList<>();
public Map<Integer, String> 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 + "]";
}
}