diff --git a/src/main/java/net/viper/status/StatusAPI.java b/src/main/java/net/viper/status/StatusAPI.java deleted file mode 100644 index 97bf03a..0000000 --- a/src/main/java/net/viper/status/StatusAPI.java +++ /dev/null @@ -1,1451 +0,0 @@ -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.modules.economy.EconomyModule; -import net.viper.status.modules.tablist.TablistModule; -import net.viper.status.modules.scoreboard.ScoreboardModule; -import net.viper.status.modules.antibot.AntiBotModule; -import net.viper.status.modules.network.NetworkInfoModule; -import net.viper.status.modules.network.MultiAccountGuard; -import net.viper.status.modules.AutoMessage.AutoMessageModule; -import net.viper.status.modules.customcommands.CustomCommandModule; -import net.viper.status.modules.serverswitcher.ServerSwitcherModule; -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 net.viper.status.modules.vanish.VanishModule; -import net.viper.status.modules.help.HelpModule; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.StringReader; -import java.net.InetSocketAddress; -import java.net.ServerSocket; -import java.net.Socket; -import java.net.SocketTimeoutException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLong; -import net.md_5.bungee.api.scheduler.ScheduledTask; - -/** - * StatusAPI - zentraler BungeeCord HTTP-Status- und Broadcast-Endpunkt - */ -public class StatusAPI extends Plugin implements Runnable { - - // Welt pro Spieler (UUID -> Weltname), wird von StatusAPIBridge gepusht - public static final ConcurrentHashMap playerWorlds = new ConcurrentHashMap<>(); - - // Kontostand pro Spieler (UUID -> Balance), wird von StatusAPIBridge gepusht - public static final ConcurrentHashMap playerBalances = new ConcurrentHashMap<>(); - - // PlaceholderAPI-Werte pro Spieler (UUID -> (placeholder -> aufgelöster Wert)) - public static final ConcurrentHashMap> playerPapi = new ConcurrentHashMap<>(); - - /** Alle %token%-Tokens aus den Config-Dateien – als JSON-Array für GET /papi/tokens */ - public static volatile String papiTokensJson = "[]"; - - // Debug-Modus (aus verify.properties) - public static boolean DEBUG = false; - - /** Gibt eine Info-Meldung nur im Debug-Modus aus */ - public static void debugLog(Plugin plugin, String message) { - if (DEBUG) plugin.getLogger().info(message); - } - - private volatile Thread thread; - private volatile ServerSocket serverSocket; - private volatile boolean shuttingDown = false; - private int port = 9191; - private ScheduledTask httpWatchdogTask; - private ExecutorService requestExecutor; - private final AtomicLong lastHttpRequestAt = new AtomicLong(0L); - - private ModuleManager moduleManager; - private UpdateChecker updateChecker; - private Properties verifyProperties; - - @Override - public void onEnable() { - - if (!getDataFolder().exists()) { - getDataFolder().mkdirs(); - } - - 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; - } - - // Debug-Modus - DEBUG = verifyProperties != null && Boolean.parseBoolean(verifyProperties.getProperty("debug", "false")); - - moduleManager = new ModuleManager(); - - // Module in korrekter Reihenfolge registrieren - // VanishModule MUSS vor ChatModule registriert werden (VanishProvider-Abhängigkeit) - moduleManager.registerModule(new StatsModule()); - moduleManager.registerModule(new HelpModule()); - moduleManager.registerModule(new VerifyModule()); - moduleManager.registerModule(new BroadcastModule()); - moduleManager.registerModule(new CommandBlockerModule()); - moduleManager.registerModule(new VanishModule()); - moduleManager.registerModule(new ChatModule()); - moduleManager.registerModule(new AntiBotModule()); - moduleManager.registerModule(new NetworkInfoModule()); - moduleManager.registerModule(new MultiAccountGuard()); - moduleManager.registerModule(new AutoMessageModule()); - moduleManager.registerModule(new CustomCommandModule()); - moduleManager.registerModule(new ServerSwitcherModule()); - moduleManager.registerModule(new EconomyModule()); - moduleManager.registerModule(new TablistModule()); - moduleManager.registerModule(new ScoreboardModule()); - - 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); - - // PAPI-Tokens sofort scannen + nochmal nach 5s als Fallback (falls Configs erst beim Enable erstellt) - scanAndPublishPapiTokens(); - ProxyServer.getInstance().getScheduler().schedule(this, this::scanAndPublishPapiTokens, 5, TimeUnit.SECONDS); - - // /statusapi reload Befehl registrieren - ProxyServer.getInstance().getPluginManager().registerCommand(this, new StatusAPICommand(this)); - - // FIX: ScoreboardModule mit NetworkInfoModule verbinden (TPS-Fallback) - try { - net.viper.status.modules.scoreboard.ScoreboardModule sbMod = - (net.viper.status.modules.scoreboard.ScoreboardModule) moduleManager.getModule("ScoreboardModule"); - net.viper.status.modules.network.NetworkInfoModule nimMod = - (net.viper.status.modules.network.NetworkInfoModule) moduleManager.getModule("NetworkInfoModule"); - if (sbMod != null && nimMod != null) { - sbMod.setNetworkInfoModule(nimMod); - getLogger().info("[StatusAPI] ScoreboardModule → NetworkInfoModule TPS-Fallback verbunden."); - } - } catch (Exception e) { - getLogger().warning("[StatusAPI] TPS-Fallback konnte nicht verbunden werden: " + e.getMessage()); - } - - // WebServer starten - shuttingDown = false; - requestExecutor = Executors.newFixedThreadPool(4, r -> { - Thread t = new Thread(r, "StatusAPI-HTTP-Worker"); - t.setDaemon(true); - return t; - }); - startHttpServerThread(); - httpWatchdogTask = ProxyServer.getInstance().getScheduler().schedule( - this, this::ensureHttpServerAlive, 15, 15, TimeUnit.SECONDS); - - // Update-Checker - String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0"; - updateChecker = new UpdateChecker(this, currentVersion, 6); - checkAndMaybeUpdate(); - ProxyServer.getInstance().getScheduler().schedule(this, this::checkAndMaybeUpdate, 6, 6, TimeUnit.HOURS); - } - - @Override - public void onDisable() { - shuttingDown = true; - if (moduleManager != null) { - moduleManager.disableAll(this); - } - if (httpWatchdogTask != null) { - httpWatchdogTask.cancel(); - httpWatchdogTask = null; - } - stopHttpServerThread(); - if (requestExecutor != null) { - requestExecutor.shutdownNow(); - requestExecutor = null; - } - } - - private synchronized void startHttpServerThread() { - if (thread != null && thread.isAlive()) { - return; - } - thread = new Thread(this, "StatusAPI-HTTP-Server"); - thread.setDaemon(true); - thread.start(); - } - - private synchronized void stopHttpServerThread() { - Thread localThread = thread; - if (localThread != null) { - localThread.interrupt(); - } - ServerSocket localServerSocket = serverSocket; - if (localServerSocket != null) { - try { - localServerSocket.close(); - } catch (IOException ignored) { - } - } - if (localThread != null) { - try { - localThread.join(1500); - } catch (InterruptedException ignored) { - Thread.currentThread().interrupt(); - } - } - thread = null; - } - - private void ensureHttpServerAlive() { - if (shuttingDown) return; - Thread t = thread; - if (t == null || !t.isAlive()) { - getLogger().warning("HTTP-Server-Thread war gestoppt und wird neu gestartet."); - startHttpServerThread(); - } - } - - // --- MERGE LOGIK --- - private void mergeVerifyConfig() { - try { - File file = new File(getDataFolder(), "verify.properties"); - verifyProperties = new Properties(); - if (file.exists()) { - try (java.io.InputStreamReader reader = new java.io.InputStreamReader( - new FileInputStream(file), StandardCharsets.UTF_8)) { - verifyProperties.load(reader); - } - } 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; - } - } - - public ModuleManager getModuleManager() { - return moduleManager; - } - - // --- 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(); - getLogger().warning("----------------------------------------"); - getLogger().warning("Neue Version verfügbar: " + newVersion); - getLogger().warning("Download: " + updateChecker.getLatestUrl()); - getLogger().warning("----------------------------------------"); - } - } catch (Exception e) { - getLogger().severe("Fehler beim Update-Check: " + e.getMessage()); - } - } - - // --- WebServer --- - @Override - public void run() { - while (!shuttingDown && !Thread.currentThread().isInterrupted()) { - // FIX #1: reuseAddress muss VOR dem bind() gesetzt werden. - // new ServerSocket(port) bindet sofort → stattdessen unboundenen Socket anlegen. - ServerSocket localServerSocket = null; - try { - localServerSocket = new ServerSocket(); - localServerSocket.setReuseAddress(true); - localServerSocket.setSoTimeout(1000); - localServerSocket.bind(new InetSocketAddress(port)); - this.serverSocket = localServerSocket; - - while (!shuttingDown && !Thread.currentThread().isInterrupted()) { - try { - Socket clientSocket = localServerSocket.accept(); - submitConnection(clientSocket); - } catch (SocketTimeoutException ignored) { - // Poll-Schleife für Interrupt/Shutdown - } catch (IOException e) { - if (!shuttingDown) { - getLogger().warning("Fehler beim Akzeptieren einer Verbindung: " + e.getMessage()); - } - } catch (Throwable t) { - if (!shuttingDown) { - getLogger().severe("Unbehandelter Fehler im HTTP-Accept-Loop: " + t.getMessage()); - } - } - } - } catch (IOException e) { - if (!shuttingDown) { - getLogger().severe("Konnte ServerSocket nicht starten: " + e.getMessage()); - try { - Thread.sleep(2000L); - } catch (InterruptedException ignored) { - Thread.currentThread().interrupt(); - } - } - } finally { - if (localServerSocket != null) { - try { localServerSocket.close(); } catch (IOException ignored) {} - } - serverSocket = null; - } - } - } - - private void submitConnection(Socket clientSocket) { - if (clientSocket == null) return; - try { - clientSocket.setSoTimeout(5000); - clientSocket.setTcpNoDelay(true); - } catch (Exception ignored) {} - - ExecutorService executor = requestExecutor; - if (executor == null || executor.isShutdown()) { - try { clientSocket.close(); } catch (IOException ignored) {} - return; - } - - try { - executor.execute(() -> { - try { - handleConnection(clientSocket); - } finally { - try { clientSocket.close(); } catch (IOException ignored) {} - } - }); - } catch (RejectedExecutionException ex) { - try { clientSocket.close(); } catch (IOException ignored) {} - } - } - - private void handleConnection(Socket clientSocket) { - try (BufferedReader in = new BufferedReader( - new InputStreamReader(clientSocket.getInputStream(), StandardCharsets.UTF_8)); - OutputStream out = clientSocket.getOutputStream()) { - - String inputLine = in.readLine(); - if (inputLine == null) return; - lastHttpRequestAt.set(System.currentTimeMillis()); - - String[] reqParts = inputLine.split(" "); - if (reqParts.length < 2) return; - String method = reqParts[0].trim(); - String path = reqParts[1].trim(); - String pathOnly = path; - int queryIndex = path.indexOf('?'); - if (queryIndex >= 0) pathOnly = path.substring(0, queryIndex); - - // GET /health - if ("GET".equalsIgnoreCase(method) && "/health".equalsIgnoreCase(path)) { - long lastMs = lastHttpRequestAt.get(); - long age = lastMs <= 0L ? -1L : (System.currentTimeMillis() - lastMs); - sendHttpResponse(out, "{\"success\":true,\"online\":true,\"last_request_age_ms\":" + age + "}", 200); - return; - } - - // GET /antibot/security-log - if ("GET".equalsIgnoreCase(method) && "/antibot/security-log".equalsIgnoreCase(pathOnly)) { - Map payload = new LinkedHashMap<>(); - payload.put("success", true); - payload.put("events", loadAntiBotSecurityEvents(250)); - sendHttpResponse(out, buildJsonString(payload), 200); - return; - } - - // Headers lesen - Map headers = new HashMap<>(); - String line; - while ((line = in.readLine()) != null && !line.isEmpty()) { - int idx = line.indexOf(':'); - if (idx > 0) { - headers.put(line.substring(0, idx).trim().toLowerCase(Locale.ROOT), - line.substring(idx + 1).trim()); - } - } - - // GET /network/backendguard/config - if ("GET".equalsIgnoreCase(method) && path.equalsIgnoreCase("/network/backendguard/config")) { - Properties guardProps = loadNetworkGuardProperties(); - String requiredApiKey = guardProps.getProperty("backendguard.sync.api_key", "").trim(); - String providedApiKey = headers.getOrDefault("x-api-key", headers.getOrDefault("x-apikey", "")); - if (!requiredApiKey.isEmpty() && !requiredApiKey.equals(providedApiKey == null ? "" : providedApiKey.trim())) { - sendHttpResponse(out, "{\"success\":false,\"error\":\"bad_api_key\"}", 403); - return; - } - Map payload = new LinkedHashMap<>(); - payload.put("success", true); - Map guard = new LinkedHashMap<>(); - guard.put("enforcement_enabled", Boolean.parseBoolean(guardProps.getProperty("backendguard.enforcement_enabled", "true"))); - guard.put("log_blocked_attempts", Boolean.parseBoolean(guardProps.getProperty("backendguard.log_blocked_attempts", "true"))); - guard.put("kick_message", guardProps.getProperty("backendguard.kick_message", "&cBitte verbinde dich nur ueber den Proxy.")); - guard.put("allowed_proxy_ips", parseCommaListProperty(guardProps.getProperty("backendguard.allowed_proxy_ips", "127.0.0.1,::1"))); - guard.put("allowed_proxy_cidrs", parseCommaListProperty(guardProps.getProperty("backendguard.allowed_proxy_cidrs", ""))); - payload.put("backend_guard", guard); - sendHttpResponse(out, buildJsonString(payload), 200); - return; - } - - // POST /forum/notify - if ("POST".equalsIgnoreCase(method) && path.equalsIgnoreCase("/forum/notify")) { - String body = readBody(in, headers); - 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 /network/attack - if ("POST".equalsIgnoreCase(method) && path.equalsIgnoreCase("/network/attack")) { - String body = readBody(in, headers); - String apiKeyHeader = headers.getOrDefault("x-api-key", headers.getOrDefault("x-apikey", "")); - NetworkInfoModule mod = (NetworkInfoModule) moduleManager.getModule("NetworkInfoModule"); - if (mod == null || !mod.isEnabled() || !mod.isAttackNotificationsEnabled()) { - sendHttpResponse(out, "{\"success\":false,\"error\":\"network_module_disabled\"}", 403); - return; - } - if (!mod.isAttackApiKeyValid(apiKeyHeader)) { - sendHttpResponse(out, "{\"success\":false,\"error\":\"bad_api_key\"}", 403); - return; - } - String eventType = extractJsonString(body, "event"); - if (eventType == null || eventType.trim().isEmpty()) eventType = "detected"; - String source = extractJsonString(body, "source"); - - Integer cps = null, blockedIps = null; - Long blockedConnections = null; - String cpsStr = extractJsonString(body, "connectionsPerSecond"); - if (cpsStr == null || cpsStr.isEmpty()) cpsStr = extractJsonString(body, "cps"); - try { if (cpsStr != null && !cpsStr.isEmpty()) cps = Integer.valueOf(cpsStr.trim()); } catch (Exception ignored) {} - String blockedIpsStr = extractJsonString(body, "ipAddressesBlocked"); - if (blockedIpsStr == null || blockedIpsStr.isEmpty()) blockedIpsStr = extractJsonString(body, "blockedIps"); - try { if (blockedIpsStr != null && !blockedIpsStr.isEmpty()) blockedIps = Integer.valueOf(blockedIpsStr.trim()); } catch (Exception ignored) {} - String blockedConnectionsStr = extractJsonString(body, "connectionsBlocked"); - if (blockedConnectionsStr == null || blockedConnectionsStr.isEmpty()) blockedConnectionsStr = extractJsonString(body, "blockedConnections"); - try { if (blockedConnectionsStr != null && !blockedConnectionsStr.isEmpty()) blockedConnections = Long.valueOf(blockedConnectionsStr.trim()); } catch (Exception ignored) {} - - boolean sent = mod.sendAttackNotification(eventType, cps, blockedIps, blockedConnections, source); - sendHttpResponse(out, sent ? "{\"success\":true}" : "{\"success\":false,\"error\":\"webhook_disabled_or_missing\"}", sent ? 200 : 400); - return; - } - - // POST /broadcast/cancel - if ("POST".equalsIgnoreCase(method) && (path.equalsIgnoreCase("/broadcast/cancel") || path.equalsIgnoreCase("/cancel"))) { - String body = readBody(in, headers); - 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); - sendHttpResponse(out, ok ? "{\"success\":true}" : "{\"success\":false,\"error\":\"not_found\"}", ok ? 200 : 404); - } else { - sendHttpResponse(out, "{\"success\":false,\"error\":\"no_broadcast_module\"}", 500); - } - return; - } - - // POST /broadcast - if ("POST".equalsIgnoreCase(method) && ("/broadcast".equalsIgnoreCase(path) || "/".equals(path) || path.isEmpty())) { - String body = readBody(in, headers); - 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"; - - Object mod = moduleManager.getModule("BroadcastModule"); - if (!(mod instanceof BroadcastModule)) { - sendHttpResponse(out, "{\"success\":false,\"error\":\"no_broadcast_module\"}", 500); - return; - } - BroadcastModule bm = (BroadcastModule) mod; - - if (scheduleTimeStr != null && !scheduleTimeStr.trim().isEmpty()) { - long scheduleMillis; - try { - scheduleMillis = Long.parseLong(scheduleTimeStr.trim()); - if (scheduleMillis < 1_000_000_000_000L) scheduleMillis *= 1000L; - } catch (NumberFormatException ignored) { - try { - long v = (long) Double.parseDouble(scheduleTimeStr.trim()); - scheduleMillis = v < 1_000_000_000_000L ? v * 1000L : v; - } catch (Exception ex) { - sendHttpResponse(out, "{\"success\":false,\"error\":\"bad_scheduleTime\"}", 400); - return; - } - } - boolean ok = bm.scheduleBroadcast(scheduleMillis, sourceName, message, type, apiKeyHeader, - prefix, prefixColor, bracketColor, messageColor, - recur == null ? "none" : recur, clientScheduleId); - sendHttpResponse(out, ok ? "{\"success\":true}" : "{\"success\":false,\"error\":\"rejected\"}", ok ? 200 : 403); - return; - } - - if (message == null || message.isEmpty()) { - sendHttpResponse(out, "{\"success\":false,\"error\":\"missing_message\"}", 400); - return; - } - boolean ok = bm.handleBroadcast(sourceName, message, type, apiKeyHeader, prefix, prefixColor, bracketColor, messageColor); - sendHttpResponse(out, ok ? "{\"success\":true}" : "{\"success\":false,\"error\":\"rejected\"}", ok ? 200 : 403); - return; - } - - // GET /stats/player?uuid=... oder ?name=... - if ("GET".equalsIgnoreCase(method) && "/stats/player".equalsIgnoreCase(pathOnly)) { - Map qp = parseQueryParams(path); - StatsModule statsMod = (StatsModule) moduleManager.getModule("StatsModule"); - PlayerStats ps = resolvePlayer(qp.get("uuid"), qp.get("name"), statsMod); - if (ps == null) { - sendHttpResponse(out, "{\"success\":false,\"error\":\"player_not_found\"}", 404); - return; - } - Map payload = new LinkedHashMap<>(); - payload.put("success", true); - Map playerMap = new LinkedHashMap<>(); - playerMap.put("uuid", ps.uuid.toString()); - playerMap.put("name", ps.name); - playerMap.put("first_seen", ps.firstSeen); - playerMap.put("last_seen", ps.lastSeen); - playerMap.put("playtime", ps.getPlaytimeWithCurrentSession()); - playerMap.put("joins", ps.joins); - playerMap.put("kills", ps.kills); - playerMap.put("deaths", ps.deaths); - playerMap.put("online", ProxyServer.getInstance().getPlayer(ps.uuid) != null); - // Balance direkt aus MySQL (serverübergreifend) - double playerBalance = playerBalances.getOrDefault(ps.uuid, 0.0); - Map economy = new LinkedHashMap<>(); - economy.put("balance", playerBalance); - economy.put("total_earned", ps.totalEarned); - economy.put("total_spent", ps.totalSpent); - economy.put("transactions_count", ps.transactionsCount); - playerMap.put("economy", economy); - Map punishments = new LinkedHashMap<>(); - punishments.put("bans", ps.bansCount); - punishments.put("mutes", ps.mutesCount); - punishments.put("warns", ps.warnsCount); - punishments.put("last_punishment_at", ps.lastPunishmentAt); - punishments.put("last_punishment_type", ps.lastPunishmentType != null ? ps.lastPunishmentType : ""); - punishments.put("punishment_score", ps.punishmentScore); - playerMap.put("punishments", punishments); - payload.put("player", playerMap); - sendHttpResponse(out, buildJsonString(payload), 200); - return; - } - - // GET /economy/player?uuid=... oder ?name=... - // Kein Cache – UUID und Balance kommen direkt aus der DB - if ("GET".equalsIgnoreCase(method) && "/economy/player".equalsIgnoreCase(pathOnly)) { - Map qp = parseQueryParams(path); - // UUID auflösen aus Query-Param - UUID ecoUuid = null; - String ecoName = null; - String uuidParam = qp.get("uuid"); - String nameParam = qp.get("name"); - if (uuidParam != null && !uuidParam.isEmpty()) { - try { ecoUuid = UUID.fromString(uuidParam.trim()); } catch (IllegalArgumentException ignored) {} - } - if (ecoUuid == null) { - sendHttpResponse(out, "{\"success\":false,\"error\":\"player_not_found\"}", 404); - return; - } - if (ecoName == null) ecoName = uuidParam; - // Balance aus playerBalances Map lesen (befüllt von StatusAPIBridge via NexEco) - double directBalance = playerBalances.getOrDefault(ecoUuid, 0.0); - Map payload = new LinkedHashMap<>(); - payload.put("success", true); - payload.put("uuid", ecoUuid.toString()); - payload.put("name", ecoName); - Map economy = new LinkedHashMap<>(); - economy.put("balance", directBalance); - payload.put("economy", economy); - sendHttpResponse(out, buildJsonString(payload), 200); - return; - } - - // POST /economy/update - // Empfängt Balance-Updates von StatusAPIBridge (Vault/NexEco → HTTP) - // Schreibt NUR in playerBalances für Tablist/Scoreboard – KEINE DB-Schreiboperationen - if ("POST".equalsIgnoreCase(method) && "/economy/update".equalsIgnoreCase(pathOnly)) { - String body = readBody(in, headers); - // UUID auflösen - UUID ecoUpdUuid = null; - String uuidBody = extractJsonString(body, "uuid"); - if (uuidBody != null && !uuidBody.isEmpty()) { - try { ecoUpdUuid = UUID.fromString(uuidBody.trim()); } catch (IllegalArgumentException ignored) {} - } - if (ecoUpdUuid == null) { - sendHttpResponse(out, "{\"success\":false,\"error\":\"player_not_found\"}", 404); - return; - } - // Balance NUR in playerBalances Map speichern (für Tablist/Scoreboard) - // Die echte DB-Verwaltung macht ausschließlich NexEco - String balStr = extractJsonString(body, "balance"); - if (balStr != null && !balStr.isEmpty()) { - try { - double newBal = Double.parseDouble(balStr); - playerBalances.put(ecoUpdUuid, newBal); - } catch (NumberFormatException ignored) {} - } - // Stats-Felder (total_earned etc.) im Cache aktualisieren, falls vorhanden - StatsModule statsModEco = (StatsModule) moduleManager.getModule("StatsModule"); - if (statsModEco != null) { - PlayerStats psEco = statsModEco.getManager().getIfPresent(ecoUpdUuid); - if (psEco != null) { - String earnStr = extractJsonString(body, "total_earned"); - if (earnStr == null) earnStr = extractJsonString(body, "totalEarned"); - String spentStr = extractJsonString(body, "total_spent"); - if (spentStr == null) spentStr = extractJsonString(body, "totalSpent"); - String txStr = extractJsonString(body, "transactions_count"); - if (txStr == null) txStr = extractJsonString(body, "transactionsCount"); - synchronized (psEco) { - try { if (balStr != null && !balStr.isEmpty()) psEco.balance = Double.parseDouble(balStr); } catch (Exception ignored) {} - try { if (earnStr != null && !earnStr.isEmpty()) psEco.totalEarned = Double.parseDouble(earnStr); } catch (Exception ignored) {} - try { if (spentStr != null && !spentStr.isEmpty()) psEco.totalSpent = Double.parseDouble(spentStr); } catch (Exception ignored) {} - try { if (txStr != null && !txStr.isEmpty()) psEco.transactionsCount = Integer.parseInt(txStr); } catch (Exception ignored) {} - } - } - } - sendHttpResponse(out, "{\"success\":true}", 200); - return; - } - - // POST /punishment/update - if ("POST".equalsIgnoreCase(method) && "/punishment/update".equalsIgnoreCase(pathOnly)) { - String body = readBody(in, headers); - StatsModule statsModPun = (StatsModule) moduleManager.getModule("StatsModule"); - PlayerStats psPun = resolvePlayer(extractJsonString(body, "uuid"), extractJsonString(body, "name"), statsModPun); - if (psPun == null) { - sendHttpResponse(out, "{\"success\":false,\"error\":\"player_not_found\"}", 404); - return; - } - String bansStr = extractJsonString(body, "bans"); - String mutesStr = extractJsonString(body, "mutes"); - String warnsStr = extractJsonString(body, "warns"); - String lastAtStr = extractJsonString(body, "last_punishment_at"); - if (lastAtStr == null) lastAtStr = extractJsonString(body, "lastPunishmentAt"); - String typeStr = extractJsonString(body, "last_punishment_type"); - if (typeStr == null) typeStr = extractJsonString(body, "lastPunishmentType"); - String scoreStr = extractJsonString(body, "punishment_score"); - if (scoreStr == null) scoreStr = extractJsonString(body, "punishmentScore"); - // Typ auf erlaubte Werte begrenzen - String safeType = null; - if (typeStr != null) { - String t = typeStr.trim().toLowerCase(Locale.ROOT); - safeType = (t.equals("ban") || t.equals("mute") || t.equals("warn") || t.equals("kick")) ? t : ""; - } - synchronized (psPun) { - try { if (bansStr != null && !bansStr.isEmpty()) psPun.bansCount = Integer.parseInt(bansStr); } catch (Exception ignored) {} - try { if (mutesStr != null && !mutesStr.isEmpty()) psPun.mutesCount = Integer.parseInt(mutesStr); } catch (Exception ignored) {} - try { if (warnsStr != null && !warnsStr.isEmpty()) psPun.warnsCount = Integer.parseInt(warnsStr); } catch (Exception ignored) {} - try { if (lastAtStr != null && !lastAtStr.isEmpty()) psPun.lastPunishmentAt = Long.parseLong(lastAtStr); } catch (Exception ignored) {} - if (safeType != null) psPun.lastPunishmentType = safeType; - try { if (scoreStr != null && !scoreStr.isEmpty()) psPun.punishmentScore = Integer.parseInt(scoreStr); } catch (Exception ignored) {} - } - sendHttpResponse(out, "{\"success\":true}", 200); - return; - } - - // POST /stats/update – Kills und Deaths eines Spielers aktualisieren (von Backend-Server via StatusAPIBridge) - if ("POST".equalsIgnoreCase(method) && "/stats/update".equalsIgnoreCase(pathOnly)) { - String body = readBody(in, headers); - StatsModule statsModUpd = (StatsModule) moduleManager.getModule("StatsModule"); - if (statsModUpd == null) { - sendHttpResponse(out, "{\"success\":false,\"error\":\"stats_module_unavailable\"}", 503); - return; - } - String uuidStr = extractJsonString(body, "uuid"); - String nameStr = extractJsonString(body, "name"); - PlayerStats psUpd = resolvePlayer(uuidStr, nameStr, statsModUpd); - if (psUpd == null) { - // Spieler noch nicht bekannt → ignorieren (er hat sich noch nicht eingeloggt) - sendHttpResponse(out, "{\"success\":false,\"error\":\"player_not_found\"}", 404); - return; - } - String killsStr = extractJsonString(body, "kills"); - String deathsStr = extractJsonString(body, "deaths"); - // Playtime auch updaten - String playtimeStr = extractJsonString(body, "playtime"); - synchronized (psUpd) { - // HÖCHSTER WERT gewinnt – mehrere Unterserver können unterschiedliche Werte haben - try { if (killsStr != null && !killsStr.isEmpty()) { - int v = Integer.parseInt(killsStr.trim()); - if (v > psUpd.kills) psUpd.kills = v; - }} catch (Exception ignored) {} - try { if (deathsStr != null && !deathsStr.isEmpty()) { - int v = Integer.parseInt(deathsStr.trim()); - if (v > psUpd.deaths) psUpd.deaths = v; - }} catch (Exception ignored) {} - try { if (playtimeStr != null && !playtimeStr.isEmpty()) { - long v = Long.parseLong(playtimeStr.trim()); - if (v > psUpd.totalPlaytime) psUpd.totalPlaytime = v; - }} catch (Exception ignored) {} - } - sendHttpResponse(out, "{\"success\":true}", 200); - return; - } - - // POST /scoreboard/health – Leben eines Spielers aktualisieren (von StatusAPIBridge) - if ("POST".equalsIgnoreCase(method) && "/scoreboard/health".equalsIgnoreCase(pathOnly)) { - String body = readBody(in, headers); - String uuidStr = extractJsonString(body, "uuid"); - String healthStr = extractJsonString(body, "health"); - if (uuidStr != null && !uuidStr.isEmpty() && healthStr != null && !healthStr.isEmpty()) { - try { - UUID hUuid = UUID.fromString(uuidStr.trim()); - double health = Double.parseDouble(healthStr.trim()); - net.viper.status.modules.scoreboard.ScoreboardModule.playerHealth.put(hUuid, health); - } catch (Exception ignored) {} - } - sendHttpResponse(out, "{\"success\":true}", 200); - return; - } - - // POST /scoreboard/compass – Himmelsrichtung eines Spielers (von StatusAPIBridge) - if ("POST".equalsIgnoreCase(method) && "/scoreboard/compass".equalsIgnoreCase(pathOnly)) { - String body = readBody(in, headers); - String uuidStr = extractJsonString(body, "uuid"); - String compassStr = extractJsonString(body, "compass"); - if (uuidStr != null && !uuidStr.isEmpty() && compassStr != null && !compassStr.isEmpty()) { - try { - UUID cUuid = UUID.fromString(uuidStr.trim()); - net.viper.status.modules.scoreboard.ScoreboardModule.playerCompass.put(cUuid, compassStr.trim()); - } catch (Exception ignored) {} - } - sendHttpResponse(out, "{\"success\":true}", 200); - return; - } - - // POST /scoreboard/tps – TPS des Spieler-Servers (von StatusAPIBridge) - if ("POST".equalsIgnoreCase(method) && "/scoreboard/tps".equalsIgnoreCase(pathOnly)) { - String body = readBody(in, headers); - String uuidStr = extractJsonString(body, "uuid"); - String tpsStr = extractJsonString(body, "tps"); - if (uuidStr != null && !uuidStr.isEmpty() && tpsStr != null && !tpsStr.isEmpty()) { - try { - UUID tUuid = UUID.fromString(uuidStr.trim()); - double tps = Double.parseDouble(tpsStr.trim()); - net.viper.status.modules.scoreboard.ScoreboardModule.playerTps.put(tUuid, tps); - } catch (Exception ignored) {} - } - sendHttpResponse(out, "{\"success\":true}", 200); - return; - } - - // POST /player/world – Welt eines Spielers aktualisieren (von StatusAPIBridge) - if ("POST".equalsIgnoreCase(method) && "/player/world".equalsIgnoreCase(pathOnly)) { - String body = readBody(in, headers); - String uuidStr = extractJsonString(body, "uuid"); - String worldStr = extractJsonString(body, "world"); - if (uuidStr != null && !uuidStr.isEmpty() && worldStr != null && !worldStr.isEmpty()) { - try { - playerWorlds.put(UUID.fromString(uuidStr.trim()), worldStr.trim()); - } catch (IllegalArgumentException ignored) {} - } - sendHttpResponse(out, "{\"success\":true}", 200); - return; - } - - // POST /ticket/update – TicketSystem Daten (von StatusAPIBridge) - if ("POST".equalsIgnoreCase(method) && "/ticket/update".equalsIgnoreCase(pathOnly)) { - String body = readBody(in, headers); - String uuidStr = extractJsonString(body, "uuid"); - if (uuidStr != null && !uuidStr.isEmpty()) { - try { - UUID uid = UUID.fromString(uuidStr.trim()); - String myOpen = extractJsonString(body, "my_open"); - if (myOpen != null) - net.viper.status.modules.scoreboard.ScoreboardModule.ticketMyOpen.put(uid, Integer.parseInt(myOpen)); - } catch (Exception ignored) {} - } - try { - String totOpen = extractJsonString(body, "total_open"); - String totClaimed = extractJsonString(body, "total_claimed"); - String ratGood = extractJsonString(body, "rating_good"); - String ratBad = extractJsonString(body, "rating_bad"); - if (totOpen != null) net.viper.status.modules.scoreboard.ScoreboardModule.ticketTotalOpen.set(Integer.parseInt(totOpen)); - if (totClaimed != null) net.viper.status.modules.scoreboard.ScoreboardModule.ticketTotalClaimed.set(Integer.parseInt(totClaimed)); - if (ratGood != null) net.viper.status.modules.scoreboard.ScoreboardModule.ticketRatingGood.set(Integer.parseInt(ratGood)); - if (ratBad != null) net.viper.status.modules.scoreboard.ScoreboardModule.ticketRatingBad.set(Integer.parseInt(ratBad)); - } catch (Exception ignored) {} - sendHttpResponse(out, "{\"success\":true}", 200); - return; - } - - // POST /player/data – Koordinaten, Gamemode, Exp, Food, Speed - if ("POST".equalsIgnoreCase(method) && "/player/data".equalsIgnoreCase(pathOnly)) { - String body = readBody(in, headers); - String uuidStr = extractJsonString(body, "uuid"); - if (uuidStr != null && !uuidStr.isEmpty()) { - try { - UUID uid = UUID.fromString(uuidStr.trim()); - String xS = extractJsonString(body, "x"); - String yS = extractJsonString(body, "y"); - String zS = extractJsonString(body, "z"); - String gm = extractJsonString(body, "gamemode"); - String expS= extractJsonString(body, "exp"); - String fdS = extractJsonString(body, "food"); - String spS = extractJsonString(body, "speed"); - String wld = extractJsonString(body, "world"); - if (xS != null) net.viper.status.modules.scoreboard.ScoreboardModule.playerX.put(uid, (int)Double.parseDouble(xS)); - if (yS != null) net.viper.status.modules.scoreboard.ScoreboardModule.playerY.put(uid, (int)Double.parseDouble(yS)); - if (zS != null) net.viper.status.modules.scoreboard.ScoreboardModule.playerZ.put(uid, (int)Double.parseDouble(zS)); - if (gm != null) net.viper.status.modules.scoreboard.ScoreboardModule.playerGamemode.put(uid, gm); - if (expS!= null) net.viper.status.modules.scoreboard.ScoreboardModule.playerExp.put(uid, (int)Double.parseDouble(expS)); - if (fdS != null) net.viper.status.modules.scoreboard.ScoreboardModule.playerFood.put(uid, (int)Double.parseDouble(fdS)); - if (spS != null) net.viper.status.modules.scoreboard.ScoreboardModule.playerSpeed.put(uid, Double.parseDouble(spS)); - if (wld != null) net.viper.status.modules.scoreboard.ScoreboardModule.playerWorld.put(uid, wld); - } catch (Exception ignored) {} - } - sendHttpResponse(out, "{\"success\":true}", 200); - return; - } - - // GET /papi/tokens – liefert alle erkannten %token%-Placeholder als JSON-Array - if ("GET".equalsIgnoreCase(method) && "/papi/tokens".equalsIgnoreCase(pathOnly)) { - sendHttpResponse(out, papiTokensJson, 200); - return; - } - - // POST /player/papi – empfängt von StatusAPIBridge aufgelöste PAPI-Werte - if ("POST".equalsIgnoreCase(method) && "/player/papi".equalsIgnoreCase(pathOnly)) { - String body = readBody(in, headers); - String uuidStr = extractJsonString(body, "uuid"); - if (uuidStr != null && !uuidStr.isEmpty()) { - try { - UUID papiUuid = UUID.fromString(uuidStr.trim()); - Map map = playerPapi.computeIfAbsent(papiUuid, k -> new ConcurrentHashMap<>()); - // "placeholders"-Objekt manuell parsen - int start = body.indexOf("\"placeholders\""); - if (start >= 0) { - int brace = body.indexOf('{', start + 14); - if (brace >= 0) { - int i = brace + 1; - while (i < body.length()) { - while (i < body.length() && Character.isWhitespace(body.charAt(i))) i++; - if (i >= body.length() || body.charAt(i) == '}') break; - if (body.charAt(i) != '"') { i++; continue; } - i++; - StringBuilder key = new StringBuilder(); - while (i < body.length() && body.charAt(i) != '"') { - char ch = body.charAt(i++); - if (ch == '\\' && i < body.length()) i++; else key.append(ch); - } - i++; - while (i < body.length() && (body.charAt(i) == ':' || Character.isWhitespace(body.charAt(i)))) i++; - if (i < body.length() && body.charAt(i) == '"') { - i++; - StringBuilder val = new StringBuilder(); - boolean esc = false; - while (i < body.length()) { - char ch = body.charAt(i++); - if (esc) { val.append(ch == 'n' ? '\n' : ch == 't' ? '\t' : ch); esc = false; } - else if (ch == '\\') esc = true; - else if (ch == '"') break; - else val.append(ch); - } - if (key.length() > 0) map.put(key.toString(), val.toString()); - } - while (i < body.length() && (body.charAt(i) == ',' || Character.isWhitespace(body.charAt(i)))) i++; - } - } - } - } catch (Exception ignored) {} - } - sendHttpResponse(out, "{\"success\":true}", 200); - return; - } - - // GET – Status-Endpunkt - if (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); - - int globalLimit = ProxyServer.getInstance().getConfig().getPlayerLimit(); - if (globalLimit <= 0) { - try { - Iterator limIt = ProxyServer.getInstance().getConfig().getListeners().iterator(); - if (limIt.hasNext()) { - int listenerMax = limIt.next().getMaxPlayers(); - if (listenerMax > 0) globalLimit = listenerMax; - } - } catch (Exception ignored) {} - } - data.put("max_players", String.valueOf(globalLimit)); - - 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) {} - - 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", java.util.UUID.class).invoke(api, p.getUniqueId()); - if (isBedrock) { - bedrockId = (String) api.getClass().getMethod("getBedrockId", java.util.UUID.class).invoke(api, p.getUniqueId()); - } - } catch (Exception ignored) {} - // Fallback: Floodgate-UUIDs haben MSB == 0 (00000000-0000-0000-xxxx-xxxxxxxxxxxx) - if (!isBedrock) { - isBedrock = p.getUniqueId().getMostSignificantBits() == 0L; - } - 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", java.util.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); - - // Aktueller Sub-Server des Spielers (z.B. "Lobby", "Survival") - try { - if (p.getServer() != null && p.getServer().getInfo() != null) { - playerInfo.put("server", p.getServer().getInfo().getName()); - } - } catch (Exception ignored) {} - - 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("kills", ps.kills); - playerInfo.put("deaths", ps.deaths); - playerInfo.put("first_seen", ps.firstSeen); - playerInfo.put("last_seen", ps.lastSeen); - // Balance direkt aus MySQL (serverübergreifend) - double statusBalance = playerBalances.getOrDefault(p.getUniqueId(), 0.0); - Map eco = new LinkedHashMap<>(); - eco.put("balance", statusBalance); - eco.put("total_earned", ps.totalEarned); - eco.put("total_spent", ps.totalSpent); - eco.put("transactions_count", ps.transactionsCount); - playerInfo.put("economy", eco); - Map pun = new LinkedHashMap<>(); - pun.put("bans", ps.bansCount); - pun.put("mutes", ps.mutesCount); - pun.put("warns", ps.warnsCount); - pun.put("last_punishment_at", ps.lastPunishmentAt); - pun.put("last_punishment_type", ps.lastPunishmentType != null ? ps.lastPunishmentType : ""); - pun.put("punishment_score", ps.punishmentScore); - playerInfo.put("punishments", pun); - } - } - playersList.add(playerInfo); - } - data.put("players", playersList); - - NetworkInfoModule networkInfoModule = (NetworkInfoModule) moduleManager.getModule("NetworkInfoModule"); - if (networkInfoModule != null && networkInfoModule.isEnabled()) { - data.put("network", networkInfoModule.buildSnapshot()); - } - - AntiBotModule antiBotModule = (AntiBotModule) moduleManager.getModule("AntiBotModule"); - if (antiBotModule != null && antiBotModule.isEnabled()) { - data.put("antibot", antiBotModule.buildSnapshot()); - } - - String json = buildJsonString(data); - byte[] jsonBytes = json.getBytes(StandardCharsets.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(StandardCharsets.UTF_8)); - out.write(jsonBytes); - out.flush(); - } - - } catch (Exception e) { - getLogger().severe("Fehler beim Verarbeiten der Anfrage: " + e.getMessage()); - } - } - - // --- Hilfsmethoden --- - - /** - * Liest den HTTP-Body basierend auf Content-Length. - */ - private String readBody(BufferedReader in, Map headers) throws IOException { - int contentLength = 0; - if (headers.containsKey("content-length")) { - try { contentLength = Integer.parseInt(headers.get("content-length")); } catch (NumberFormatException ignored) {} - } - if (contentLength <= 0) return ""; - char[] bodyChars = new char[contentLength]; - int read = 0; - while (read < contentLength) { - int r = in.read(bodyChars, read, contentLength - read); - if (r == -1) break; - read += r; - } - return new String(bodyChars); - } - - 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) { - switch (ch) { - case 'n': sb.append('\n'); break; - case 'r': sb.append('\r'); break; - case 't': sb.append('\t'); break; - case '"': sb.append('"'); break; - case '\\': sb.append('\\'); break; - case '/': sb.append('/'); break; - case 'b': sb.append('\b'); break; - case 'f': sb.append('\f'); break; - case 'u': - if (i + 4 <= json.length()) { - try { - int cp = Integer.parseInt(json.substring(i, i + 4), 16); - sb.append((char) cp); - i += 4; - } catch (NumberFormatException ignored) { - sb.append("\\u"); - } - } - break; - default: sb.append(ch); break; - } - 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 Properties loadNetworkGuardProperties() { - Properties props = new Properties(); - File file = new File(getDataFolder(), "network-guard.properties"); - if (!file.exists()) return props; - try (FileInputStream fis = new FileInputStream(file)) { - props.load(new java.io.InputStreamReader(fis, StandardCharsets.UTF_8)); - } catch (Exception e) { - getLogger().warning("Konnte network-guard.properties nicht laden: " + e.getMessage()); - } - return props; - } - - private List parseCommaListProperty(String raw) { - List out = new ArrayList<>(); - if (raw == null || raw.trim().isEmpty()) return out; - for (String p : raw.split(",")) { - String trimmed = p == null ? "" : p.trim(); - if (!trimmed.isEmpty()) out.add(trimmed); - } - return out; - } - - private Map parseQueryParams(String path) { - Map params = new HashMap<>(); - int q = path.indexOf('?'); - if (q < 0) return params; - String query = path.substring(q + 1); - for (String pair : query.split("&")) { - if (pair.isEmpty()) continue; - int eq = pair.indexOf('='); - if (eq <= 0) continue; - params.put(pair.substring(0, eq).trim().toLowerCase(Locale.ROOT), pair.substring(eq + 1).trim()); - } - return params; - } - - private PlayerStats resolvePlayer(String uuidStr, String name, StatsModule statsModule) { - if (statsModule == null) return null; - if (uuidStr != null && !uuidStr.isEmpty()) { - try { - return statsModule.getManager().getIfPresent(UUID.fromString(uuidStr.trim())); - } catch (IllegalArgumentException ignored) {} - } - if (name != null && !name.isEmpty()) { - String lower = name.trim().toLowerCase(Locale.ROOT); - for (PlayerStats ps : statsModule.getManager().all()) { - if (ps.name.toLowerCase(Locale.ROOT).equals(lower)) return ps; - } - } - return null; - } - - private void sendHttpResponse(OutputStream out, String json, int code) throws IOException { - byte[] jsonBytes = json.getBytes(StandardCharsets.UTF_8); - String status = code == 200 ? "OK" : (code == 403 ? "Forbidden" : code == 404 ? "Not Found" : code == 400 ? "Bad Request" : "Error"); - StringBuilder response = new StringBuilder(); - response.append("HTTP/1.1 ").append(code).append(" ").append(status).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(); - } - - @SuppressWarnings("unchecked") - 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) return buildJsonString((Map) value); - 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(); - } - return "\"" + escapeJson(String.valueOf(value)) + "\""; - } - - private String escapeJson(String s) { - if (s == null) return ""; - return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r"); - } - - private List> loadAntiBotSecurityEvents(int maxEntries) { - List> out = new ArrayList<>(); - File logFile = new File(getDataFolder(), "antibot-security.log"); - if (!logFile.exists() || maxEntries <= 0) return out; - try { - List lines = Files.readAllLines(logFile.toPath(), StandardCharsets.UTF_8); - for (int i = lines.size() - 1; i >= 0 && out.size() < maxEntries; i--) { - String line = lines.get(i); - if (line == null || line.trim().isEmpty()) continue; - Map parsed = parseSecurityLogLine(line); - String event = parsed.get("event"); - if (!isAttackSecurityEvent(event)) continue; - String player = parsed.get("player"); - String uuid = parsed.get("uuid"); - if (player == null || player.trim().isEmpty() || "-".equals(player) || "unknown".equalsIgnoreCase(player)) continue; - if (uuid == null || uuid.trim().isEmpty()) uuid = "-"; - Map row = new LinkedHashMap<>(); - row.put("datetime", parsed.getOrDefault("datetime", "")); - row.put("player", player); - row.put("uuid", uuid); - row.put("ip", parsed.getOrDefault("ip", "-")); - out.add(row); - } - } catch (Exception e) { - getLogger().warning("Konnte antibot-security.log nicht lesen: " + e.getMessage()); - } - return out; - } - - private Map parseSecurityLogLine(String line) { - Map map = new LinkedHashMap<>(); - if (line == null) return map; - String[] segments = line.split("\\s*\\|\\s*"); - if (segments.length > 0) map.put("datetime", segments[0].trim()); - for (int i = 1; i < segments.length; i++) { - String seg = segments[i]; - int idx = seg.indexOf('='); - if (idx <= 0) continue; - map.put(seg.substring(0, idx).trim().toLowerCase(Locale.ROOT), seg.substring(idx + 1).trim()); - } - return map; - } - - private boolean isAttackSecurityEvent(String event) { - if (event == null) return false; - String e = event.trim().toLowerCase(Locale.ROOT); - return e.contains("ip_rate") || e.contains("vpn") || e.contains("learning_threshold_block") || e.contains("block"); - } - - // ── PAPI-Token-Erkennung ────────────────────────────────────────────────── - - /** Alle Tokens die StatusAPI selbst auflöst – werden nicht an PAPI weitergegeben */ - private static final Set NATIVE_TOKENS = new HashSet<>(Arrays.asList( - "player", "rank", "money", "server", "compass", "health", "hearts", "ping", - "online", "maxplayers", "tps", "ram", "time", "playtime", "x", "y", "z", - "world", "gamemode", "exp", "food", "foodsym", "speed", "uptime", "servers", - "proxymem", "date", "news", "line", "balance", - "ticket_my_open", "ticket_open", "ticket_claimed", - "ticket_rating_good", "ticket_rating_bad", "ticket_rating_pct" - )); - - /** - * Scannt alle .properties-Dateien im Plugin-Ordner nach %token%-Mustern, - * filtert nativ unterstützte Tokens heraus und veröffentlicht den Rest - * als JSON-Array unter GET /papi/tokens für StatusAPIBridge. - */ - public void scanAndPublishPapiTokens() { - Set tokens = new LinkedHashSet<>(); - File folder = getDataFolder(); - if (folder.exists()) { - File[] files = folder.listFiles((dir, name) -> name.endsWith(".properties")); - if (files != null) { - for (File f : files) { - try (BufferedReader br = new BufferedReader( - new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8))) { - String line; - while ((line = br.readLine()) != null) { - extractAllTokensFromText(line, tokens); - } - } catch (IOException ignored) {} - } - } - } - // Ressourcen-Defaults scannen (falls noch keine Dateien im Ordner) - for (String resource : new String[]{"scoreboard.properties"}) { - try (java.io.InputStream is = getResourceAsStream(resource)) { - if (is == null) continue; - BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)); - String line; - while ((line = br.readLine()) != null) { - extractAllTokensFromText(line, tokens); - } - } catch (IOException ignored) {} - } - tokens.removeAll(NATIVE_TOKENS); - // JSON-Array bauen - StringBuilder json = new StringBuilder("["); - boolean first = true; - for (String token : tokens) { - if (!first) json.append(","); - json.append("\"").append(token.replace("\\", "\\\\").replace("\"", "\\\"")).append("\""); - first = false; - } - json.append("]"); - papiTokensJson = json.toString(); - if (!tokens.isEmpty()) { - getLogger().info("[StatusAPI] " + tokens.size() + " PAPI-Token(s) erkannt: " + tokens); - } - } - - private static void extractAllTokensFromText(String text, Set result) { - if (text == null || text.startsWith("#") || !text.contains("%")) return; - int eq = text.indexOf('='); - String value = eq >= 0 ? text.substring(eq + 1) : text; - int i = 0; - while (i < value.length()) { - int start = value.indexOf('%', i); - if (start < 0) break; - int end = value.indexOf('%', start + 1); - if (end < 0) break; - String token = value.substring(start + 1, end); - if (!token.isEmpty() && !token.contains(" ") && token.matches("[a-zA-Z0-9_:]+")) { - result.add(token); - } - i = end + 1; - } - } - - // ── Reload ──────────────────────────────────────────────────────────────── - - /** - * Lädt Scoreboard und Tablist neu (Config + Tasks), ohne den HTTP-Server zu berühren. - * Alle anderen Module (Chat, AntiBot, etc.) bleiben unberührt. - */ - public void reloadModules() { - getLogger().info("[StatusAPI] Reload von Scoreboard und Tablist..."); - - net.viper.status.module.Module sbMod = moduleManager.getModule("ScoreboardModule"); - net.viper.status.module.Module tabMod = moduleManager.getModule("TablistModule"); - - if (sbMod != null) sbMod.onDisable(this); - if (tabMod != null) tabMod.onDisable(this); - - // Neue Instanzen erstellen und registrieren - net.viper.status.modules.scoreboard.ScoreboardModule newSb = new net.viper.status.modules.scoreboard.ScoreboardModule(); - net.viper.status.modules.tablist.TablistModule newTab = new net.viper.status.modules.tablist.TablistModule(); - - moduleManager.replaceModule("ScoreboardModule", newSb); - moduleManager.replaceModule("TablistModule", newTab); - - newSb.onEnable(this); - newTab.onEnable(this); - - // TPS-Fallback neu verbinden - try { - net.viper.status.modules.network.NetworkInfoModule nim = - (net.viper.status.modules.network.NetworkInfoModule) moduleManager.getModule("NetworkInfoModule"); - if (nim != null) newSb.setNetworkInfoModule(nim); - } catch (Exception ignored) {} - - scanAndPublishPapiTokens(); - getLogger().info("[StatusAPI] Reload abgeschlossen."); - } - - // ── /statusapi Befehl ───────────────────────────────────────────────────── - - private static class StatusAPICommand extends net.md_5.bungee.api.plugin.Command { - - private final StatusAPI plugin; - - StatusAPICommand(StatusAPI plugin) { - super("statusapi", "statusapi.admin", "sapi"); - this.plugin = plugin; - } - - @Override - public void execute(net.md_5.bungee.api.CommandSender sender, String[] args) { - if (args.length == 0 || args[0].equalsIgnoreCase("help")) { - boolean isAdmin = sender.hasPermission("statusapi.admin") - || !(sender instanceof net.md_5.bungee.api.connection.ProxiedPlayer); - send(sender, "&8&m──────────────────────────────────────────"); - send(sender, "&6&lStatusAPI &7| Befehle"); - if (isAdmin) { - send(sender, "&e/statusapi reload &7– Scoreboard & Tablist neu laden"); - } else { - send(sender, "&7Keine weiteren Unterbefehle verfügbar."); - } - send(sender, "&8&m──────────────────────────────────────────"); - return; - } - - if (!sender.hasPermission("statusapi.admin")) { - send(sender, "&cKeine Berechtigung."); - return; - } - - switch (args[0].toLowerCase()) { - case "reload": - send(sender, "&7Lade &6Scoreboard &7und &6Tablist &7neu..."); - plugin.reloadModules(); - send(sender, "&aScoreboard &7und &aTablist &7wurden neu geladen."); - send(sender, "&7PAPI-Tokens erkannt: &e" + papiTokensJson); - break; - - default: - send(sender, "&cUnbekannter Unterbefehl. Nutze &e/statusapi help&c."); - break; - } - } - - private static void send(net.md_5.bungee.api.CommandSender s, String text) { - s.sendMessage(new net.md_5.bungee.api.chat.TextComponent( - net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', text))); - } - } -} \ No newline at end of file