From e47b9839fb98231e4d63b0ac2a66514e5f0b9996 Mon Sep 17 00:00:00 2001 From: M_Viper Date: Thu, 7 May 2026 19:39:24 +0000 Subject: [PATCH] Soft-delete copy _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/StatusAPI.java --- .../main/java/net/viper/status/StatusAPI.java | 1050 +++++++++++++++++ 1 file changed, 1050 insertions(+) create mode 100644 _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/StatusAPI.java diff --git a/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/StatusAPI.java b/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/StatusAPI.java new file mode 100644 index 0000000..f1ed9a9 --- /dev/null +++ b/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/StatusAPI.java @@ -0,0 +1,1050 @@ +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.antibot.AntiBotModule; +import net.viper.status.modules.network.NetworkInfoModule; +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 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<>(); + + 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; + } + + moduleManager = new ModuleManager(); + + // Module in korrekter Reihenfolge registrieren + // VanishModule MUSS vor ChatModule registriert werden (VanishProvider-Abhängigkeit) + moduleManager.registerModule(new StatsModule()); + 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 AutoMessageModule()); + moduleManager.registerModule(new CustomCommandModule()); + moduleManager.registerModule(new ServerSwitcherModule()); + moduleManager.registerModule(new EconomyModule()); + moduleManager.registerModule(new TablistModule()); + + 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 + 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 (FileInputStream fis = new FileInputStream(file)) { + verifyProperties.load(fis); + } + } 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("online", ProxyServer.getInstance().getPlayer(ps.uuid) != null); + // Balance direkt aus MySQL (serverübergreifend) + EconomyModule ecoModPlayer = (EconomyModule) moduleManager.getModule("EconomyModule"); + double playerBalance = (ecoModPlayer != null && ecoModPlayer.getManager() != null) + ? ecoModPlayer.getManager().getBalance(ps.uuid) + : ps.balance; + 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); + EconomyModule ecoModGet = (EconomyModule) moduleManager.getModule("EconomyModule"); + if (ecoModGet == null || ecoModGet.getManager() == null) { + sendHttpResponse(out, "{\"success\":false,\"error\":\"economy_module_unavailable\"}", 503); + return; + } + // UUID auflösen: erst Query-Param, dann Name über EconomyManager (DB-Lookup) + 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 && nameParam != null && !nameParam.isEmpty()) { + ecoUuid = ecoModGet.getManager().resolveUUID(nameParam.trim()); + ecoName = nameParam.trim(); + } + if (ecoUuid == null) { + sendHttpResponse(out, "{\"success\":false,\"error\":\"player_not_found\"}", 404); + return; + } + if (ecoName == null) ecoName = uuidParam; + double directBalance = ecoModGet.getManager().getBalance(ecoUuid); + 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 + // Kein Cache – Balance wird direkt in die DB geschrieben + if ("POST".equalsIgnoreCase(method) && "/economy/update".equalsIgnoreCase(pathOnly)) { + String body = readBody(in, headers); + EconomyModule ecoModUpd = (EconomyModule) moduleManager.getModule("EconomyModule"); + if (ecoModUpd == null || ecoModUpd.getManager() == null) { + sendHttpResponse(out, "{\"success\":false,\"error\":\"economy_module_unavailable\"}", 503); + return; + } + // UUID auflösen + UUID ecoUpdUuid = null; + String uuidBody = extractJsonString(body, "uuid"); + String nameBody = extractJsonString(body, "name"); + if (uuidBody != null && !uuidBody.isEmpty()) { + try { ecoUpdUuid = UUID.fromString(uuidBody.trim()); } catch (IllegalArgumentException ignored) {} + } + if (ecoUpdUuid == null && nameBody != null && !nameBody.isEmpty()) { + ecoUpdUuid = ecoModUpd.getManager().resolveUUID(nameBody.trim()); + } + if (ecoUpdUuid == null) { + sendHttpResponse(out, "{\"success\":false,\"error\":\"player_not_found\"}", 404); + return; + } + // Balance direkt in DB schreiben + String balStr = extractJsonString(body, "balance"); + if (balStr != null && !balStr.isEmpty()) { + try { + double newBal = Double.parseDouble(balStr); + ecoModUpd.getManager().setBalance(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 /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; + } + + // 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); + + 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); + // Balance direkt aus MySQL (serverübergreifend) + EconomyModule ecoModStatus = (EconomyModule) moduleManager.getModule("EconomyModule"); + double statusBalance = (ecoModStatus != null && ecoModStatus.getManager() != null) + ? ecoModStatus.getManager().getBalance(p.getUniqueId()) + : ps.balance; + 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"); + } +} \ No newline at end of file