diff --git a/src/main/java/net/viper/status/modules/antibot/AntiBotModule.java b/src/main/java/net/viper/status/modules/antibot/AntiBotModule.java new file mode 100644 index 0000000..b58dcef --- /dev/null +++ b/src/main/java/net/viper/status/modules/antibot/AntiBotModule.java @@ -0,0 +1,672 @@ +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.event.PreLoginEvent; +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.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +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.util.ArrayList; +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.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Eigenstaendige AntiBot/Attack-Guard Funktionen, angelehnt an BetterBungee-Ideen. + */ +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 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(); + + @Override + public String getName() { + return "AntiBotModule"; + } + + @Override + public void onEnable(Plugin plugin) { + if (!(plugin instanceof StatusAPI)) { + return; + } + this.plugin = (StatusAPI) plugin; + ensureModuleConfigExists(); + loadConfig(); + + if (!enabled) { + this.plugin.getLogger().info("[AntiBotModule] deaktiviert via " + CONFIG_FILE_NAME + " (antibot.enabled=false)"); + 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(); + blockedIpsCurrentAttack.clear(); + attackMode = false; + } + + public boolean isEnabled() { + return enabled; + } + + private void reloadRuntimeState() { + perIpWindows.clear(); + blockedIpsUntil.clear(); + vpnCache.clear(); + blockedIpsCurrentAttack.clear(); + attackMode = false; + attackCalmSince = 0L; + blockedConnectionsCurrentAttack.set(0L); + currentSecondConnections.set(0); + lastCps = 0; + peakCps.set(0); + loadConfig(); + } + + 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("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); + return m; + } + + @EventHandler + public void onPreLogin(PreLoginEvent event) { + if (!enabled) { + return; + } + + String ip = extractIp(event.getConnection()); + if (ip == null || ip.isEmpty()) { + return; + } + + recordConnection(); + long now = System.currentTimeMillis(); + + cleanupExpired(now); + + Long blockedUntil = blockedIpsUntil.get(ip); + if (blockedUntil != null && blockedUntil > now) { + blockEvent(event); + return; + } + + if (isIpRateExceeded(ip, now)) { + blockIp(ip, now); + blockEvent(event); + return; + } + + if (vpnCheckEnabled) { + VpnCheckResult info = getVpnInfo(ip, now); + if (info != null) { + boolean shouldBlock = (vpnBlockProxy && info.proxy) || (vpnBlockHosting && info.hosting); + if (shouldBlock) { + blockIp(ip, now); + blockEvent(event); + } + } + } + } + + 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(); + if (sa.getAddress() != null) { + return sa.getAddress().getHostAddress(); + } + return sa.getHostString(); + } + return String.valueOf(conn.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); + } + } + + private void cleanupExpired(long now) { + for (Map.Entry entry : blockedIpsUntil.entrySet()) { + if (entry.getValue() <= now) { + blockedIpsUntil.remove(entry.getKey()); + } + } + + for (Map.Entry entry : vpnCache.entrySet()) { + if (entry.getValue().expiresAt <= now) { + vpnCache.remove(entry.getKey()); + } + } + } + + 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); + } 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 value = raw.trim().toLowerCase(Locale.ROOT); + if ("strict".equals(value)) { + return "strict"; + } + return "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; + return; + } + + 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 value = raw.trim().toLowerCase(Locale.ROOT); + return "strict".equals(value) || "high-traffic".equals(value); + } + + 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++) { + String current = lines.get(i).trim(); + if (current.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 " + CONFIG_FILE_NAME + " nicht im JAR gefunden."); + return; + } + byte[] buffer = new byte[4096]; + int read; + while ((read = in.read(buffer)) != -1) { + out.write(buffer, 0, read); + } + plugin.getLogger().info("[AntiBotModule] " + CONFIG_FILE_NAME + " wurde erstellt."); + } catch (Exception e) { + plugin.getLogger().warning("[AntiBotModule] Konnte " + CONFIG_FILE_NAME + " nicht erstellen: " + e.getMessage()); + } + } + + 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"); + + int code = conn.getResponseCode(); + if (code < 200 || code >= 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(); + } + } + } + + 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 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 + ChatColor.GRAY + " (Modul eingeschaltet)"); + if (attackMode) { + sender.sendMessage(ChatColor.YELLOW + "Attack Mode: " + ChatColor.RED + "AKTIV" + ChatColor.GRAY + " (Angriff erkannt)"); + } else { + sender.sendMessage(ChatColor.YELLOW + "Attack Mode: " + ChatColor.GREEN + "Normal" + ChatColor.GRAY + " (kein Angriff erkannt)"); + } + 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); + 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(); + Long removed = blockedIpsUntil.remove(ip); + if (removed != 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. Siehe Konsole."); + 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"); + } + } +}