Impl token persistence (#19)

This commit is contained in:
2022-10-06 13:27:04 +08:00
parent b70b73667d
commit ef885af137
5 changed files with 128 additions and 50 deletions

View File

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

View File

@ -19,6 +19,7 @@ package com.github.jie65535.opencommand;
import com.github.jie65535.opencommand.json.JsonRequest; import com.github.jie65535.opencommand.json.JsonRequest;
import com.github.jie65535.opencommand.json.JsonResponse; import com.github.jie65535.opencommand.json.JsonResponse;
import com.github.jie65535.opencommand.model.Client;
import com.github.jie65535.opencommand.socket.SocketData; import com.github.jie65535.opencommand.socket.SocketData;
import emu.grasscutter.command.CommandMap; import emu.grasscutter.command.CommandMap;
import emu.grasscutter.server.http.Router; import emu.grasscutter.server.http.Router;
@ -42,17 +43,17 @@ public final class OpenCommandHandler implements Router {
javalin.post("/opencommand/api", OpenCommandHandler::handle); javalin.post("/opencommand/api", OpenCommandHandler::handle);
} }
private static final Map<String, Integer> clients = new HashMap<>();
private static final Map<String, Date> tokenExpireTime = new HashMap<>();
private static final Map<String, Integer> codes = new HashMap<>(); private static final Map<String, Integer> codes = new HashMap<>();
private static final Int2ObjectMap<Date> codeExpireTime = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap<Date> codeExpireTime = new Int2ObjectOpenHashMap<>();
public static void handle(Context context) { public static void handle(Context context) {
// Trigger cleanup action
cleanupExpiredData();
var plugin = OpenCommandPlugin.getInstance(); var plugin = OpenCommandPlugin.getInstance();
var config = plugin.getConfig(); var config = plugin.getConfig();
var data = plugin.getData();
var now = new Date(); var now = new Date();
// Trigger cleanup action
cleanupExpiredCodes();
data.removeExpiredClients();
var req = context.bodyAsClass(JsonRequest.class); var req = context.bodyAsClass(JsonRequest.class);
if (req.action.equals("sendCode")) { if (req.action.equals("sendCode")) {
@ -74,9 +75,8 @@ public final class OpenCommandHandler implements Router {
token = Utils.bytesToHex(Crypto.createSessionKey(32)); token = Utils.bytesToHex(Crypto.createSessionKey(32));
int code = Utils.randomRange(1000, 9999); int code = Utils.randomRange(1000, 9999);
codeExpireTime.put(playerId, new Date(now.getTime() + config.codeExpirationTime_S * 1000L)); 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); codes.put(token, code);
clients.put(token, playerId); data.addClient(new Client(token, playerId, new Date(now.getTime() + config.tempTokenExpirationTime_S * 1000L)));
player.dropMessage("[Open Command] Verification code: " + code); player.dropMessage("[Open Command] Verification code: " + code);
context.json(new JsonResponse(token)); context.json(new JsonResponse(token));
} }
@ -97,7 +97,8 @@ public final class OpenCommandHandler implements Router {
return; return;
} }
var isConsole = req.token.equals(config.consoleToken); var isConsole = req.token.equals(config.consoleToken);
if (!isConsole && !clients.containsKey(req.token)) { var client = data.getClientByToken(req.token);
if (!isConsole && client == null) {
context.json(new JsonResponse(401, "Unauthorized")); context.json(new JsonResponse(401, "Unauthorized"));
return; return;
} }
@ -131,9 +132,10 @@ public final class OpenCommandHandler implements Router {
if (codes.get(req.token).equals(req.data)) { if (codes.get(req.token).equals(req.data)) {
codes.remove(req.token); codes.remove(req.token);
// update token expire time // update token expire time
tokenExpireTime.put(req.token, new Date(now.getTime() + config.tokenLastUseExpirationTime_H * 60L * 60L * 1000L)); client.tokenExpireTime = new Date(now.getTime() + config.tokenLastUseExpirationTime_H * 60L * 60L * 1000L);
context.json(new JsonResponse()); context.json(new JsonResponse());
plugin.getLogger().info(String.format("Player %d has passed the verification, ip: %s", clients.get(req.token), context.ip())); plugin.getLogger().info(String.format("Player %d has passed the verification, ip: %s", client.playerId, context.ip()));
plugin.saveData();
} else { } else {
context.json(new JsonResponse(400, "Verification failed")); context.json(new JsonResponse(400, "Verification failed"));
} }
@ -142,9 +144,8 @@ public final class OpenCommandHandler implements Router {
} else { } else {
if (req.action.equals("command")) { if (req.action.equals("command")) {
// update token expire time // update token expire time
tokenExpireTime.put(req.token, new Date(now.getTime() + config.tokenLastUseExpirationTime_H * 60L * 60L * 1000L)); client.tokenExpireTime = new Date(now.getTime() + config.tokenLastUseExpirationTime_H * 60L * 60L * 1000L);
var playerId = clients.get(req.token); var player = plugin.getServer().getPlayerByUid(client.playerId);
var player = plugin.getServer().getPlayerByUid(playerId);
var command = req.data.toString(); var command = req.data.toString();
if (player == null) { if (player == null) {
context.json(new JsonResponse(404, "Player not found")); context.json(new JsonResponse(404, "Player not found"));
@ -171,18 +172,10 @@ public final class OpenCommandHandler implements Router {
context.json(new JsonResponse(403, "forbidden")); context.json(new JsonResponse(403, "forbidden"));
} }
private static void cleanupExpiredData() { private static void cleanupExpiredCodes() {
var now = new Date(); var now = new Date();
codeExpireTime.int2ObjectEntrySet().removeIf(entry -> entry.getValue().before(now)); codeExpireTime.int2ObjectEntrySet().removeIf(entry -> entry.getValue().before(now));
if (codeExpireTime.isEmpty())
var it = tokenExpireTime.entrySet().iterator(); codes.clear();
while (it.hasNext()) {
var entry = it.next();
if (entry.getValue().before(now)) {
it.remove();
// remove expired token
clients.remove(entry.getKey());
}
}
} }
} }

View File

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

View File

@ -40,6 +40,8 @@ public final class OpenCommandPlugin extends Plugin {
private OpenCommandConfig config; private OpenCommandConfig config;
private OpenCommandData data;
private Grasscutter.ServerRunMode runMode = Grasscutter.ServerRunMode.HYBRID; private Grasscutter.ServerRunMode runMode = Grasscutter.ServerRunMode.HYBRID;
@Override @Override
@ -47,6 +49,8 @@ public final class OpenCommandPlugin extends Plugin {
instance = this; instance = this;
// 加载配置 // 加载配置
loadConfig(); loadConfig();
// 加载数据
loadData();
// 启动Socket // 启动Socket
startSocket(); startSocket();
} }
@ -77,6 +81,7 @@ public final class OpenCommandPlugin extends Plugin {
@Override @Override
public void onDisable() { public void onDisable() {
saveData();
getLogger().info("[OpenCommand] Disabled"); getLogger().info("[OpenCommand] Disabled");
} }
@ -84,6 +89,10 @@ public final class OpenCommandPlugin extends Plugin {
return config; return config;
} }
public OpenCommandData getData() {
return data;
}
private void loadConfig() { private void loadConfig() {
var configFile = new File(getDataFolder(), "config.json"); var configFile = new File(getDataFolder(), "config.json");
if (!configFile.exists()) { if (!configFile.exists()) {
@ -110,6 +119,31 @@ public final class OpenCommandPlugin extends Plugin {
} }
} }
private void loadData() {
var dataFile = new File(getDataFolder(), "data.json");
if (!dataFile.exists()) {
data = new OpenCommandData();
saveData();
} else {
try {
data = JsonUtils.loadToClass(dataFile.getAbsolutePath(), OpenCommandData.class);
} catch (Exception exception) {
data = new OpenCommandData();
getLogger().error("[OpenCommand] There was an error while trying to load the data from data.json. Please make sure that there are no syntax errors. If you want to start with a default data, delete your existing data.json.");
}
}
}
public void saveData() {
try (var file = new FileWriter(new File(getDataFolder(), "data.json"))) {
file.write(JsonUtils.encode(data));
} catch (IOException e) {
getLogger().error("[OpenCommand] Unable to write to data file.");
} catch (Exception e) {
getLogger().error("[OpenCommand] Unable to save data file.");
}
}
private void startSocket() { private void startSocket() {
if (runMode == Grasscutter.ServerRunMode.GAME_ONLY) { if (runMode == Grasscutter.ServerRunMode.GAME_ONLY) {
getLogger().info("[OpenCommand] Starting socket client..."); getLogger().info("[OpenCommand] Starting socket client...");

View File

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