Dateien nach "src/main/java/net/viper/status/modules/antibot" hochladen

This commit is contained in:
2026-04-02 06:26:59 +00:00
parent 4a7a4c69cd
commit 4ada8fcde6

View File

@@ -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<String> blockedIpsCurrentAttack = ConcurrentHashMap.newKeySet();
private final Map<String, IpWindow> perIpWindows = new ConcurrentHashMap<String, IpWindow>();
private final Map<String, Long> blockedIpsUntil = new ConcurrentHashMap<String, Long>();
private final Map<String, VpnCacheEntry> vpnCache = new ConcurrentHashMap<String, VpnCacheEntry>();
@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<String, Object> buildSnapshot() {
Map<String, Object> out = new LinkedHashMap<String, Object>();
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<String, Object> buildThresholds() {
Map<String, Object> m = new LinkedHashMap<String, Object>();
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<String, Long> entry : blockedIpsUntil.entrySet()) {
if (entry.getValue() <= now) {
blockedIpsUntil.remove(entry.getKey());
}
}
for (Map.Entry<String, VpnCacheEntry> 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<String, String> values = new LinkedHashMap<String, String>();
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<String, String> keyValues) throws Exception {
File target = new File(plugin.getDataFolder(), CONFIG_FILE_NAME);
List<String> lines = target.exists()
? Files.readAllLines(target.toPath(), StandardCharsets.UTF_8)
: new ArrayList<String>();
for (Map.Entry<String, String> 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 <strict|high-traffic>");
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 <ip>");
sender.sendMessage(ChatColor.YELLOW + "/antibot profile <strict|high-traffic>");
sender.sendMessage(ChatColor.YELLOW + "/antibot reload");
}
}
}