diff --git a/src/main/java/net/viper/status/StatusAPI.java b/src/main/java/net/viper/status/StatusAPI.java index 20a8946..c6efcd8 100644 --- a/src/main/java/net/viper/status/StatusAPI.java +++ b/src/main/java/net/viper/status/StatusAPI.java @@ -1,602 +1,605 @@ -package net.viper.status; - -import net.md_5.bungee.api.ProxyServer; -import net.md_5.bungee.api.config.ListenerInfo; -import net.md_5.bungee.api.connection.ProxiedPlayer; -import net.md_5.bungee.api.plugin.Plugin; -import net.viper.status.module.ModuleManager; -import net.viper.status.stats.PlayerStats; -import net.viper.status.stats.StatsModule; -import net.viper.status.modules.verify.VerifyModule; -import net.viper.status.modules.commandblocker.CommandBlockerModule; -import net.viper.status.modules.broadcast.BroadcastModule; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; -import java.io.StringReader; -import java.net.ServerSocket; -import java.net.Socket; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.util.*; -import java.util.concurrent.TimeUnit; - -/** - * StatusAPI - zentraler Bungee HTTP-Status- und Broadcast-Endpunkt - */ - -public class StatusAPI extends Plugin implements Runnable { - - private Thread thread; - private int port = 9191; - - private ModuleManager moduleManager; - private UpdateChecker updateChecker; - private Properties verifyProperties; - - @Override - public void onEnable() { - getLogger().info("StatusAPI Core wird initialisiert..."); - - if (!getDataFolder().exists()) { - getDataFolder().mkdirs(); - } - - // Config mergen/updaten - mergeVerifyConfig(); - - // Port aus verify.properties lesen - String portStr = verifyProperties != null ? verifyProperties.getProperty("statusapi.port", "9191") : "9191"; - try { - port = Integer.parseInt(portStr); - } catch (NumberFormatException e) { - getLogger().warning("Ungültiger Port in verify.properties, nutze Standard-Port 9191."); - port = 9191; - } - - moduleManager = new ModuleManager(); - - // Module registrieren - moduleManager.registerModule(new StatsModule()); - moduleManager.registerModule(new VerifyModule()); - moduleManager.registerModule(new BroadcastModule()); - moduleManager.registerModule(new CommandBlockerModule()); - // ForumBridge für /forumlink - try { - Class forumBridge = Class.forName("net.viper.status.modules.forum.ForumBridgeModule"); - Object forumBridgeInstance = forumBridge.getDeclaredConstructor().newInstance(); - moduleManager.registerModule((net.viper.status.module.Module) forumBridgeInstance); - } catch (Exception e) { - getLogger().warning("ForumBridgeModule konnte nicht geladen werden: " + e.getMessage()); - } - - moduleManager.enableAll(this); - - // WebServer starten - getLogger().info("Starte Web-Server auf Port " + port + "..."); - thread = new Thread(this, "StatusAPI-HTTP-Server"); - thread.start(); - - // Update System - String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0"; - updateChecker = new UpdateChecker(this, currentVersion, 6); - // ...fileDownloader entfernt... - - checkAndMaybeUpdate(); - ProxyServer.getInstance().getScheduler().schedule(this, this::checkAndMaybeUpdate, 6, 6, TimeUnit.HOURS); - } - - @Override - public void onDisable() { - getLogger().info("Stoppe Module..."); - if (moduleManager != null) { - moduleManager.disableAll(this); - } - - getLogger().info("Stoppe Web-Server..."); - if (thread != null) { - thread.interrupt(); - try { thread.join(1000); } catch (InterruptedException ignored) {} - } - } - - /** - * Hilfsmethode um Property-Werte zu bereinigen (unescape). - * Entfernt z.B. den Backslash vor URLs wie https\:// - */ - private String unescapePropertiesString(String input) { - if (input == null) return null; - try { - Properties p = new Properties(); - p.load(new StringReader("dummy=" + input)); - return p.getProperty("dummy"); - } catch (IOException e) { - return input; // Fallback, falls Parsing fehlschlägt - } - } - - // --- MERGE LOGIK --- - private void mergeVerifyConfig() { - try { - File file = new File(getDataFolder(), "verify.properties"); - verifyProperties = new Properties(); - if (file.exists()) { - try (FileInputStream fis = new FileInputStream(file)) { - verifyProperties.load(fis); - } - getLogger().info("verify.properties geladen."); - } else { - getLogger().warning("verify.properties nicht gefunden."); - } - } catch (IOException e) { - getLogger().severe("Fehler beim Laden der verify.properties: " + e.getMessage()); - } - } - - public Properties getVerifyProperties() { - synchronized (this) { - return verifyProperties; - } - } - - // --- Update Logik --- - private void checkAndMaybeUpdate() { - try { - updateChecker.checkNow(); - String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0"; - - if (updateChecker.isUpdateAvailable(currentVersion)) { - String newVersion = updateChecker.getLatestVersion(); - String url = updateChecker.getLatestUrl(); - getLogger().warning("----------------------------------------"); - getLogger().warning("Neue Version verfügbar: " + newVersion); - getLogger().warning("Starte automatisches Update..."); - getLogger().warning("----------------------------------------"); - - File pluginFile = getFile(); - File newFile = new File(pluginFile.getParentFile(), "StatusAPI.jar.new"); - - // ...fileDownloader entfernt... - } - } catch (Exception e) { - getLogger().severe("Fehler beim Update-Check: " + e.getMessage()); - } - } - - private void triggerUpdateScript(File currentFile, File newFile) { - try { - File pluginsFolder = currentFile.getParentFile(); - File rootFolder = pluginsFolder.getParentFile(); - File batFile = new File(rootFolder, "StatusAPI_Update_" + System.currentTimeMillis() + ".bat"); - - String batContent = "@echo off\n" + - "echo Bitte warten, der Server fährt herunter...\n" + - "timeout /t 5 /nobreak >nul\n" + - "cd /d \"" + pluginsFolder.getAbsolutePath().replace("\\", "/") + "\"\n" + - "echo Fuehre Datei-Tausch durch...\n" + - "if exist StatusAPI.jar.bak del StatusAPI.jar.bak\n" + - "if exist StatusAPI.jar (\n" + - " ren StatusAPI.jar StatusAPI.jar.bak\n" + - ")\n" + - "if exist StatusAPI.new.jar (\n" + - " ren StatusAPI.new.jar StatusAPI.jar\n" + - " echo Update erfolgreich!\n" + - ") else (\n" + - " echo FEHLER: StatusAPI.new.jar nicht gefunden!\n" + - " pause\n" + - ")\n" + - "del \"%~f0\""; - - try (PrintWriter out = new PrintWriter(batFile)) { out.println(batContent); } - - for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { - p.disconnect("§cServer fährt für ein Update neu herunter. Bitte etwas warten."); - } - - getLogger().info("Starte Update-Skript..."); - Runtime.getRuntime().exec("cmd /c start \"Update_Proc\" \"" + batFile.getAbsolutePath() + "\""); - ProxyServer.getInstance().stop(); - - } catch (Exception e) { - getLogger().severe("Fehler beim Vorbereiten des Updates: " + e.getMessage()); - } - } - - // --- WebServer & JSON --- - @Override - public void run() { - try (ServerSocket serverSocket = new ServerSocket(port)) { - serverSocket.setSoTimeout(1000); - while (!Thread.interrupted()) { - try { - Socket clientSocket = serverSocket.accept(); - handleConnection(clientSocket); - } catch (java.net.SocketTimeoutException e) {} - catch (IOException e) { - getLogger().warning("Fehler beim Akzeptieren einer Verbindung: " + e.getMessage()); - } - } - } catch (IOException e) { - getLogger().severe("Konnte ServerSocket nicht starten: " + e.getMessage()); - } - } - - private void handleConnection(Socket clientSocket) { - - // (doppelter/fehlerhafter Block entfernt) - try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), "UTF-8")); - OutputStream out = clientSocket.getOutputStream()) { - - String inputLine = in.readLine(); - if (inputLine == null) return; - - String[] reqParts = inputLine.split(" "); - if (reqParts.length < 2) return; - String method = reqParts[0].trim(); - String path = reqParts[1].trim(); - - Map headers = new HashMap<>(); - String line; - while ((line = in.readLine()) != null && !line.isEmpty()) { - int idx = line.indexOf(':'); - if (idx > 0) { - String key = line.substring(0, idx).trim().toLowerCase(Locale.ROOT); - String val = line.substring(idx + 1).trim(); - headers.put(key, val); - } - } - - // --- POST /forum/notify --- - if ("POST".equalsIgnoreCase(method) && path.equalsIgnoreCase("/forum/notify")) { - int contentLength = 0; - if (headers.containsKey("content-length")) { - try { contentLength = Integer.parseInt(headers.get("content-length")); } catch (NumberFormatException ignored) {} - } - char[] bodyChars = new char[Math.max(0, contentLength)]; - if (contentLength > 0) { - int read = 0; - while (read < contentLength) { - int r = in.read(bodyChars, read, contentLength - read); - if (r == -1) break; - read += r; - } - } - String body = new String(bodyChars); - String apiKeyHeader = headers.getOrDefault("x-api-key", headers.getOrDefault("x-apikey", "")); - Object mod = moduleManager.getModule("ForumBridgeModule"); - if (mod != null) { - try { - java.lang.reflect.Method m = mod.getClass().getMethod("handleNotify", String.class, String.class); - String resp = (String) m.invoke(mod, body, apiKeyHeader); - sendHttpResponse(out, resp, 200); - } catch (Exception e) { - e.printStackTrace(); - sendHttpResponse(out, "{\"success\":false,\"error\":\"internal\"}", 500); - } - } else { - sendHttpResponse(out, "{\"success\":false,\"error\":\"no_forum_module\"}", 500); - } - return; - } - - // --- POST /broadcast/cancel --- - if ("POST".equalsIgnoreCase(method) && (path.equalsIgnoreCase("/broadcast/cancel") || path.equalsIgnoreCase("/cancel"))) { - int contentLength = 0; - if (headers.containsKey("content-length")) { - try { contentLength = Integer.parseInt(headers.get("content-length")); } catch (NumberFormatException ignored) {} - } - char[] bodyChars = new char[Math.max(0, contentLength)]; - if (contentLength > 0) { - int read = 0; - while (read < contentLength) { - int r = in.read(bodyChars, read, contentLength - read); - if (r == -1) break; - read += r; - } - } - String body = new String(bodyChars); - String clientScheduleId = extractJsonString(body, "clientScheduleId"); - if (clientScheduleId == null || clientScheduleId.isEmpty()) { - sendHttpResponse(out, "{\"success\":false,\"error\":\"missing_clientScheduleId\"}", 400); - return; - } - Object mod = moduleManager.getModule("BroadcastModule"); - if (mod instanceof BroadcastModule) { - boolean ok = ((BroadcastModule) mod).cancelScheduled(clientScheduleId); - if (ok) sendHttpResponse(out, "{\"success\":true}", 200); - else sendHttpResponse(out, "{\"success\":false,\"error\":\"not_found\"}", 404); - return; - } else { - sendHttpResponse(out, "{\"success\":false,\"error\":\"no_broadcast_module\"}", 500); - return; - } - } - - // --- POST /broadcast --- - if ("POST".equalsIgnoreCase(method) && ("/broadcast".equalsIgnoreCase(path) || "/".equals(path) || path.isEmpty())) { - int contentLength = 0; - if (headers.containsKey("content-length")) { - try { contentLength = Integer.parseInt(headers.get("content-length")); } catch (NumberFormatException ignored) {} - } - - char[] bodyChars = new char[Math.max(0, contentLength)]; - if (contentLength > 0) { - int read = 0; - while (read < contentLength) { - int r = in.read(bodyChars, read, contentLength - read); - if (r == -1) break; - read += r; - } - } - String body = new String(bodyChars); - - String apiKeyHeader = headers.getOrDefault("x-api-key", headers.getOrDefault("x-apikey", "")); - - String message = extractJsonString(body, "message"); - String type = extractJsonString(body, "type"); - String prefix = extractJsonString(body, "prefix"); - String prefixColor = extractJsonString(body, "prefixColor"); - String bracketColor = extractJsonString(body, "bracketColor"); - String messageColor = extractJsonString(body, "messageColor"); - String sourceName = extractJsonString(body, "source"); - String scheduleTimeStr = extractJsonString(body, "scheduleTime"); - String recur = extractJsonString(body, "recur"); - String clientScheduleId = extractJsonString(body, "clientScheduleId"); - - if (sourceName == null || sourceName.isEmpty()) sourceName = "PulseCast"; - if (type == null || type.isEmpty()) type = "global"; - - if (scheduleTimeStr != null && !scheduleTimeStr.trim().isEmpty()) { - long scheduleMillis = 0L; - try { - scheduleMillis = Long.parseLong(scheduleTimeStr.trim()); - if (scheduleMillis < 1_000_000_000_000L) scheduleMillis = scheduleMillis * 1000L; - } catch (NumberFormatException ignored) { - try { - double d = Double.parseDouble(scheduleTimeStr.trim()); - long v = (long) d; - if (v < 1_000_000_000_000L) v = v * 1000L; - scheduleMillis = v; - } catch (Exception ex) { - sendHttpResponse(out, "{\"success\":false,\"error\":\"bad_scheduleTime\"}", 400); - return; - } - } - - try { - Object mod = moduleManager.getModule("BroadcastModule"); - if (mod instanceof BroadcastModule) { - BroadcastModule bm = (BroadcastModule) mod; - boolean ok = bm.scheduleBroadcast(scheduleMillis, sourceName, message, type, apiKeyHeader, - prefix, prefixColor, bracketColor, messageColor, (recur == null ? "none" : recur), (clientScheduleId == null ? null : clientScheduleId)); - - if (ok) sendHttpResponse(out, "{\"success\":true}", 200); - else sendHttpResponse(out, "{\"success\":false,\"error\":\"rejected\"}", 403); - return; - } else { - sendHttpResponse(out, "{\"success\":false,\"error\":\"no_broadcast_module\"}", 500); - return; - } - } catch (Throwable t) { - t.printStackTrace(); - sendHttpResponse(out, "{\"success\":false,\"error\":\"internal\"}", 500); - return; - } - } - - if (message == null || message.isEmpty()) { - String resp = "{\"success\":false,\"error\":\"missing_message\"}"; - sendHttpResponse(out, resp, 400); - return; - } - - try { - Object mod = moduleManager.getModule("BroadcastModule"); - if (mod instanceof BroadcastModule) { - BroadcastModule bm = (BroadcastModule) mod; - boolean ok = bm.handleBroadcast(sourceName, message, type, apiKeyHeader, prefix, prefixColor, bracketColor, messageColor); - - if (ok) sendHttpResponse(out, "{\"success\":true}", 200); - else sendHttpResponse(out, "{\"success\":false,\"error\":\"rejected\"}", 403); - return; - } else { - sendHttpResponse(out, "{\"success\":false,\"error\":\"no_broadcast_module\"}", 500); - return; - } - } catch (Throwable t) { - t.printStackTrace(); - sendHttpResponse(out, "{\"success\":false,\"error\":\"internal\"}", 500); - return; - } - } - - // --- GET Handler --- - if (inputLine != null && inputLine.startsWith("GET")) { - Map data = new LinkedHashMap<>(); - data.put("online", true); - - String versionRaw = ProxyServer.getInstance().getVersion(); - String versionClean = (versionRaw != null && versionRaw.contains(":")) ? versionRaw.split(":")[2].trim() : versionRaw; - data.put("version", versionClean); - data.put("max_players", String.valueOf(ProxyServer.getInstance().getConfig().getPlayerLimit())); - - String motd = "BungeeCord"; - try { - Iterator it = ProxyServer.getInstance().getConfig().getListeners().iterator(); - if (it.hasNext()) motd = it.next().getMotd(); - } catch (Exception ignored) {} - data.put("motd", motd); - - StatsModule statsModule = (StatsModule) moduleManager.getModule("StatsModule"); - - boolean luckPermsEnabled = ProxyServer.getInstance().getPluginManager().getPlugin("LuckPerms") != null; - List> playersList = new ArrayList<>(); - - for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { - Map playerInfo = new LinkedHashMap<>(); - playerInfo.put("name", p.getName()); - try { playerInfo.put("uuid", p.getUniqueId().toString()); } catch (Exception ignored) {} - - // Floodgate-Bedrock-Erkennung - boolean isBedrock = false; - String bedrockId = null; - try { - Class floodgateApi = Class.forName("org.geysermc.floodgate.api.FloodgateApi"); - Object api = floodgateApi.getMethod("getInstance").invoke(null); - isBedrock = (boolean) api.getClass().getMethod("isBedrockPlayer", UUID.class).invoke(api, p.getUniqueId()); - if (isBedrock) { - bedrockId = (String) api.getClass().getMethod("getBedrockId", UUID.class).invoke(api, p.getUniqueId()); - } - } catch (Exception ignored) {} - playerInfo.put("isBedrock", isBedrock); - if (bedrockId != null) playerInfo.put("bedrockId", bedrockId); - - String prefix = ""; - if (luckPermsEnabled) { - try { - Class providerClass = Class.forName("net.luckperms.api.LuckPermsProvider"); - Object luckPermsApi = providerClass.getMethod("get").invoke(null); - Object userManager = luckPermsApi.getClass().getMethod("getUserManager").invoke(luckPermsApi); - Object user = userManager.getClass().getMethod("getUser", UUID.class).invoke(userManager, p.getUniqueId()); - if (user != null) { - Class queryOptionsClass = Class.forName("net.luckperms.api.query.QueryOptions"); - Object queryOptions = queryOptionsClass.getMethod("defaultContextualOptions").invoke(null); - Object cachedData = user.getClass().getMethod("getCachedData").invoke(user); - Object metaData = cachedData.getClass().getMethod("getMetaData", queryOptionsClass).invoke(cachedData, queryOptions); - Object result = metaData.getClass().getMethod("getPrefix").invoke(metaData); - if (result != null) prefix = (String) result; - } - } catch (Exception ignored) {} - } - playerInfo.put("prefix", prefix); - - if (statsModule != null) { - PlayerStats ps = statsModule.getManager().getIfPresent(p.getUniqueId()); - if (ps != null) { - playerInfo.put("playtime", ps.getPlaytimeWithCurrentSession()); - playerInfo.put("joins", ps.joins); - playerInfo.put("first_seen", ps.firstSeen); - playerInfo.put("last_seen", ps.lastSeen); - } - } - playersList.add(playerInfo); - } - data.put("players", playersList); - - String json = buildJsonString(data); - byte[] jsonBytes = json.getBytes("UTF-8"); - StringBuilder response = new StringBuilder(); - response.append("HTTP/1.1 200 OK\r\n"); - response.append("Content-Type: application/json; charset=UTF-8\r\n"); - response.append("Access-Control-Allow-Origin: *\r\n"); - response.append("Content-Length: ").append(jsonBytes.length).append("\r\n"); - response.append("Connection: close\r\n\r\n"); - out.write(response.toString().getBytes("UTF-8")); - out.write(jsonBytes); - out.flush(); - } - } catch (Exception e) { - getLogger().severe("Fehler beim Verarbeiten der Anfrage: " + e.getMessage()); - } - } - - private String extractJsonString(String json, String key) { - if (json == null || key == null) return null; - String search = "\"" + key + "\""; - int idx = json.indexOf(search); - if (idx < 0) return null; - int colon = json.indexOf(':', idx + search.length()); - if (colon < 0) return null; - int i = colon + 1; - while (i < json.length() && Character.isWhitespace(json.charAt(i))) i++; - if (i >= json.length()) return null; - char c = json.charAt(i); - if (c == '"') { - i++; - StringBuilder sb = new StringBuilder(); - boolean escape = false; - while (i < json.length()) { - char ch = json.charAt(i++); - if (escape) { - sb.append(ch); - escape = false; - } else { - if (ch == '\\') escape = true; - else if (ch == '"') break; - else sb.append(ch); - } - } - return sb.toString(); - } else { - StringBuilder sb = new StringBuilder(); - while (i < json.length()) { - char ch = json.charAt(i); - if (ch == ',' || ch == '}' || ch == '\n' || ch == '\r') break; - sb.append(ch); - i++; - } - return sb.toString().trim(); - } - } - - private void sendHttpResponse(OutputStream out, String json, int code) throws IOException { - byte[] jsonBytes = json.getBytes(StandardCharsets.UTF_8); - StringBuilder response = new StringBuilder(); - response.append("HTTP/1.1 ").append(code).append(" ").append(code == 200 ? "OK" : "ERROR").append("\r\n"); - response.append("Content-Type: application/json; charset=UTF-8\r\n"); - response.append("Access-Control-Allow-Origin: *\r\n"); - response.append("Content-Length: ").append(jsonBytes.length).append("\r\n"); - response.append("Connection: close\r\n\r\n"); - out.write(response.toString().getBytes(StandardCharsets.UTF_8)); - out.write(jsonBytes); - out.flush(); - } - - private String buildJsonString(Map data) { - StringBuilder sb = new StringBuilder("{"); - boolean first = true; - for (Map.Entry entry : data.entrySet()) { - if (!first) sb.append(","); - first = false; - sb.append("\"").append(escapeJson(entry.getKey())).append("\":").append(valueToString(entry.getValue())); - } - sb.append("}"); - return sb.toString(); - } - - private String valueToString(Object value) { - if (value == null) return "null"; - else if (value instanceof Boolean) return value.toString(); - else if (value instanceof Number) return value.toString(); - else if (value instanceof Map) { - @SuppressWarnings("unchecked") - Map m = (Map) value; - return buildJsonString(m); - } else if (value instanceof List) { - StringBuilder sb = new StringBuilder("["); - List list = (List) value; - for (int i = 0; i < list.size(); i++) { - if (i > 0) sb.append(","); - Object item = list.get(i); - if (item instanceof Map) sb.append(buildJsonString((Map) item)); - else if (item instanceof Number) sb.append(item.toString()); - else sb.append("\"").append(escapeJson(String.valueOf(item))).append("\""); - } - sb.append("]"); - return sb.toString(); - } - else return "\"" + escapeJson(String.valueOf(value)) + "\""; - } - - private String escapeJson(String s) { - if (s == null) return ""; - return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r"); - } +package net.viper.status; + +import net.md_5.bungee.api.ProxyServer; +import net.md_5.bungee.api.config.ListenerInfo; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.plugin.Plugin; +import net.viper.status.module.ModuleManager; +import net.viper.status.stats.PlayerStats; +import net.viper.status.stats.StatsModule; +import net.viper.status.modules.verify.VerifyModule; +import net.viper.status.modules.commandblocker.CommandBlockerModule; +import net.viper.status.modules.broadcast.BroadcastModule; +import net.viper.status.modules.chat.ChatModule; + + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.StringReader; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.*; +import java.util.concurrent.TimeUnit; + +/** + * StatusAPI - zentraler Bungee HTTP-Status- und Broadcast-Endpunkt + */ + +public class StatusAPI extends Plugin implements Runnable { + + private Thread thread; + private int port = 9191; + + private ModuleManager moduleManager; + private UpdateChecker updateChecker; + private Properties verifyProperties; + + @Override + public void onEnable() { + getLogger().info("StatusAPI Core wird initialisiert..."); + + if (!getDataFolder().exists()) { + getDataFolder().mkdirs(); + } + + // Config mergen/updaten + mergeVerifyConfig(); + + // Port aus verify.properties lesen + String portStr = verifyProperties != null ? verifyProperties.getProperty("statusapi.port", "9191") : "9191"; + try { + port = Integer.parseInt(portStr); + } catch (NumberFormatException e) { + getLogger().warning("Ungültiger Port in verify.properties, nutze Standard-Port 9191."); + port = 9191; + } + + moduleManager = new ModuleManager(); + + // Module registrieren + moduleManager.registerModule(new StatsModule()); + moduleManager.registerModule(new VerifyModule()); + moduleManager.registerModule(new BroadcastModule()); + moduleManager.registerModule(new CommandBlockerModule()); + moduleManager.registerModule(new ChatModule()); + + try { + Class forumBridge = Class.forName("net.viper.status.modules.forum.ForumBridgeModule"); + Object forumBridgeInstance = forumBridge.getDeclaredConstructor().newInstance(); + moduleManager.registerModule((net.viper.status.module.Module) forumBridgeInstance); + } catch (Exception e) { + getLogger().warning("ForumBridgeModule konnte nicht geladen werden: " + e.getMessage()); + } + + moduleManager.enableAll(this); + + // WebServer starten + getLogger().info("Starte Web-Server auf Port " + port + "..."); + thread = new Thread(this, "StatusAPI-HTTP-Server"); + thread.start(); + + // Update System + String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0"; + updateChecker = new UpdateChecker(this, currentVersion, 6); + // ...fileDownloader entfernt... + + checkAndMaybeUpdate(); + ProxyServer.getInstance().getScheduler().schedule(this, this::checkAndMaybeUpdate, 6, 6, TimeUnit.HOURS); + } + + @Override + public void onDisable() { + getLogger().info("Stoppe Module..."); + if (moduleManager != null) { + moduleManager.disableAll(this); + } + + getLogger().info("Stoppe Web-Server..."); + if (thread != null) { + thread.interrupt(); + try { thread.join(1000); } catch (InterruptedException ignored) {} + } + } + + /** + * Hilfsmethode um Property-Werte zu bereinigen (unescape). + * Entfernt z.B. den Backslash vor URLs wie https\:// + */ + private String unescapePropertiesString(String input) { + if (input == null) return null; + try { + Properties p = new Properties(); + p.load(new StringReader("dummy=" + input)); + return p.getProperty("dummy"); + } catch (IOException e) { + return input; // Fallback, falls Parsing fehlschlägt + } + } + + // --- MERGE LOGIK --- + private void mergeVerifyConfig() { + try { + File file = new File(getDataFolder(), "verify.properties"); + verifyProperties = new Properties(); + if (file.exists()) { + try (FileInputStream fis = new FileInputStream(file)) { + verifyProperties.load(fis); + } + getLogger().info("verify.properties geladen."); + } else { + getLogger().warning("verify.properties nicht gefunden."); + } + } catch (IOException e) { + getLogger().severe("Fehler beim Laden der verify.properties: " + e.getMessage()); + } + } + + public Properties getVerifyProperties() { + synchronized (this) { + return verifyProperties; + } + } + + // --- Update Logik --- + private void checkAndMaybeUpdate() { + try { + updateChecker.checkNow(); + String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0"; + + if (updateChecker.isUpdateAvailable(currentVersion)) { + String newVersion = updateChecker.getLatestVersion(); + String url = updateChecker.getLatestUrl(); + getLogger().warning("----------------------------------------"); + getLogger().warning("Neue Version verfügbar: " + newVersion); + getLogger().warning("Starte automatisches Update..."); + getLogger().warning("----------------------------------------"); + + File pluginFile = getFile(); + File newFile = new File(pluginFile.getParentFile(), "StatusAPI.jar.new"); + + // ...fileDownloader entfernt... + } + } catch (Exception e) { + getLogger().severe("Fehler beim Update-Check: " + e.getMessage()); + } + } + + private void triggerUpdateScript(File currentFile, File newFile) { + try { + File pluginsFolder = currentFile.getParentFile(); + File rootFolder = pluginsFolder.getParentFile(); + File batFile = new File(rootFolder, "StatusAPI_Update_" + System.currentTimeMillis() + ".bat"); + + String batContent = "@echo off\n" + + "echo Bitte warten, der Server fährt herunter...\n" + + "timeout /t 5 /nobreak >nul\n" + + "cd /d \"" + pluginsFolder.getAbsolutePath().replace("\\", "/") + "\"\n" + + "echo Fuehre Datei-Tausch durch...\n" + + "if exist StatusAPI.jar.bak del StatusAPI.jar.bak\n" + + "if exist StatusAPI.jar (\n" + + " ren StatusAPI.jar StatusAPI.jar.bak\n" + + ")\n" + + "if exist StatusAPI.new.jar (\n" + + " ren StatusAPI.new.jar StatusAPI.jar\n" + + " echo Update erfolgreich!\n" + + ") else (\n" + + " echo FEHLER: StatusAPI.new.jar nicht gefunden!\n" + + " pause\n" + + ")\n" + + "del \"%~f0\""; + + try (PrintWriter out = new PrintWriter(batFile)) { out.println(batContent); } + + for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { + p.disconnect("§cServer fährt für ein Update neu herunter. Bitte etwas warten."); + } + + getLogger().info("Starte Update-Skript..."); + Runtime.getRuntime().exec("cmd /c start \"Update_Proc\" \"" + batFile.getAbsolutePath() + "\""); + ProxyServer.getInstance().stop(); + + } catch (Exception e) { + getLogger().severe("Fehler beim Vorbereiten des Updates: " + e.getMessage()); + } + } + + // --- WebServer & JSON --- + @Override + public void run() { + try (ServerSocket serverSocket = new ServerSocket(port)) { + serverSocket.setSoTimeout(1000); + while (!Thread.interrupted()) { + try { + Socket clientSocket = serverSocket.accept(); + handleConnection(clientSocket); + } catch (java.net.SocketTimeoutException e) {} + catch (IOException e) { + getLogger().warning("Fehler beim Akzeptieren einer Verbindung: " + e.getMessage()); + } + } + } catch (IOException e) { + getLogger().severe("Konnte ServerSocket nicht starten: " + e.getMessage()); + } + } + + private void handleConnection(Socket clientSocket) { + + // (doppelter/fehlerhafter Block entfernt) + try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), "UTF-8")); + OutputStream out = clientSocket.getOutputStream()) { + + String inputLine = in.readLine(); + if (inputLine == null) return; + + String[] reqParts = inputLine.split(" "); + if (reqParts.length < 2) return; + String method = reqParts[0].trim(); + String path = reqParts[1].trim(); + + Map headers = new HashMap<>(); + String line; + while ((line = in.readLine()) != null && !line.isEmpty()) { + int idx = line.indexOf(':'); + if (idx > 0) { + String key = line.substring(0, idx).trim().toLowerCase(Locale.ROOT); + String val = line.substring(idx + 1).trim(); + headers.put(key, val); + } + } + + // --- POST /forum/notify --- + if ("POST".equalsIgnoreCase(method) && path.equalsIgnoreCase("/forum/notify")) { + int contentLength = 0; + if (headers.containsKey("content-length")) { + try { contentLength = Integer.parseInt(headers.get("content-length")); } catch (NumberFormatException ignored) {} + } + char[] bodyChars = new char[Math.max(0, contentLength)]; + if (contentLength > 0) { + int read = 0; + while (read < contentLength) { + int r = in.read(bodyChars, read, contentLength - read); + if (r == -1) break; + read += r; + } + } + String body = new String(bodyChars); + String apiKeyHeader = headers.getOrDefault("x-api-key", headers.getOrDefault("x-apikey", "")); + Object mod = moduleManager.getModule("ForumBridgeModule"); + if (mod != null) { + try { + java.lang.reflect.Method m = mod.getClass().getMethod("handleNotify", String.class, String.class); + String resp = (String) m.invoke(mod, body, apiKeyHeader); + sendHttpResponse(out, resp, 200); + } catch (Exception e) { + e.printStackTrace(); + sendHttpResponse(out, "{\"success\":false,\"error\":\"internal\"}", 500); + } + } else { + sendHttpResponse(out, "{\"success\":false,\"error\":\"no_forum_module\"}", 500); + } + return; + } + + // --- POST /broadcast/cancel --- + if ("POST".equalsIgnoreCase(method) && (path.equalsIgnoreCase("/broadcast/cancel") || path.equalsIgnoreCase("/cancel"))) { + int contentLength = 0; + if (headers.containsKey("content-length")) { + try { contentLength = Integer.parseInt(headers.get("content-length")); } catch (NumberFormatException ignored) {} + } + char[] bodyChars = new char[Math.max(0, contentLength)]; + if (contentLength > 0) { + int read = 0; + while (read < contentLength) { + int r = in.read(bodyChars, read, contentLength - read); + if (r == -1) break; + read += r; + } + } + String body = new String(bodyChars); + String clientScheduleId = extractJsonString(body, "clientScheduleId"); + if (clientScheduleId == null || clientScheduleId.isEmpty()) { + sendHttpResponse(out, "{\"success\":false,\"error\":\"missing_clientScheduleId\"}", 400); + return; + } + Object mod = moduleManager.getModule("BroadcastModule"); + if (mod instanceof BroadcastModule) { + boolean ok = ((BroadcastModule) mod).cancelScheduled(clientScheduleId); + if (ok) sendHttpResponse(out, "{\"success\":true}", 200); + else sendHttpResponse(out, "{\"success\":false,\"error\":\"not_found\"}", 404); + return; + } else { + sendHttpResponse(out, "{\"success\":false,\"error\":\"no_broadcast_module\"}", 500); + return; + } + } + + // --- POST /broadcast --- + if ("POST".equalsIgnoreCase(method) && ("/broadcast".equalsIgnoreCase(path) || "/".equals(path) || path.isEmpty())) { + int contentLength = 0; + if (headers.containsKey("content-length")) { + try { contentLength = Integer.parseInt(headers.get("content-length")); } catch (NumberFormatException ignored) {} + } + + char[] bodyChars = new char[Math.max(0, contentLength)]; + if (contentLength > 0) { + int read = 0; + while (read < contentLength) { + int r = in.read(bodyChars, read, contentLength - read); + if (r == -1) break; + read += r; + } + } + String body = new String(bodyChars); + + String apiKeyHeader = headers.getOrDefault("x-api-key", headers.getOrDefault("x-apikey", "")); + + String message = extractJsonString(body, "message"); + String type = extractJsonString(body, "type"); + String prefix = extractJsonString(body, "prefix"); + String prefixColor = extractJsonString(body, "prefixColor"); + String bracketColor = extractJsonString(body, "bracketColor"); + String messageColor = extractJsonString(body, "messageColor"); + String sourceName = extractJsonString(body, "source"); + String scheduleTimeStr = extractJsonString(body, "scheduleTime"); + String recur = extractJsonString(body, "recur"); + String clientScheduleId = extractJsonString(body, "clientScheduleId"); + + if (sourceName == null || sourceName.isEmpty()) sourceName = "PulseCast"; + if (type == null || type.isEmpty()) type = "global"; + + if (scheduleTimeStr != null && !scheduleTimeStr.trim().isEmpty()) { + long scheduleMillis = 0L; + try { + scheduleMillis = Long.parseLong(scheduleTimeStr.trim()); + if (scheduleMillis < 1_000_000_000_000L) scheduleMillis = scheduleMillis * 1000L; + } catch (NumberFormatException ignored) { + try { + double d = Double.parseDouble(scheduleTimeStr.trim()); + long v = (long) d; + if (v < 1_000_000_000_000L) v = v * 1000L; + scheduleMillis = v; + } catch (Exception ex) { + sendHttpResponse(out, "{\"success\":false,\"error\":\"bad_scheduleTime\"}", 400); + return; + } + } + + try { + Object mod = moduleManager.getModule("BroadcastModule"); + if (mod instanceof BroadcastModule) { + BroadcastModule bm = (BroadcastModule) mod; + boolean ok = bm.scheduleBroadcast(scheduleMillis, sourceName, message, type, apiKeyHeader, + prefix, prefixColor, bracketColor, messageColor, (recur == null ? "none" : recur), (clientScheduleId == null ? null : clientScheduleId)); + + if (ok) sendHttpResponse(out, "{\"success\":true}", 200); + else sendHttpResponse(out, "{\"success\":false,\"error\":\"rejected\"}", 403); + return; + } else { + sendHttpResponse(out, "{\"success\":false,\"error\":\"no_broadcast_module\"}", 500); + return; + } + } catch (Throwable t) { + t.printStackTrace(); + sendHttpResponse(out, "{\"success\":false,\"error\":\"internal\"}", 500); + return; + } + } + + if (message == null || message.isEmpty()) { + String resp = "{\"success\":false,\"error\":\"missing_message\"}"; + sendHttpResponse(out, resp, 400); + return; + } + + try { + Object mod = moduleManager.getModule("BroadcastModule"); + if (mod instanceof BroadcastModule) { + BroadcastModule bm = (BroadcastModule) mod; + boolean ok = bm.handleBroadcast(sourceName, message, type, apiKeyHeader, prefix, prefixColor, bracketColor, messageColor); + + if (ok) sendHttpResponse(out, "{\"success\":true}", 200); + else sendHttpResponse(out, "{\"success\":false,\"error\":\"rejected\"}", 403); + return; + } else { + sendHttpResponse(out, "{\"success\":false,\"error\":\"no_broadcast_module\"}", 500); + return; + } + } catch (Throwable t) { + t.printStackTrace(); + sendHttpResponse(out, "{\"success\":false,\"error\":\"internal\"}", 500); + return; + } + } + + // --- GET Handler --- + if (inputLine != null && inputLine.startsWith("GET")) { + Map data = new LinkedHashMap<>(); + data.put("online", true); + + String versionRaw = ProxyServer.getInstance().getVersion(); + String versionClean = (versionRaw != null && versionRaw.contains(":")) ? versionRaw.split(":")[2].trim() : versionRaw; + data.put("version", versionClean); + data.put("max_players", String.valueOf(ProxyServer.getInstance().getConfig().getPlayerLimit())); + + String motd = "BungeeCord"; + try { + Iterator it = ProxyServer.getInstance().getConfig().getListeners().iterator(); + if (it.hasNext()) motd = it.next().getMotd(); + } catch (Exception ignored) {} + data.put("motd", motd); + + StatsModule statsModule = (StatsModule) moduleManager.getModule("StatsModule"); + + boolean luckPermsEnabled = ProxyServer.getInstance().getPluginManager().getPlugin("LuckPerms") != null; + List> playersList = new ArrayList<>(); + + for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { + Map playerInfo = new LinkedHashMap<>(); + playerInfo.put("name", p.getName()); + try { playerInfo.put("uuid", p.getUniqueId().toString()); } catch (Exception ignored) {} + + // Floodgate-Bedrock-Erkennung + boolean isBedrock = false; + String bedrockId = null; + try { + Class floodgateApi = Class.forName("org.geysermc.floodgate.api.FloodgateApi"); + Object api = floodgateApi.getMethod("getInstance").invoke(null); + isBedrock = (boolean) api.getClass().getMethod("isBedrockPlayer", UUID.class).invoke(api, p.getUniqueId()); + if (isBedrock) { + bedrockId = (String) api.getClass().getMethod("getBedrockId", UUID.class).invoke(api, p.getUniqueId()); + } + } catch (Exception ignored) {} + playerInfo.put("isBedrock", isBedrock); + if (bedrockId != null) playerInfo.put("bedrockId", bedrockId); + + String prefix = ""; + if (luckPermsEnabled) { + try { + Class providerClass = Class.forName("net.luckperms.api.LuckPermsProvider"); + Object luckPermsApi = providerClass.getMethod("get").invoke(null); + Object userManager = luckPermsApi.getClass().getMethod("getUserManager").invoke(luckPermsApi); + Object user = userManager.getClass().getMethod("getUser", UUID.class).invoke(userManager, p.getUniqueId()); + if (user != null) { + Class queryOptionsClass = Class.forName("net.luckperms.api.query.QueryOptions"); + Object queryOptions = queryOptionsClass.getMethod("defaultContextualOptions").invoke(null); + Object cachedData = user.getClass().getMethod("getCachedData").invoke(user); + Object metaData = cachedData.getClass().getMethod("getMetaData", queryOptionsClass).invoke(cachedData, queryOptions); + Object result = metaData.getClass().getMethod("getPrefix").invoke(metaData); + if (result != null) prefix = (String) result; + } + } catch (Exception ignored) {} + } + playerInfo.put("prefix", prefix); + + if (statsModule != null) { + PlayerStats ps = statsModule.getManager().getIfPresent(p.getUniqueId()); + if (ps != null) { + playerInfo.put("playtime", ps.getPlaytimeWithCurrentSession()); + playerInfo.put("joins", ps.joins); + playerInfo.put("first_seen", ps.firstSeen); + playerInfo.put("last_seen", ps.lastSeen); + } + } + playersList.add(playerInfo); + } + data.put("players", playersList); + + String json = buildJsonString(data); + byte[] jsonBytes = json.getBytes("UTF-8"); + StringBuilder response = new StringBuilder(); + response.append("HTTP/1.1 200 OK\r\n"); + response.append("Content-Type: application/json; charset=UTF-8\r\n"); + response.append("Access-Control-Allow-Origin: *\r\n"); + response.append("Content-Length: ").append(jsonBytes.length).append("\r\n"); + response.append("Connection: close\r\n\r\n"); + out.write(response.toString().getBytes("UTF-8")); + out.write(jsonBytes); + out.flush(); + } + } catch (Exception e) { + getLogger().severe("Fehler beim Verarbeiten der Anfrage: " + e.getMessage()); + } + } + + private String extractJsonString(String json, String key) { + if (json == null || key == null) return null; + String search = "\"" + key + "\""; + int idx = json.indexOf(search); + if (idx < 0) return null; + int colon = json.indexOf(':', idx + search.length()); + if (colon < 0) return null; + int i = colon + 1; + while (i < json.length() && Character.isWhitespace(json.charAt(i))) i++; + if (i >= json.length()) return null; + char c = json.charAt(i); + if (c == '"') { + i++; + StringBuilder sb = new StringBuilder(); + boolean escape = false; + while (i < json.length()) { + char ch = json.charAt(i++); + if (escape) { + sb.append(ch); + escape = false; + } else { + if (ch == '\\') escape = true; + else if (ch == '"') break; + else sb.append(ch); + } + } + return sb.toString(); + } else { + StringBuilder sb = new StringBuilder(); + while (i < json.length()) { + char ch = json.charAt(i); + if (ch == ',' || ch == '}' || ch == '\n' || ch == '\r') break; + sb.append(ch); + i++; + } + return sb.toString().trim(); + } + } + + private void sendHttpResponse(OutputStream out, String json, int code) throws IOException { + byte[] jsonBytes = json.getBytes(StandardCharsets.UTF_8); + StringBuilder response = new StringBuilder(); + response.append("HTTP/1.1 ").append(code).append(" ").append(code == 200 ? "OK" : "ERROR").append("\r\n"); + response.append("Content-Type: application/json; charset=UTF-8\r\n"); + response.append("Access-Control-Allow-Origin: *\r\n"); + response.append("Content-Length: ").append(jsonBytes.length).append("\r\n"); + response.append("Connection: close\r\n\r\n"); + out.write(response.toString().getBytes(StandardCharsets.UTF_8)); + out.write(jsonBytes); + out.flush(); + } + + private String buildJsonString(Map data) { + StringBuilder sb = new StringBuilder("{"); + boolean first = true; + for (Map.Entry entry : data.entrySet()) { + if (!first) sb.append(","); + first = false; + sb.append("\"").append(escapeJson(entry.getKey())).append("\":").append(valueToString(entry.getValue())); + } + sb.append("}"); + return sb.toString(); + } + + private String valueToString(Object value) { + if (value == null) return "null"; + else if (value instanceof Boolean) return value.toString(); + else if (value instanceof Number) return value.toString(); + else if (value instanceof Map) { + @SuppressWarnings("unchecked") + Map m = (Map) value; + return buildJsonString(m); + } else if (value instanceof List) { + StringBuilder sb = new StringBuilder("["); + List list = (List) value; + for (int i = 0; i < list.size(); i++) { + if (i > 0) sb.append(","); + Object item = list.get(i); + if (item instanceof Map) sb.append(buildJsonString((Map) item)); + else if (item instanceof Number) sb.append(item.toString()); + else sb.append("\"").append(escapeJson(String.valueOf(item))).append("\""); + } + sb.append("]"); + return sb.toString(); + } + else return "\"" + escapeJson(String.valueOf(value)) + "\""; + } + + private String escapeJson(String s) { + if (s == null) return ""; + return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r"); + } } \ No newline at end of file