From e32a7457ebbd63b5b2275b665827e23e23fe625d Mon Sep 17 00:00:00 2001 From: M_Viper Date: Thu, 7 May 2026 19:39:27 +0000 Subject: [PATCH] Soft-delete copy _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/antibot/AntiBotModule.java --- .../status/modules/antibot/AntiBotModule.java | 840 ++++++++++++++++++ 1 file changed, 840 insertions(+) create mode 100644 _trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/antibot/AntiBotModule.java diff --git a/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/antibot/AntiBotModule.java b/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/antibot/AntiBotModule.java new file mode 100644 index 0000000..3ff9864 --- /dev/null +++ b/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/antibot/AntiBotModule.java @@ -0,0 +1,840 @@ +package net.viper.status.modules.antibot; + +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.ProxyServer; +import net.md_5.bungee.api.connection.PendingConnection; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.event.PreLoginEvent; +import net.md_5.bungee.api.event.PostLoginEvent; +import net.md_5.bungee.api.plugin.Command; +import net.md_5.bungee.api.plugin.Listener; +import net.md_5.bungee.api.plugin.Plugin; +import net.md_5.bungee.event.EventHandler; +import net.viper.status.StatusAPI; +import net.viper.status.module.Module; +import net.viper.status.modules.network.NetworkInfoModule; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.URL; +import java.nio.file.Files; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Date; +import java.util.Deque; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Eigenständiger AntiBot/Attack-Guard. + * + * Fixes: + * - cleanupExpired() nutzt jetzt removeIf() statt Iteration + remove() (Bug #3) + * - applyProfileDefaults() setzt korrekten attackDefaultSource aus Config + */ +public class AntiBotModule implements Module, Listener { + + private static final String CONFIG_FILE_NAME = "network-guard.properties"; + + private StatusAPI plugin; + + private boolean enabled = true; + private String profile = "high-traffic"; + private int maxCps = 120; + private int attackStartCps = 220; + private int attackStopCps = 120; + private int attackCalmSeconds = 20; + private int ipConnectionsPerMinute = 18; + private int ipBlockSeconds = 600; + private String kickMessage = "Zu viele Verbindungen von deiner IP. Bitte warte kurz."; + + private boolean vpnCheckEnabled = false; + private boolean vpnBlockProxy = true; + private boolean vpnBlockHosting = true; + private int vpnCacheMinutes = 30; + private int vpnTimeoutMs = 2500; + private boolean securityLogEnabled = true; + private String securityLogFileName = "antibot-security.log"; + private File securityLogFile; + private final Object securityLogLock = new Object(); + + private boolean learningModeEnabled = true; + private int learningScoreThreshold = 100; + private int learningDecayPerSecond = 2; + private int learningStateWindowSeconds = 120; + private int learningRapidWindowMs = 1500; + private int learningRapidPoints = 12; + private int learningIpRateExceededPoints = 30; + private int learningVpnProxyPoints = 40; + private int learningVpnHostingPoints = 30; + private int learningAttackModePoints = 12; + private int learningHighCpsPoints = 10; + private int learningRecentEventLimit = 30; + + private final AtomicInteger currentSecondConnections = new AtomicInteger(0); + private volatile long currentSecond = System.currentTimeMillis() / 1000L; + private volatile int lastCps = 0; + private final AtomicInteger peakCps = new AtomicInteger(0); + + private volatile boolean attackMode = false; + private volatile long attackCalmSince = 0L; + private final AtomicLong blockedConnectionsTotal = new AtomicLong(0L); + private final AtomicLong blockedConnectionsCurrentAttack = new AtomicLong(0L); + private final Set blockedIpsCurrentAttack = ConcurrentHashMap.newKeySet(); + + private final Map perIpWindows = new ConcurrentHashMap<>(); + private final Map blockedIpsUntil = new ConcurrentHashMap<>(); + private final Map vpnCache = new ConcurrentHashMap<>(); + private final Map recentIdentityByIp = new ConcurrentHashMap<>(); + private final Map learningProfiles = new ConcurrentHashMap<>(); + private final Deque learningRecentEvents = new ArrayDeque<>(); + + @Override + public String getName() { return "AntiBotModule"; } + + @Override + public void onEnable(Plugin plugin) { + if (!(plugin instanceof StatusAPI)) return; + this.plugin = (StatusAPI) plugin; + ensureModuleConfigExists(); + loadConfig(); + ensureSecurityLogFile(); + + if (!enabled) { + this.plugin.getLogger().info("[AntiBotModule] deaktiviert via " + CONFIG_FILE_NAME); + return; + } + + ProxyServer.getInstance().getPluginManager().registerListener(this.plugin, this); + ProxyServer.getInstance().getPluginManager().registerCommand(this.plugin, new AntiBotCommand()); + ProxyServer.getInstance().getScheduler().schedule(this.plugin, this::tick, 1, 1, TimeUnit.SECONDS); + + this.plugin.getLogger().info("[AntiBotModule] aktiviert. maxCps=" + maxCps + + ", attackStartCps=" + attackStartCps + ", ip/min=" + ipConnectionsPerMinute); + } + + @Override + public void onDisable(Plugin plugin) { + perIpWindows.clear(); + blockedIpsUntil.clear(); + vpnCache.clear(); + learningProfiles.clear(); + synchronized (learningRecentEvents) { learningRecentEvents.clear(); } + blockedIpsCurrentAttack.clear(); + attackMode = false; + } + + public boolean isEnabled() { return enabled; } + + private void reloadRuntimeState() { + perIpWindows.clear(); + blockedIpsUntil.clear(); + vpnCache.clear(); + learningProfiles.clear(); + synchronized (learningRecentEvents) { learningRecentEvents.clear(); } + blockedIpsCurrentAttack.clear(); + attackMode = false; + attackCalmSince = 0L; + blockedConnectionsCurrentAttack.set(0L); + currentSecondConnections.set(0); + lastCps = 0; + peakCps.set(0); + loadConfig(); + ensureSecurityLogFile(); + } + + public Map buildSnapshot() { + Map out = new LinkedHashMap<>(); + out.put("enabled", enabled); + out.put("profile", profile); + out.put("attack_mode", attackMode); + out.put("protection_enabled", enabled); + out.put("attack_mode_status", attackMode ? "active" : "normal"); + out.put("attack_mode_display", attackMode ? "Angriff erkannt" : "Normalbetrieb"); + out.put("status_message", enabled + ? (attackMode ? "AntiBot aktiv: Angriff erkannt" : "AntiBot aktiv: kein Angriff erkannt") + : "AntiBot deaktiviert"); + out.put("last_cps", lastCps); + out.put("peak_cps", peakCps.get()); + out.put("blocked_ips_active", blockedIpsUntil.size()); + out.put("blocked_connections_total", blockedConnectionsTotal.get()); + out.put("vpn_check_enabled", vpnCheckEnabled); + out.put("learning_mode_enabled", learningModeEnabled); + out.put("learning_profiles", learningProfiles.size()); + out.put("thresholds", buildThresholds()); + return out; + } + + private Map buildThresholds() { + Map m = new LinkedHashMap<>(); + m.put("max_cps", maxCps); + m.put("attack_start_cps", attackStartCps); + m.put("attack_stop_cps", attackStopCps); + m.put("attack_calm_seconds", attackCalmSeconds); + m.put("ip_connections_per_minute", ipConnectionsPerMinute); + m.put("ip_block_seconds", ipBlockSeconds); + m.put("learning_score_threshold", learningScoreThreshold); + m.put("learning_decay_per_second", learningDecayPerSecond); + return m; + } + + @EventHandler + public void onPreLogin(PreLoginEvent event) { + if (!enabled) return; + + String ip = extractIp(event.getConnection()); + if (ip == null || ip.isEmpty()) return; + + cacheRecentIdentity(ip, event.getConnection(), System.currentTimeMillis()); + recordConnection(); + long now = System.currentTimeMillis(); + + // FIX #3: cleanupExpired verwendet removeIf statt Iteration+remove + cleanupExpired(now); + + Long blockedUntil = blockedIpsUntil.get(ip); + if (blockedUntil != null && blockedUntil > now) { + logSecurityEvent("ip_block_active", ip, event.getConnection(), "blocked_until_ms=" + blockedUntil); + blockEvent(event); + return; + } + + if (learningModeEnabled) { + evaluateLearningBaseline(ip, now); + Long learningBlock = blockedIpsUntil.get(ip); + if (learningBlock != null && learningBlock > now) { + blockEvent(event); + return; + } + } + + boolean ipRateExceeded = isIpRateExceeded(ip, now); + if (ipRateExceeded) { + if (learningModeEnabled) { + int score = addLearningScore(ip, now, learningIpRateExceededPoints, "ip-rate-exceeded", true); + logSecurityEvent("ip_rate_exceeded_scored", ip, event.getConnection(), "score=" + score + ", threshold=" + learningScoreThreshold); + if (score >= learningScoreThreshold) { + logSecurityEvent("learning_threshold_block", ip, event.getConnection(), "reason=ip-rate-exceeded, score=" + score); + blockEvent(event); + return; + } + } else { + blockIp(ip, now); + logSecurityEvent("ip_rate_limit_block", ip, event.getConnection(), "mode=direct"); + blockEvent(event); + return; + } + } + + if (vpnCheckEnabled) { + VpnCheckResult info = getVpnInfo(ip, now); + if (info != null) { + boolean shouldBlock = (vpnBlockProxy && info.proxy) || (vpnBlockHosting && info.hosting); + if (shouldBlock) { + logSecurityEvent("vpn_detected", ip, event.getConnection(), "proxy=" + info.proxy + ", hosting=" + info.hosting); + if (learningModeEnabled) { + if (vpnBlockProxy && info.proxy) addLearningScore(ip, now, learningVpnProxyPoints, "vpn-proxy", false); + if (vpnBlockHosting && info.hosting) addLearningScore(ip, now, learningVpnHostingPoints, "vpn-hosting", false); + int current = getLearningScore(ip, now); + if (current >= learningScoreThreshold) { + blockIp(ip, now); + logSecurityEvent("learning_threshold_block", ip, event.getConnection(), "reason=vpn, score=" + current); + recordLearningEvent("BLOCK " + ip + " reason=vpn score=" + current); + blockEvent(event); + } + } else { + blockIp(ip, now); + logSecurityEvent("vpn_block", ip, event.getConnection(), "mode=direct, proxy=" + info.proxy + ", hosting=" + info.hosting); + blockEvent(event); + } + } + } + } + } + + @EventHandler + public void onPostLogin(PostLoginEvent event) { + if (!enabled || event == null || event.getPlayer() == null) return; + ProxiedPlayer player = event.getPlayer(); + String ip = extractIpFromPlayer(player); + if (ip == null || ip.isEmpty()) return; + cacheRecentIdentityDirect(ip, player.getName(), player.getUniqueId(), System.currentTimeMillis()); + } + + private void blockEvent(PreLoginEvent event) { + event.setCancelled(true); + } + + private String extractIp(PendingConnection conn) { + if (conn == null || conn.getAddress() == null) return null; + if (conn.getAddress() instanceof InetSocketAddress) { + InetSocketAddress sa = (InetSocketAddress) conn.getAddress(); + return sa.getAddress() != null ? sa.getAddress().getHostAddress() : sa.getHostString(); + } + return String.valueOf(conn.getAddress()); + } + + private String extractIpFromPlayer(ProxiedPlayer player) { + if (player == null || player.getAddress() == null) return null; + if (player.getAddress() instanceof InetSocketAddress) { + InetSocketAddress sa = (InetSocketAddress) player.getAddress(); + return sa.getAddress() != null ? sa.getAddress().getHostAddress() : sa.getHostString(); + } + return String.valueOf(player.getAddress()); + } + + private void recordConnection() { + long sec = System.currentTimeMillis() / 1000L; + if (sec != currentSecond) { + synchronized (this) { + if (sec != currentSecond) { + currentSecond = sec; + currentSecondConnections.set(0); + } + } + } + currentSecondConnections.incrementAndGet(); + } + + private boolean isIpRateExceeded(String ip, long now) { + IpWindow window = perIpWindows.computeIfAbsent(ip, k -> new IpWindow(now)); + synchronized (window) { + long diff = now - window.windowStart; + if (diff > 60_000L) { + window.windowStart = now; + window.count = 0; + } + window.count++; + return window.count > Math.max(1, ipConnectionsPerMinute); + } + } + + private void blockIp(String ip, long now) { + blockedIpsUntil.put(ip, now + Math.max(1, ipBlockSeconds) * 1000L); + blockedConnectionsTotal.incrementAndGet(); + if (attackMode) { + blockedConnectionsCurrentAttack.incrementAndGet(); + blockedIpsCurrentAttack.add(ip); + } + } + + /** + * FIX #3: Verwendet removeIf() statt for-each + remove() um ConcurrentModificationException zu vermeiden. + */ + private void cleanupExpired(long now) { + blockedIpsUntil.entrySet().removeIf(e -> e.getValue() <= now); + vpnCache.entrySet().removeIf(e -> e.getValue().expiresAt <= now); + recentIdentityByIp.entrySet().removeIf(e -> { + RecentPlayerIdentity id = e.getValue(); + return id == null || (now - id.updatedAtMs) > 600_000L; + }); + if (learningModeEnabled) { + long staleAfter = Math.max(60, learningStateWindowSeconds) * 1000L; + learningProfiles.entrySet().removeIf(e -> { + LearningProfile lp = e.getValue(); + return lp == null || ((now - lp.lastSeenAt) > staleAfter && lp.score <= 0); + }); + } + } + + private void tick() { + if (!enabled) return; + int cps = currentSecondConnections.getAndSet(0); + lastCps = cps; + if (cps > peakCps.get()) peakCps.set(cps); + + long now = System.currentTimeMillis(); + + if (!attackMode && cps >= Math.max(1, attackStartCps)) { + attackMode = true; + attackCalmSince = 0L; + blockedConnectionsCurrentAttack.set(0L); + blockedIpsCurrentAttack.clear(); + sendAttackToWebhook("detected", cps, null, null, "StatusAPI AntiBot"); + plugin.getLogger().warning("[AntiBotModule] Attack erkannt. CPS=" + cps); + return; + } + + if (attackMode) { + if (cps <= Math.max(1, attackStopCps)) { + if (attackCalmSince == 0L) attackCalmSince = now; + long calmFor = now - attackCalmSince; + if (calmFor >= Math.max(1, attackCalmSeconds) * 1000L) { + attackMode = false; + attackCalmSince = 0L; + int blockedIps = blockedIpsCurrentAttack.size(); + long blockedConns = blockedConnectionsCurrentAttack.get(); + sendAttackToWebhook("stopped", cps, blockedIps, blockedConns, "StatusAPI AntiBot"); + plugin.getLogger().warning("[AntiBotModule] Attack beendet. blockedIps=" + blockedIps + ", blockedConnections=" + blockedConns); + } + } else { + attackCalmSince = 0L; + } + } + } + + private void sendAttackToWebhook(String type, Integer cps, Integer blockedIps, Long blockedConnections, String source) { + NetworkInfoModule networkInfoModule = getNetworkInfoModule(); + if (networkInfoModule == null) return; + networkInfoModule.sendAttackNotification(type, cps, blockedIps, blockedConnections, source); + } + + private NetworkInfoModule getNetworkInfoModule() { + if (plugin == null || plugin.getModuleManager() == null) return null; + return (NetworkInfoModule) plugin.getModuleManager().getModule("NetworkInfoModule"); + } + + private void loadConfig() { + File file = new File(plugin.getDataFolder(), CONFIG_FILE_NAME); + if (!file.exists()) return; + + Properties props = new Properties(); + try (FileInputStream in = new FileInputStream(file)) { + props.load(new InputStreamReader(in, StandardCharsets.UTF_8)); + + enabled = parseBoolean(props.getProperty("antibot.enabled"), true); + profile = normalizeProfile(props.getProperty("antibot.profile", "high-traffic")); + applyProfileDefaults(profile); + + maxCps = parseInt(props.getProperty("antibot.max_cps"), maxCps); + attackStartCps = parseInt(props.getProperty("antibot.attack.start_cps"), attackStartCps); + attackStopCps = parseInt(props.getProperty("antibot.attack.stop_cps"), attackStopCps); + attackCalmSeconds = parseInt(props.getProperty("antibot.attack.stop_grace_seconds"), attackCalmSeconds); + ipConnectionsPerMinute = parseInt(props.getProperty("antibot.ip.max_connections_per_minute"), ipConnectionsPerMinute); + ipBlockSeconds = parseInt(props.getProperty("antibot.ip.block_seconds"), ipBlockSeconds); + kickMessage = props.getProperty("antibot.kick_message", kickMessage); + + vpnCheckEnabled = parseBoolean(props.getProperty("antibot.vpn_check.enabled"), vpnCheckEnabled); + vpnBlockProxy = parseBoolean(props.getProperty("antibot.vpn_check.block_proxy"), vpnBlockProxy); + vpnBlockHosting = parseBoolean(props.getProperty("antibot.vpn_check.block_hosting"), vpnBlockHosting); + vpnCacheMinutes = parseInt(props.getProperty("antibot.vpn_check.cache_minutes"), vpnCacheMinutes); + vpnTimeoutMs = parseInt(props.getProperty("antibot.vpn_check.timeout_ms"), vpnTimeoutMs); + securityLogEnabled = parseBoolean(props.getProperty("antibot.security_log.enabled"), securityLogEnabled); + securityLogFileName = props.getProperty("antibot.security_log.file", securityLogFileName).trim(); + if (securityLogFileName.isEmpty()) securityLogFileName = "antibot-security.log"; + + learningModeEnabled = parseBoolean(props.getProperty("antibot.learning.enabled"), learningModeEnabled); + learningScoreThreshold = parseInt(props.getProperty("antibot.learning.score_threshold"), learningScoreThreshold); + learningDecayPerSecond = parseInt(props.getProperty("antibot.learning.decay_per_second"), learningDecayPerSecond); + learningStateWindowSeconds = parseInt(props.getProperty("antibot.learning.state_window_seconds"), learningStateWindowSeconds); + learningRapidWindowMs = parseInt(props.getProperty("antibot.learning.rapid.window_ms"), learningRapidWindowMs); + learningRapidPoints = parseInt(props.getProperty("antibot.learning.rapid.points"), learningRapidPoints); + learningIpRateExceededPoints = parseInt(props.getProperty("antibot.learning.ip_rate_exceeded.points"), learningIpRateExceededPoints); + learningVpnProxyPoints = parseInt(props.getProperty("antibot.learning.vpn_proxy.points"), learningVpnProxyPoints); + learningVpnHostingPoints = parseInt(props.getProperty("antibot.learning.vpn_hosting.points"), learningVpnHostingPoints); + learningAttackModePoints = parseInt(props.getProperty("antibot.learning.attack_mode.points"), learningAttackModePoints); + learningHighCpsPoints = parseInt(props.getProperty("antibot.learning.high_cps.points"), learningHighCpsPoints); + learningRecentEventLimit = parseInt(props.getProperty("antibot.learning.recent_events.limit"), learningRecentEventLimit); + } catch (Exception e) { + plugin.getLogger().warning("[AntiBotModule] Fehler beim Laden von " + CONFIG_FILE_NAME + ": " + e.getMessage()); + } + } + + private String normalizeProfile(String raw) { + if (raw == null) return "high-traffic"; + String v = raw.trim().toLowerCase(Locale.ROOT); + return "strict".equals(v) ? "strict" : "high-traffic"; + } + + private void applyProfileDefaults(String profileName) { + if ("strict".equals(profileName)) { + maxCps = 120; attackStartCps = 220; attackStopCps = 120; attackCalmSeconds = 20; + ipConnectionsPerMinute = 18; ipBlockSeconds = 900; + vpnCheckEnabled = true; vpnBlockProxy = true; vpnBlockHosting = true; + vpnCacheMinutes = 30; vpnTimeoutMs = 2500; + } else { + maxCps = 180; attackStartCps = 300; attackStopCps = 170; attackCalmSeconds = 25; + ipConnectionsPerMinute = 24; ipBlockSeconds = 600; + vpnCheckEnabled = false; vpnBlockProxy = true; vpnBlockHosting = true; + vpnCacheMinutes = 30; vpnTimeoutMs = 2500; + } + } + + private boolean isSupportedProfile(String raw) { + if (raw == null) return false; + String v = raw.trim().toLowerCase(Locale.ROOT); + return "strict".equals(v) || "high-traffic".equals(v); + } + + private boolean applyProfileAndPersist(String requestedProfile) { + if (!isSupportedProfile(requestedProfile)) return false; + String normalized = normalizeProfile(requestedProfile); + profile = normalized; + applyProfileDefaults(normalized); + + Map values = new LinkedHashMap<>(); + values.put("antibot.profile", normalized); + values.put("antibot.max_cps", String.valueOf(maxCps)); + values.put("antibot.attack.start_cps", String.valueOf(attackStartCps)); + values.put("antibot.attack.stop_cps", String.valueOf(attackStopCps)); + values.put("antibot.attack.stop_grace_seconds", String.valueOf(attackCalmSeconds)); + values.put("antibot.ip.max_connections_per_minute", String.valueOf(ipConnectionsPerMinute)); + values.put("antibot.ip.block_seconds", String.valueOf(ipBlockSeconds)); + values.put("antibot.vpn_check.enabled", String.valueOf(vpnCheckEnabled)); + values.put("antibot.vpn_check.block_proxy", String.valueOf(vpnBlockProxy)); + values.put("antibot.vpn_check.block_hosting", String.valueOf(vpnBlockHosting)); + values.put("antibot.vpn_check.cache_minutes", String.valueOf(vpnCacheMinutes)); + values.put("antibot.vpn_check.timeout_ms", String.valueOf(vpnTimeoutMs)); + try { updateConfigValues(values); return true; } + catch (Exception e) { plugin.getLogger().warning("[AntiBotModule] Konnte Profil nicht speichern: " + e.getMessage()); return false; } + } + + private synchronized void updateConfigValues(Map keyValues) throws Exception { + File target = new File(plugin.getDataFolder(), CONFIG_FILE_NAME); + List lines = target.exists() + ? Files.readAllLines(target.toPath(), StandardCharsets.UTF_8) + : new ArrayList<>(); + for (Map.Entry entry : keyValues.entrySet()) { + String key = entry.getKey(); + String newLine = key + "=" + entry.getValue(); + boolean replaced = false; + for (int i = 0; i < lines.size(); i++) { + if (lines.get(i).trim().startsWith(key + "=")) { + lines.set(i, newLine); + replaced = true; + break; + } + } + if (!replaced) lines.add(newLine); + } + Files.write(target.toPath(), lines, StandardCharsets.UTF_8); + } + + private void ensureModuleConfigExists() { + File target = new File(plugin.getDataFolder(), CONFIG_FILE_NAME); + if (target.exists()) return; + if (!plugin.getDataFolder().exists()) plugin.getDataFolder().mkdirs(); + try (InputStream in = plugin.getResourceAsStream(CONFIG_FILE_NAME); + FileOutputStream out = new FileOutputStream(target)) { + if (in == null) { plugin.getLogger().warning("[AntiBotModule] Standarddatei nicht im JAR."); return; } + byte[] buffer = new byte[4096]; int read; + while ((read = in.read(buffer)) != -1) out.write(buffer, 0, read); + } catch (Exception e) { + plugin.getLogger().warning("[AntiBotModule] Konnte Config nicht erstellen: " + e.getMessage()); + } + } + + private void evaluateLearningBaseline(String ip, long now) { + LearningProfile lp = learningProfiles.computeIfAbsent(ip, k -> new LearningProfile(now)); + synchronized (lp) { + decayLearningProfile(lp, now); + long delta = now - lp.lastConnectionAt; + if (lp.lastConnectionAt > 0 && delta <= Math.max(250L, learningRapidWindowMs)) { + lp.rapidStreak++; + int points = learningRapidPoints + Math.min(lp.rapidStreak, 5); + lp.score += Math.max(1, points); + recordLearningEvent("IP=" + ip + " +" + points + " rapid-connect score=" + lp.score); + } else { + lp.rapidStreak = 0; + } + if (attackMode) { lp.score += Math.max(1, learningAttackModePoints); recordLearningEvent("IP=" + ip + " +" + learningAttackModePoints + " attack-mode score=" + lp.score); } + if (lastCps >= Math.max(1, maxCps)) { lp.score += Math.max(1, learningHighCpsPoints); recordLearningEvent("IP=" + ip + " +" + learningHighCpsPoints + " high-cps score=" + lp.score); } + lp.lastConnectionAt = now; + lp.lastSeenAt = now; + if (lp.score >= learningScoreThreshold) { + blockIp(ip, now); + recordLearningEvent("BLOCK " + ip + " reason=learning-threshold score=" + lp.score); + } + } + } + + private int addLearningScore(String ip, long now, int points, String reason, boolean checkThreshold) { + LearningProfile lp = learningProfiles.computeIfAbsent(ip, k -> new LearningProfile(now)); + synchronized (lp) { + decayLearningProfile(lp, now); + int add = Math.max(1, points); + lp.score += add; + lp.lastSeenAt = now; + recordLearningEvent("IP=" + ip + " +" + add + " " + reason + " score=" + lp.score); + if (checkThreshold && lp.score >= learningScoreThreshold) { + blockIp(ip, now); + recordLearningEvent("BLOCK " + ip + " reason=" + reason + " score=" + lp.score); + } + return lp.score; + } + } + + private int getLearningScore(String ip, long now) { + LearningProfile lp = learningProfiles.get(ip); + if (lp == null) return 0; + synchronized (lp) { decayLearningProfile(lp, now); return lp.score; } + } + + private void decayLearningProfile(LearningProfile lp, long now) { + long elapsedMs = Math.max(0L, now - lp.lastScoreUpdateAt); + if (elapsedMs > 0L) { + long decay = (elapsedMs / 1000L) * Math.max(0, learningDecayPerSecond); + if (decay > 0L) lp.score = (int) Math.max(0L, lp.score - decay); + lp.lastScoreUpdateAt = now; + } + long resetAfter = Math.max(30, learningStateWindowSeconds) * 1000L; + if (lp.lastSeenAt > 0L && now - lp.lastSeenAt > resetAfter) { + lp.score = 0; + lp.rapidStreak = 0; + } + } + + private void recordLearningEvent(String event) { + String line = new SimpleDateFormat("HH:mm:ss").format(new Date()) + " " + event; + synchronized (learningRecentEvents) { + learningRecentEvents.addLast(line); + while (learningRecentEvents.size() > Math.max(5, learningRecentEventLimit)) learningRecentEvents.pollFirst(); + } + } + + private void ensureSecurityLogFile() { + if (plugin == null) return; + if (!plugin.getDataFolder().exists()) plugin.getDataFolder().mkdirs(); + securityLogFile = new File(plugin.getDataFolder(), securityLogFileName); + try { if (!securityLogFile.exists()) securityLogFile.createNewFile(); } + catch (Exception e) { plugin.getLogger().warning("[AntiBotModule] Konnte Sicherheitslog nicht erstellen: " + e.getMessage()); } + } + + private void logSecurityEvent(String eventType, String ip, PendingConnection conn, String details) { + if (!securityLogEnabled || plugin == null) return; + if (securityLogFile == null) { ensureSecurityLogFile(); if (securityLogFile == null) return; } + + String name = extractPlayerName(conn); + String uuid = extractPlayerUuid(conn, name); + if ((name == null || name.isEmpty() || "unknown".equalsIgnoreCase(name)) && ip != null) { + RecentPlayerIdentity cached = recentIdentityByIp.get(ip); + if (cached != null) { + if (cached.playerName != null && !cached.playerName.trim().isEmpty()) name = cached.playerName; + if ((uuid == null || uuid.isEmpty()) && cached.playerUuid != null && !cached.playerUuid.trim().isEmpty()) uuid = cached.playerUuid; + } + } + if (name == null || name.trim().isEmpty()) name = "unknown"; + if (uuid == null || uuid.trim().isEmpty()) uuid = "unknown"; + + String line = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + + " | event=" + safeLog(eventType) + + " | ip=" + safeLog(ip) + + " | player=" + safeLog(name) + + " | uuid=" + safeLog(uuid) + + " | details=" + safeLog(details); + + synchronized (securityLogLock) { + try (BufferedWriter bw = new BufferedWriter(new FileWriter(securityLogFile, true))) { + bw.write(line); bw.newLine(); + } catch (Exception e) { + plugin.getLogger().warning("[AntiBotModule] Sicherheitslog-Schreibfehler: " + e.getMessage()); + } + } + } + + private void cacheRecentIdentity(String ip, PendingConnection conn, long now) { + if (ip == null || ip.isEmpty() || conn == null) return; + String name = extractPlayerName(conn); + String uuid = extractPlayerUuid(conn, name); + if ((name == null || name.isEmpty()) && (uuid == null || uuid.isEmpty())) return; + RecentPlayerIdentity identity = recentIdentityByIp.computeIfAbsent(ip, k -> new RecentPlayerIdentity()); + synchronized (identity) { + if (name != null && !name.trim().isEmpty()) identity.playerName = name.trim(); + if (uuid != null && !uuid.trim().isEmpty()) identity.playerUuid = uuid.trim(); + identity.updatedAtMs = now; + } + } + + private void cacheRecentIdentityDirect(String ip, String playerName, UUID playerUuid, long now) { + if (ip == null || ip.isEmpty()) return; + String name = playerName == null ? "" : playerName.trim(); + String uuid = playerUuid == null ? "" : playerUuid.toString(); + if (name.isEmpty() && uuid.isEmpty()) return; + RecentPlayerIdentity identity = recentIdentityByIp.computeIfAbsent(ip, k -> new RecentPlayerIdentity()); + synchronized (identity) { + if (!name.isEmpty()) identity.playerName = name; + if (!uuid.isEmpty()) identity.playerUuid = uuid; + identity.updatedAtMs = now; + } + } + + private String extractPlayerName(PendingConnection conn) { + if (conn == null) return ""; + try { String raw = conn.getName(); return raw == null ? "" : raw.trim(); } catch (Exception ignored) { return ""; } + } + + private String extractPlayerUuid(PendingConnection conn, String playerName) { + if (conn != null) { + try { UUID uuid = conn.getUniqueId(); if (uuid != null) return uuid.toString(); } catch (Exception ignored) {} + } + if (playerName != null && !playerName.trim().isEmpty()) { + return UUID.nameUUIDFromBytes(("OfflinePlayer:" + playerName.trim()).getBytes(StandardCharsets.UTF_8)).toString(); + } + return ""; + } + + private String safeLog(String input) { + if (input == null || input.isEmpty()) return "-"; + return input.replace("\n", " ").replace("\r", " ").trim(); + } + + private List getRecentLearningEvents(int max) { + List out = new ArrayList<>(); + synchronized (learningRecentEvents) { + int skip = Math.max(0, learningRecentEvents.size() - Math.max(1, max)); + int idx = 0; + for (String line : learningRecentEvents) { + if (idx++ < skip) continue; + out.add(line); + } + } + return out; + } + + private boolean parseBoolean(String s, boolean fallback) { + if (s == null) return fallback; + return Boolean.parseBoolean(s.trim()); + } + + private int parseInt(String s, int fallback) { + try { return Integer.parseInt(s == null ? "" : s.trim()); } catch (Exception ignored) { return fallback; } + } + + private VpnCheckResult getVpnInfo(String ip, long now) { + VpnCacheEntry cached = vpnCache.get(ip); + if (cached != null && cached.expiresAt > now) return cached.result; + VpnCheckResult fresh = requestIpApi(ip); + if (fresh != null) { + VpnCacheEntry entry = new VpnCacheEntry(); + entry.result = fresh; + entry.expiresAt = now + Math.max(1, vpnCacheMinutes) * 60_000L; + vpnCache.put(ip, entry); + } + return fresh; + } + + private VpnCheckResult requestIpApi(String ip) { + HttpURLConnection conn = null; + try { + String url = "http://ip-api.com/json/" + ip + "?fields=status,proxy,hosting"; + conn = (HttpURLConnection) new URL(url).openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(vpnTimeoutMs); + conn.setReadTimeout(vpnTimeoutMs); + conn.setRequestProperty("User-Agent", "StatusAPI-AntiBot/1.0"); + if (conn.getResponseCode() < 200 || conn.getResponseCode() >= 300) return null; + StringBuilder sb = new StringBuilder(); + try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) { + String line; while ((line = br.readLine()) != null) sb.append(line); + } + String json = sb.toString(); + if (json.isEmpty() || !json.contains("\"status\":\"success\"")) return null; + VpnCheckResult result = new VpnCheckResult(); + result.proxy = json.contains("\"proxy\":true"); + result.hosting = json.contains("\"hosting\":true"); + return result; + } catch (Exception ignored) { return null; } + finally { if (conn != null) conn.disconnect(); } + } + + // --- Interne Klassen --- + + private static class IpWindow { + long windowStart; int count; + IpWindow(long now) { this.windowStart = now; this.count = 0; } + } + + private static class VpnCacheEntry { VpnCheckResult result; long expiresAt; } + private static class VpnCheckResult { boolean proxy; boolean hosting; } + + private static class LearningProfile { + long lastConnectionAt, lastScoreUpdateAt, lastSeenAt; + int rapidStreak, score; + LearningProfile(long now) { lastConnectionAt = lastScoreUpdateAt = lastSeenAt = now; } + } + + private static class RecentPlayerIdentity { String playerName; String playerUuid; long updatedAtMs; } + + // --- Command --- + + private class AntiBotCommand extends Command { + AntiBotCommand() { super("antibot", "statusapi.antibot"); } + + @Override + public void execute(CommandSender sender, String[] args) { + if (!enabled) { sender.sendMessage(ChatColor.RED + "AntiBotModule ist deaktiviert."); return; } + + if (args.length == 0 || "status".equalsIgnoreCase(args[0])) { + sender.sendMessage(ChatColor.GOLD + "----- AntiBot Status -----"); + sender.sendMessage(ChatColor.YELLOW + "Schutz aktiv: " + ChatColor.WHITE + enabled); + sender.sendMessage(ChatColor.YELLOW + "Profil: " + ChatColor.WHITE + profile); + if (attackMode) sender.sendMessage(ChatColor.YELLOW + "Attack Mode: " + ChatColor.RED + "AKTIV"); + else sender.sendMessage(ChatColor.YELLOW + "Attack Mode: " + ChatColor.GREEN + "Normal"); + sender.sendMessage(ChatColor.YELLOW + "CPS: " + ChatColor.WHITE + lastCps + ChatColor.GRAY + " (Peak " + peakCps.get() + ")"); + sender.sendMessage(ChatColor.YELLOW + "Schwellen: " + ChatColor.WHITE + "start " + attackStartCps + ChatColor.GRAY + " / " + ChatColor.WHITE + "stop " + attackStopCps + ChatColor.GRAY + " CPS"); + sender.sendMessage(ChatColor.YELLOW + "Active IP Blocks: " + ChatColor.WHITE + blockedIpsUntil.size()); + sender.sendMessage(ChatColor.YELLOW + "Total blocked connections: " + ChatColor.WHITE + blockedConnectionsTotal.get()); + sender.sendMessage(ChatColor.YELLOW + "VPN Check: " + ChatColor.WHITE + vpnCheckEnabled); + sender.sendMessage(ChatColor.YELLOW + "Learning Mode: " + ChatColor.WHITE + learningModeEnabled + + ChatColor.GRAY + " (threshold=" + learningScoreThreshold + ")"); + List recent = getRecentLearningEvents(3); + if (!recent.isEmpty()) { + sender.sendMessage(ChatColor.YELLOW + "Learning Events:"); + for (String line : recent) sender.sendMessage(ChatColor.GRAY + "- " + line); + } + return; + } + + if ("clearblocks".equalsIgnoreCase(args[0])) { + blockedIpsUntil.clear(); + sender.sendMessage(ChatColor.GREEN + "Alle IP-Blocks wurden entfernt."); + return; + } + + if ("unblock".equalsIgnoreCase(args[0]) && args.length >= 2) { + String ip = args[1].trim(); + if (blockedIpsUntil.remove(ip) != null) sender.sendMessage(ChatColor.GREEN + "IP entblockt: " + ip); + else sender.sendMessage(ChatColor.RED + "IP war nicht geblockt: " + ip); + return; + } + + if ("profile".equalsIgnoreCase(args[0])) { + if (args.length < 2) { + sender.sendMessage(ChatColor.YELLOW + "Aktuelles Profil: " + ChatColor.WHITE + profile); + sender.sendMessage(ChatColor.YELLOW + "Benutzung: /antibot profile "); + return; + } + String requested = args[1].trim().toLowerCase(Locale.ROOT); + if (!isSupportedProfile(requested)) { sender.sendMessage(ChatColor.RED + "Unbekanntes Profil. Erlaubt: strict, high-traffic"); return; } + boolean ok = applyProfileAndPersist(requested); + if (!ok) { sender.sendMessage(ChatColor.RED + "Profil konnte nicht gespeichert werden."); return; } + sender.sendMessage(ChatColor.GREEN + "AntiBot-Profil umgestellt auf: " + requested); + sender.sendMessage(ChatColor.GRAY + "Werte wurden in " + CONFIG_FILE_NAME + " gespeichert."); + return; + } + + if ("reload".equalsIgnoreCase(args[0])) { + reloadRuntimeState(); + sender.sendMessage(ChatColor.GREEN + "AntiBot-Konfiguration neu geladen."); + sender.sendMessage(ChatColor.GRAY + "Aktives Profil: " + profile); + return; + } + + sender.sendMessage(ChatColor.YELLOW + "/antibot status"); + sender.sendMessage(ChatColor.YELLOW + "/antibot clearblocks"); + sender.sendMessage(ChatColor.YELLOW + "/antibot unblock "); + sender.sendMessage(ChatColor.YELLOW + "/antibot profile "); + sender.sendMessage(ChatColor.YELLOW + "/antibot reload"); + } + } +}