diff --git a/.gitignore b/.gitignore index c6d98d1..5ff6309 100644 --- a/.gitignore +++ b/.gitignore @@ -1,98 +1,38 @@ -# ---> JetBrains -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf - -# AWS User-specific -.idea/**/aws.xml - -# Generated files -.idea/**/contentModel.xml - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml -.idea/**/libraries - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -# .idea/artifacts -# .idea/compiler.xml -# .idea/jarRepositories.xml -# .idea/modules.xml -# .idea/*.iml -# .idea/modules -# *.iml -# *.ipr - -# CMake -cmake-build-*/ - -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - -# File-based project format -*.iws - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# SonarLint plugin -.idea/sonarlint/ - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -# Editor-based Rest Client -.idea/httpRequests - -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser - -# ---> Maven target/ -pom.xml.tag -pom.xml.releaseBackup -pom.xml.versionsBackup -pom.xml.next -release.properties -dependency-reduced-pom.xml -buildNumber.properties -.mvn/timing.properties -# https://github.com/takari/maven-wrapper#usage-without-binary-jar -.mvn/wrapper/maven-wrapper.jar +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ -# Eclipse m2e generated files -# Eclipse Core -.project -# JDT-specific (Eclipse Java Development Tools) +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated .classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..8029398 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..2b63946 --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 9954939..ce7aa6b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ -# FMCServer +# FMCServer - ForeverMC 服务器插件 -ForeverMC 服务器状态报告插件 \ No newline at end of file +适用版本:1.5.2 + +详细说明请看主分支 README.md diff --git a/lib/install.sh b/lib/install.sh new file mode 100644 index 0000000..b36b37a --- /dev/null +++ b/lib/install.sh @@ -0,0 +1,7 @@ +mvn install:install-file \ + -Dfile=./mcpc.jar \ + -DgroupId=local \ + -DartifactId=fmc-mcpc \ + -Dversion=1.5.2 \ + -Dpackaging=jar \ + -DgeneratePom=true \ No newline at end of file diff --git a/lib/mcpc.jar b/lib/mcpc.jar new file mode 100644 index 0000000..cbf2f3b Binary files /dev/null and b/lib/mcpc.jar differ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..8f81333 --- /dev/null +++ b/pom.xml @@ -0,0 +1,61 @@ + + + 4.0.0 + + cn.forevermc.server.bukkit + FMCServer + 0.0.1+1.5.2 + + + UTF-8 + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.7.1 + + + + cn.forevermc.server.mcpc.FMCServer + + + + jar-with-dependencies + + + + + package + + single + + + + + + + + + + com.google.code.gson + gson + 2.10.1 + + + local + fmc-mcpc + 1.5.2 + provided + + + org.projectlombok + lombok + 1.18.34 + + + diff --git a/src/main/java/cn/forevermc/server/mcpc/FMCServer.java b/src/main/java/cn/forevermc/server/mcpc/FMCServer.java new file mode 100644 index 0000000..60ccee1 --- /dev/null +++ b/src/main/java/cn/forevermc/server/mcpc/FMCServer.java @@ -0,0 +1,90 @@ +package cn.forevermc.server.mcpc; + +import cn.forevermc.server.mcpc.command.DebugCommand; +import cn.forevermc.server.mcpc.command.StaffCommand; +import cn.forevermc.server.mcpc.event.DebugEvent; +import cn.forevermc.server.mcpc.service.ReportService; +import cn.forevermc.server.mcpc.util.Util; +import com.google.gson.Gson; +import lombok.Getter; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitTask; + +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * ForeverMC 服务器状态报告插件 + * + * @author 夜雨 + * @since 2022-11-14 11:05 + */ +public class FMCServer extends JavaPlugin { + + public static final Gson GSON = new Gson(); + + @Getter + private static FileConfiguration configuration; + + private static Logger log; + private BukkitTask statusServiceTask; + + @Override + public void onEnable() { + log = getLogger(); + configuration = getConfig(); + defConfig(); + + { + // config + log.info("debug: " + configuration.getBoolean("debug.enable")); + log.info("debug.staff: " + configuration.getStringList("debug.staff")); + log.info("report: " + configuration.getBoolean("report.enable")); + log.info("report.id: " + configuration.getString("report.id")); + } + + getCommand("debug").setExecutor(new DebugCommand(this)); + getCommand("staff").setExecutor(new StaffCommand(this)); + + getServer().getPluginManager().registerEvents(new DebugEvent(), this); + + if (configuration.getBoolean("report.enable")) { + statusServiceTask = new ReportService(FMCServer.this).runTaskTimerAsynchronously(FMCServer.this, 0, 60); + } + } + + @Override + public void onDisable() { + if (statusServiceTask != null) { + statusServiceTask.cancel(); + } + } + + @Override + public void reloadConfig() { + super.reloadConfig(); + configuration = getConfig(); + } + + public static void log(Level level, String msg) { + log.log(level, msg); + } + + /** 默认配置 */ + private void defConfig() { + configuration.addDefault("debug.enable", false); + configuration.addDefault("debug.staff", new String[0]); + configuration.addDefault("report.enable", false); + configuration.addDefault("report.id", UUID.randomUUID().toString()); + configuration.addDefault("report.api.info", "http://localhost:8091/fmc/server/info"); + configuration.addDefault("report.api.status", "http://localhost:8091/fmc/server/status"); + configuration.addDefault("report.token", ""); + configuration.addDefault("report.name", "FMCServer#" + Util.randomString(8)); + configuration.addDefault("report.clientURL", ""); + configuration.addDefault("report.ip", ""); + configuration.options().copyDefaults(true); + saveConfig(); + } +} diff --git a/src/main/java/cn/forevermc/server/mcpc/bean/ServerStatusRequest.java b/src/main/java/cn/forevermc/server/mcpc/bean/ServerStatusRequest.java new file mode 100644 index 0000000..a4c90d5 --- /dev/null +++ b/src/main/java/cn/forevermc/server/mcpc/bean/ServerStatusRequest.java @@ -0,0 +1,94 @@ +package cn.forevermc.server.mcpc.bean; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 服务器状态 + * + * @author 夜雨 + * @since 2024-08-06 20:38 + */ +@Data +public class ServerStatusRequest { + + private String id; + + private ServerStatus status; + + /** + * @author 夜雨 + * @since 2021-12-02 19:56 + */ + @Data + public static class ServerStatus { + + /** true 为维护中 */ + private boolean isDebugging; + + /** TPS */ + private double tps; + + /** 报告时间 */ + private long reportAt; + + /** JVM 状态 */ + private JVM jvm = new JVM(); + + /** 在线列表 */ + private List onlineList = new ArrayList<>(); + + /** + * 设置 TPS 取平均 + * + * @param tpsList TPS 列表 + */ + public void setTps(double[] tpsList) { + double sum = 0; + for (int i = 0; i < tpsList.length; i++) { + sum += tpsList[i]; + } + tps = sum / tpsList.length; + } + + /** + * JVM 状态 + * + * @author 夜雨 + * @since 2022-11-11 14:52 + */ + @Data + public static class JVM { + + /** CPU 已使用 */ + private double cpuUsed; + + /** 内存状态 */ + private Memory memory = new Memory(); + + /** + * 内存状态 + * + * @author 夜雨 + * @since 2022-11-15 09:54 + */ + @Data + public static class Memory { + + /** 已使用 */ + private long used; + + /** 已申请 */ + private long committed; + + /** 最大内存 */ + private long max; + + /** JVM 名称 */ + private String name; + } + } + } +} diff --git a/src/main/java/cn/forevermc/server/mcpc/command/DebugCommand.java b/src/main/java/cn/forevermc/server/mcpc/command/DebugCommand.java new file mode 100644 index 0000000..265dc21 --- /dev/null +++ b/src/main/java/cn/forevermc/server/mcpc/command/DebugCommand.java @@ -0,0 +1,111 @@ +package cn.forevermc.server.mcpc.command; + +import cn.forevermc.server.mcpc.FMCServer; +import cn.forevermc.server.mcpc.util.Util; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; + +/** + * 维护模式 + * + * @author 夜雨 + * @since 2024-08-07 12:44 + */ +public class DebugCommand implements CommandExecutor { + + /** + * 指令动作 + * + * @author 夜雨 + * @since 2024-08-07 20:15 + */ + private enum Action { + + /** 启用 */ + TRUE, + + /** 禁用 */ + FALSE, + + /** 当前状态 */ + STATUS + } + + private final JavaPlugin plugin; + private final List actionList; + + public DebugCommand(JavaPlugin plugin) { + this.plugin = plugin; + + // 提示 + actionList = new ArrayList<>(); + for (Action action : Action.values()) { + actionList.add(action.toString().toLowerCase()); + } + plugin.getCommand("debug").setTabCompleter(new TabCompleter() { + + @Override + public List onTabComplete(CommandSender commandSender, Command command, String s, String[] args) { + if (args.length == 1) { + return actionList; + } + return new ArrayList<>(); + } + }); + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + FileConfiguration config = FMCServer.getConfiguration(); + + boolean isEnable = config.getBoolean("debug.enable"); + Action action; + if (args.length == 1) { + // 含动作参数 + action = Util.enumValueOf(Action.class, args[0]); + if (action == Action.TRUE) { + isEnable = true; + } + if (action == Action.FALSE) { + isEnable = false; + } + if (action == Action.STATUS) { + if (isEnable) { + Util.returnMsg(sender, Level.INFO, "当前维护模式:启用"); + } else { + Util.returnMsg(sender, Level.INFO, "当前维护模式:禁用"); + } + return true; + } + } else { + // 直接切换 + isEnable = !isEnable; + } + config.set("debug.enable", isEnable); + if (isEnable) { + Player[] players = plugin.getServer().getOnlinePlayers(); + List staffList = config.getStringList("debug.staff"); + // 踢出非维护人员 + for (Player player : players) { + if (!staffList.contains(player.getName())) { + player.kickPlayer("服务器正在维护,请稍后重试"); + } + } + Util.returnMsg(sender, Level.INFO, "已启用维护模式"); + } else { + Util.returnMsg(sender, Level.INFO, "已禁用维护模式"); + } + plugin.saveConfig(); + plugin.reloadConfig(); + return true; + } +} diff --git a/src/main/java/cn/forevermc/server/mcpc/command/StaffCommand.java b/src/main/java/cn/forevermc/server/mcpc/command/StaffCommand.java new file mode 100644 index 0000000..64e27bd --- /dev/null +++ b/src/main/java/cn/forevermc/server/mcpc/command/StaffCommand.java @@ -0,0 +1,104 @@ +package cn.forevermc.server.mcpc.command; + +import cn.forevermc.server.mcpc.FMCServer; +import cn.forevermc.server.mcpc.util.Util; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.plugin.java.JavaPlugin; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.logging.Level; + +/** + * 维护人员操作 + * + * @author 夜雨 + * @since 2024-08-07 12:44 + */ +public class StaffCommand implements CommandExecutor { + + /** + * 指令动作 + * + * @author 夜雨 + * @since 2024-08-07 14:29 + */ + private enum Action { + + /** 添加 */ + ADD, + + /** 移除 */ + REMOVE, + + /** 列表 */ + LIST + } + + private final JavaPlugin plugin; + private final List actionList; + + public StaffCommand(JavaPlugin plugin) { + this.plugin = plugin; + + // 提示 + actionList = new ArrayList<>(); + for (Action action : Action.values()) { + actionList.add(action.toString().toLowerCase()); + } + plugin.getCommand("staff").setTabCompleter(new TabCompleter() { + + @Override + public List onTabComplete(CommandSender commandSender, Command command, String s, String[] args) { + if (args.length == 1) { + return actionList; + } + return new ArrayList<>(); + } + }); + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + FileConfiguration config = FMCServer.getConfiguration(); + Action action = Util.enumValueOf(Action.class, args[0]); + if (action == null) { + Util.returnMsg(sender, Level.WARNING, "使用方式: /staff [add|remove|list] "); + return true; + } + Set staffSet = new HashSet<>(config.getStringList("debug.staff")); + if (action == Action.LIST) { + Util.returnMsg(sender, Level.INFO, "维护人员列表: " + staffSet); + } else { + if (args.length < 2) { + Util.returnMsg(sender, Level.WARNING, "使用方式: /staff [add|remove|list] "); + return true; + } + String value = args[1]; + if (value.isEmpty()) { + Util.returnMsg(sender, Level.WARNING, "请输入维护人员 ID"); + return true; + } + switch (action) { + case ADD: + staffSet.add(value); + Util.returnMsg(sender, Level.INFO, String.format("已添加 %s 维护人员", value)); + break; + case REMOVE: + staffSet.remove(value); + Util.returnMsg(sender, Level.INFO, String.format("已移除 %s 维护人员", value)); + break; + } + config.set("debug.staff", staffSet.toArray(new String[0])); + plugin.saveConfig(); + plugin.reloadConfig(); + } + return true; + } +} diff --git a/src/main/java/cn/forevermc/server/mcpc/event/DebugEvent.java b/src/main/java/cn/forevermc/server/mcpc/event/DebugEvent.java new file mode 100644 index 0000000..fcf5005 --- /dev/null +++ b/src/main/java/cn/forevermc/server/mcpc/event/DebugEvent.java @@ -0,0 +1,30 @@ +package cn.forevermc.server.mcpc.event; + +import cn.forevermc.server.mcpc.FMCServer; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; + +import java.util.List; + +/** + * 调试事件 + * + * @author 夜雨 + * @since 2024-08-07 12:35 + */ +public class DebugEvent implements Listener { + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + FileConfiguration config = FMCServer.getConfiguration(); + if (!config.getBoolean("debug.enable")) { + return; + } + List staff = config.getStringList("debug.staff"); + if (!staff.contains(event.getPlayer().getName())) { + event.getPlayer().kickPlayer("服务器正在维护,请稍后重试"); + } + } +} diff --git a/src/main/java/cn/forevermc/server/mcpc/service/ReportService.java b/src/main/java/cn/forevermc/server/mcpc/service/ReportService.java new file mode 100644 index 0000000..d2d0281 --- /dev/null +++ b/src/main/java/cn/forevermc/server/mcpc/service/ReportService.java @@ -0,0 +1,159 @@ +package cn.forevermc.server.mcpc.service; + +import cn.forevermc.server.mcpc.FMCServer; +import cn.forevermc.server.mcpc.bean.ServerStatusRequest; +import cn.forevermc.server.mcpc.util.HTTP; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.sun.management.OperatingSystemMXBean; +import org.bukkit.Bukkit; +import org.bukkit.Server; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitRunnable; + +import java.io.File; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; + +/** + * 状态报告服务 + * + * @author 夜雨 + * @since 2022-11-11 14:38 + */ +public class ReportService extends BukkitRunnable { + + private final Server server = Bukkit.getServer(); + private final ServerStatusRequest request = new ServerStatusRequest(); + private final MemoryMXBean jvmMemory = ManagementFactory.getMemoryMXBean(); + private final OperatingSystemMXBean operatingSystemMXBean; + + private final String api; + private final JavaPlugin plugin; + private final double[] tpsList = new double[9]; + + /** 失败时间,用于失败时任务执行周期将会更长 */ + private long failAt = -1; + + public ReportService(JavaPlugin plugin) { + this.plugin = plugin; + + api = FMCServer.getConfiguration().getString("report.api.status"); + operatingSystemMXBean = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class); + + if (api == null || api.isEmpty()) { + FMCServer.log(Level.WARNING, "start report task fail because of not found report.api.status in configuration"); + return; + } + request.setId(plugin.getConfig().getString("report.id")); + request.setStatus(new ServerStatusRequest.ServerStatus()); + + // TPS 计算 + server.getScheduler().scheduleSyncRepeatingTask(plugin, new Runnable() { + + private int ticks; + private int tpsI; + private long cs; + private double tps; + + @Override + public void run() { + long s = System.currentTimeMillis() / 1000; + if (cs == s) { + ticks++; + } else { + cs = s; + tpsI++; + tpsList[tpsI % tpsList.length] = tps = tps == 0 ? ticks : ((tps + ticks) / 2); + ticks = 0; + } + } + }, 0, 1); + } + + /** 报告基本信息 */ + private void sendInfo() throws Exception { + FileConfiguration config = FMCServer.getConfiguration(); + String api = config.getString("report.api.info"); + if (api == null || api.isEmpty()) { + FMCServer.log(Level.WARNING, "start report task fail because of not found report.api.info in configuration"); + return; + } + File iconFile = new File(new File(plugin.getDataFolder().getParent()).getParent(), "server-icon.png"); + Server server = plugin.getServer(); + + Map params = new HashMap<>(); + List postFileList = new ArrayList<>(); + + params.put("id", config.getString("report.id").trim()); + params.put("name", config.getString("report.name").trim()); + params.put("core", server.getVersion().trim()); + params.put("bootAt", System.currentTimeMillis()); + params.put("clientURL", config.getString("report.clientURL").trim()); + params.put("maxOnline", server.getMaxPlayers()); + params.put("ip", config.getString("report.ip").trim()); + if (iconFile.exists()) { + HTTP.PostFile file = new HTTP.PostFile(); + file.setField("icon"); + file.setFileName("server-icon.png"); + file.setBytes(Files.readAllBytes(iconFile.toPath())); + postFileList.add(file); + } + String resp = HTTP.post(api, params, postFileList); + JsonObject respObj = JsonParser.parseString(resp).getAsJsonObject(); + Integer code = respObj.get("code").getAsInt(); + if (!code.equals(20000)) { + FMCServer.log(Level.WARNING, "report server info error: " + respObj.get("msg").getAsString()); + } + } + + /** 报告状态 */ + @Override + public void run() { + // 失败时延长周期,(15+-3) 秒 + if (System.currentTimeMillis() - failAt < 1E3 * 15) { + return; + } + ServerStatusRequest.ServerStatus status = request.getStatus(); + status.getOnlineList().clear(); + Player[] players = server.getOnlinePlayers(); + for (Player player : players) { + status.getOnlineList().add(player.getName()); + } + status.setTps(tpsList); + status.setDebugging(FMCServer.getConfiguration().getBoolean("debug.enable")); + status.setReportAt(System.currentTimeMillis()); + status.getJvm().setCpuUsed(operatingSystemMXBean.getProcessCpuLoad()); + status.getJvm().getMemory().setUsed(jvmMemory.getHeapMemoryUsage().getUsed()); + status.getJvm().getMemory().setCommitted(jvmMemory.getHeapMemoryUsage().getCommitted()); + + try { + String resp = HTTP.post(api, HTTP.ContentType.JSON, FMCServer.GSON.toJson(request)); + if (resp == null || resp.isEmpty()) { + FMCServer.log(Level.WARNING, "report fail for empty result"); + return; + } + JsonObject respObj = JsonParser.parseString(resp).getAsJsonObject(); + Integer code = respObj.get("code").getAsInt(); + // 数据中心通知需要发送基本信息,与接口约定返回代码为 20001,消息为 REQUIRED_BASE_INFO + if (code.equals(20001) && respObj.get("msg").getAsString().equals("REQUIRED_BASE_INFO")) { + FMCServer.log(Level.WARNING, "server required send base info"); + sendInfo(); + } else if (!code.equals(20000)) { + failAt = System.currentTimeMillis(); + FMCServer.log(Level.SEVERE, "report fail: " + respObj.get("msg").getAsString()); + } + } catch (Exception ignored) { + failAt = System.currentTimeMillis(); + // 超时,连接失败的视为终端离线 + } + } +} diff --git a/src/main/java/cn/forevermc/server/mcpc/util/HTTP.java b/src/main/java/cn/forevermc/server/mcpc/util/HTTP.java new file mode 100644 index 0000000..c27742f --- /dev/null +++ b/src/main/java/cn/forevermc/server/mcpc/util/HTTP.java @@ -0,0 +1,252 @@ +package cn.forevermc.server.mcpc.util; + +import cn.forevermc.server.mcpc.FMCServer; +import lombok.Data; +import lombok.Getter; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Map; + +/** + * HTTP 操作 + * + * @author 夜雨 + * @since 2024-08-06 14:35 + */ +public class HTTP { + + /** + * 请求类型 + * + * @author 夜雨 + * @version 2022-01-02 12:16 + */ + @Getter + public enum ContentType { + + /** 默认 URL 请求 */ + DEFAULT(null), + + /** FORM 表单模拟 */ + FORM("application/x-www-form-urlencoded"), + + /** JSON */ + JSON("application/json; charset=utf-8"), + + /** 数据流 */ + STREAM("multipart/form-data; charset=utf-8; boundary=B7018kpqFPpgrWAKIR0lHNAanJEqJEyz"); + + final String value; + + ContentType(String value) { + this.value = value; + } + + } + + /** + * 发送 POST 请求 + * + * @param url 请求地址 + * @param type 数据体类型 + * @param data 数据 + * @return 返回结果 + * @throws Exception 请求异常 + */ + public static String post(String url, ContentType type, String data) throws Exception { + HttpURLConnection connection = getConnection(url, type); + connection.setRequestMethod("POST"); + connection.setDoInput(true); + connection.setDoOutput(true); + connection.setUseCaches(false); + connection.setInstanceFollowRedirects(true); + if (data != null && !data.isEmpty()) { + BufferedOutputStream bos = new BufferedOutputStream(connection.getOutputStream()); + bos.write(data.getBytes(StandardCharsets.UTF_8)); + bos.flush(); + bos.close(); + } + + String result = ""; + if (HttpURLConnection.HTTP_OK == connection.getResponseCode()) { + result = toString(connection.getInputStream()); + } + connection.disconnect(); + return result; + } + + public static String post(String url, Map params, List postFileList) throws Exception { + final String newLine = "\r\n"; + final String paramHeader = "Content-Disposition: form-data; name=\"%s\""; + final String fileHeader = "Content-Disposition: form-data; name=\"%s\"; filename=\"%s\""; + final String split = ContentType.STREAM.getValue().split("boundary=")[1]; + + HttpURLConnection connection = getConnection(url, ContentType.STREAM); + connection.setDoInput(true); + connection.setDoOutput(true); + connection.setUseCaches(false); + connection.setRequestMethod("POST"); + connection.setChunkedStreamingMode(4096); + + DataOutputStream dos = new DataOutputStream(connection.getOutputStream()); + { + // 参数 + for (Map.Entry param : params.entrySet()) { + dos.writeBytes("--" + split); + dos.writeBytes(newLine); + // 参数协议 + dos.writeBytes(String.format(paramHeader, param.getKey())); + dos.writeBytes(newLine); + dos.writeBytes("Content-Type: text/plain; charset=UTF-8"); + dos.writeBytes(newLine); + dos.writeBytes("Content-Transfer-Encoding: 8bit"); + dos.writeBytes(newLine); + dos.writeBytes(newLine); + Object value = param.getValue(); + if (value != null) { + dos.write(value.toString().getBytes(StandardCharsets.UTF_8)); + } + dos.writeBytes(newLine); + } + // 文件 + for (PostFile postFile : postFileList) { + ByteArrayInputStream bais = new ByteArrayInputStream(postFile.getBytes()); + dos.writeBytes("--" + split); + dos.writeBytes(newLine); + dos.writeBytes(String.format(fileHeader, postFile.getField(), URLEncoder.encode(postFile.getFileName(), "UTF-8"))); + dos.writeBytes(newLine); + dos.writeBytes("Content-Type: application/octet-stream"); + dos.writeBytes(newLine); + dos.writeBytes(newLine); + + byte[] buffer = new byte[4096]; + int l; + while ((l = bais.read(buffer)) != -1) { + dos.write(buffer, 0, l); + } + // 协议结尾 + dos.writeBytes(newLine); + dos.writeBytes("--" + split + "--"); + dos.writeBytes(newLine); + } + // 协议结尾 + dos.writeBytes(newLine); + dos.writeBytes("--" + split + "--"); + dos.writeBytes(newLine); + dos.flush(); + } + // 返回 + if (connection.getResponseCode() >= 300) { + throw new Exception("HTTP Request error, Response code: " + connection.getResponseCode()); + } + String result = toString(connection.getInputStream()); + connection.disconnect(); + return result; + } + + /** + * 获取连接 + * + * @param url URL 地址 + * @param contentType Content-ContentType 数据类型 + * @return HTTP 连接 + */ + private static HttpURLConnection getConnection(String url, ContentType contentType) throws IOException, NoSuchAlgorithmException, KeyManagementException, URISyntaxException { + // 转 URL 对象 + url = url.startsWith("http") ? url : "http://" + url; + URL connectURL = new URI(url).toURL(); + + HttpURLConnection connection = (HttpURLConnection) connectURL.openConnection(); + // SSL 连接 + if (url.trim().startsWith("https")) { + SSLContext sslcontext = SSLContext.getInstance("TLS"); + sslcontext.init(null, new TrustManager[] {new X509TrustManager() { + + public X509Certificate[] getAcceptedIssuers() { + return null; + } + + public void checkServerTrusted(X509Certificate[] chain, String authType) { + } + + public void checkClientTrusted(X509Certificate[] chain, String authType) { + } + }}, null); + if (connection instanceof HttpsURLConnection) { + ((HttpsURLConnection) connection).setSSLSocketFactory(sslcontext.getSocketFactory()); + } + } + // 数据类型 + if (contentType != null && contentType.getValue() != null) { + connection.setRequestProperty("Content-Type", contentType.getValue()); + } + connection.setRequestProperty("Token", FMCServer.getConfiguration().getString("report.token")); + // 超时 + connection.setReadTimeout(3000); + connection.setConnectTimeout(3000); + return connection; + } + + /** + * 读取数据流为字符串 + * + * @param is 输入流 + * @return 字符串 + * @throws IOException IO 异常 + */ + private static String toString(InputStream is) throws IOException { + StringBuilder sb = new StringBuilder(); + BufferedInputStream bis = new BufferedInputStream(is); + + InputStreamReader isr = new InputStreamReader(bis, StandardCharsets.UTF_8); + + char[] buffer = new char[4096]; + int l; + while ((l = isr.read(buffer)) != -1) { + sb.append(buffer, 0, l); + } + isr.close(); + bis.close(); + is.close(); + return sb.toString(); + } + + /** + * 发送文件 + * + * @author 夜雨 + * @since 2024-08-07 23:50 + */ + @Data + public static class PostFile { + + /** 字段名 */ + private String field; + + /** 文件名 */ + private String fileName; + + /** 字节数据 */ + private byte[] bytes; + } +} diff --git a/src/main/java/cn/forevermc/server/mcpc/util/Util.java b/src/main/java/cn/forevermc/server/mcpc/util/Util.java new file mode 100644 index 0000000..dba5824 --- /dev/null +++ b/src/main/java/cn/forevermc/server/mcpc/util/Util.java @@ -0,0 +1,75 @@ +package cn.forevermc.server.mcpc.util; + +import cn.forevermc.server.mcpc.FMCServer; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.security.SecureRandom; +import java.util.logging.Level; + +/** + * 工具 + * + * @author 夜雨 + * @since 2024-08-06 20:40 + */ +public class Util { + + /** + * 空安全的字符串转枚举 + * + * @param enumType 枚举类 + * @param name 枚举字段字符串 + * @return 枚举 + * @param 枚举类型 + */ + public static > T enumValueOf(Class enumType, String name) { + if (name == null) { + return null; + } + T[] ts = enumType.getEnumConstants(); + if (ts == null) { + throw new IllegalArgumentException(enumType.getName() + " is not an enum type"); + } + for (int i = 0; i < ts.length; i++) { + if (ts[i].name().equalsIgnoreCase(name)) { + return ts[i]; + } + } + return null; + } + + /** + * 输出指令结果,如果是控制台则输出控制台,如果是玩家则输出到玩家 + * + * @param sender 发送者 + * @param level 等级 + * @param msg 消息 + */ + public static void returnMsg(CommandSender sender, Level level, String msg) { + if (sender instanceof Player) { + Player player = (Player) sender; + ChatColor color = level == Level.WARNING ? ChatColor.GREEN : ChatColor.RED; + player.sendMessage(color + msg); + } else { + FMCServer.log(level, msg); + } + } + + /** + * 随机字符串 + * + * @param length 长度 + * @return 随机字符串 + */ + public static String randomString(int length) { + String pool = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + SecureRandom r = new SecureRandom(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + sb.append(pool.charAt(r.nextInt(pool.length() - 1))); + } + return sb.toString(); + } +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..7897d1a --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,17 @@ +name: FMCServer +main: cn.forevermc.server.mcpc.FMCServer +version: '0.0.1' + +author: Timi +website: imyeyu.net +description: This plugin is ForeverMC Server status reporter + +commands: + debug: + description: 启用或禁用维护模式 + usage: "/ [true|false|status]" + permission: op + staff: + description: 添加或移除维护人员 + usage: "/ [add|remove|list] " + permission: op